#!/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()