diff options
| author | Agando <roduccomm@gmail.com> | 2026-10-18 14:35:09 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-10-18 14:35:09 +0200 |
| commit | 31a295012f418cfdef2d9c219baedfee1940aa4b (patch) | |
| tree | 3444bc7d17f49d4008a17570f7785e4cc80e4467 | |
| parent | 4d3fa20151e101d6486d5b57935370d93e0fcffe (diff) | |
course: weekly telemetry report script + Monday 09:00 cron
| -rw-r--r-- | scripts/course-weekly-report.py | 66 |
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() |