aboutsummaryrefslogtreecommitdiff
path: root/scripts/course-weekly-report.py
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-10-18 14:35:09 +0200
committerAgando <roduccomm@gmail.com>2026-10-18 14:35:09 +0200
commit31a295012f418cfdef2d9c219baedfee1940aa4b (patch)
tree3444bc7d17f49d4008a17570f7785e4cc80e4467 /scripts/course-weekly-report.py
parent4d3fa20151e101d6486d5b57935370d93e0fcffe (diff)
course: weekly telemetry report script + Monday 09:00 cron
Diffstat (limited to 'scripts/course-weekly-report.py')
-rw-r--r--scripts/course-weekly-report.py66
1 files changed, 66 insertions, 0 deletions
diff --git a/scripts/course-weekly-report.py b/scripts/course-weekly-report.py
new file mode 100644
index 0000000..de18457
--- /dev/null
+++ b/scripts/course-weekly-report.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python3
+"""Weekly newOS4 course telemetry report (last 7 days). No PII, aggregate only."""
+import json, glob, os
+from collections import Counter
+from datetime import datetime, timezone, timedelta
+
+ROOT = '/var/www/webx11.duckdns.org/spaces/newos4'
+DATA = os.path.join(ROOT, 'data', 'course')
+COURSE = os.path.join(ROOT, 'course')
+
+def load(p, d):
+ try:
+ with open(p, encoding='utf-8') as f: return json.load(f)
+ except Exception: return d
+
+def main():
+ cutoff = (datetime.now(timezone.utc) - timedelta(days=7)).strftime('%Y-%m-%dT%H:%M:%S')
+ opens, completes, fails, questions, cmds = Counter(), Counter(), Counter(), Counter(), Counter()
+ sessions = set(); projects = Counter(); total = 0
+ for path in sorted(glob.glob(os.path.join(DATA, 'events-*.jsonl')))[-2:]:
+ for line in open(path, encoding='utf-8', errors='ignore'):
+ try: e = json.loads(line)
+ except Exception: continue
+ sid = e.get('sid') or ''
+ if sid.startswith('test') or (e.get('ts') or '') < cutoff: continue
+ total += 1; sessions.add(sid)
+ t, d = e.get('type'), e.get('detail') or {}
+ lesson = (d.get('lesson') or '')[:20]
+ if t == 'lesson_open' and lesson: opens[lesson] += 1
+ elif t == 'lesson_complete' and lesson: completes[lesson] += 1
+ elif t == 'exercise_fail':
+ if lesson: fails[lesson] += 1
+ c = (d.get('command') or '').strip().split(' ')[0][:30]
+ if c: cmds[c] += 1
+ elif t == 'question':
+ q = ' '.join((d.get('q') or '').lower().split())[:120]
+ if len(q) >= 8: questions[q] += 1
+ elif t in ('project_open', 'project_view') and d.get('project'):
+ projects[d['project'][:30]] += 1
+
+ feed = load(os.path.join(COURSE, 'feed.json'), {'items': []})
+ week = (datetime.now(timezone.utc) - timedelta(days=7)).strftime('%Y-%m-%d')
+ new_items = [i for i in feed.get('items', []) if (i.get('date') or '') >= week]
+ lessons = []
+ for lid in sorted(set(opens) | set(completes)):
+ o, c = opens[lid], completes[lid]
+ lessons.append((lid, o, c, round(c / o, 2) if o else 0.0))
+ weak = [x for x in lessons if x[1] >= 3 and x[3] < 0.3]
+
+ c_total = sum(completes.values())
+ print('๐Ÿ“š newOS4 Course โ€” weekly report (' + datetime.now(timezone.utc).strftime('%Y-%m-%d') + ')')
+ print('Sessions: ' + str(len(sessions)) + ' | events: ' + str(total) + ' | opens: ' + str(sum(opens.values())) + ' | completes: ' + str(c_total))
+ rate = round(c_total / sum(opens.values()), 2) if sum(opens.values()) else 0.0
+ print('Overall completion: ' + str(rate) + ' | exercise failures: ' + str(sum(fails.values())) + ' | questions: ' + str(sum(questions.values())))
+ if lessons:
+ print('Lessons: ' + ' ยท '.join(f'{l} {c}/{o}' for l, o, c, r in lessons[:10]))
+ print('Projects: ' + (', '.join(f'{k} {v}' for k, v in projects.items()) or 'none'))
+ print('Content flags: ' + (', '.join(f'{l} {r:.0%} of {o}' for l, o, c, r in weak) or 'none'))
+ print('Top questions: ' + (' | '.join(f'"{q}" x{n}' for q, n in questions.most_common(3)) or 'none'))
+ print('Failed commands: ' + (', '.join(f'{c} x{n}' for c, n in cmds.most_common(3)) or 'none'))
+ print('Daily feed: ' + str(len(feed.get('items', []))) + '/60 (' + str(len(new_items)) + ' new this week)')
+ if len(sessions) < 20:
+ print('Gate: paid Pro track still OFF โ€” need >=20 sessions and >=35% completion through 3.1 over 14 days.')
+
+if __name__ == '__main__':
+ main()