From 0d0d936e7b5cd41a31773069540b1ddb8836ef44 Mon Sep 17 00:00:00 2001 From: Agando Date: Sun, 18 Oct 2026 14:05:18 +0200 Subject: newOS4: interactive AI course (terminal module, portal, atlas, telemetry, daily maintenance+cron) + free store card --- scripts/newos4-maintain.py | 118 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 scripts/newos4-maintain.py (limited to 'scripts/newos4-maintain.py') diff --git a/scripts/newos4-maintain.py b/scripts/newos4-maintain.py new file mode 100644 index 0000000..6321a3f --- /dev/null +++ b/scripts/newos4-maintain.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""newOS4 maintenance: drift, bundles, syntax, perms, data hygiene, live health. +Safe fixes only with --fix. Never touches nginx, services, sudo, or user content.""" +import glob, json, os, re, subprocess, sys +from datetime import datetime, timezone + +ROOT = '/var/www/webx11.duckdns.org/spaces/newos4' +WEB = 'https://webx11.duckdns.org/spaces/newos4' +FIX = '--fix' in sys.argv +DEEP = '--deep' in sys.argv +issues = [] +def add(l, m): issues.append((l, m)) +def run(cmd, **kw): return subprocess.run(cmd, capture_output=True, text=True, **kw) +def sh(cmd): return run(cmd, shell=True).stdout.strip() + +def check_versions(): + html = open(os.path.join(ROOT, 'main.html'), encoding='utf-8').read() + loader = open(os.path.join(ROOT, 'modules/core/module-loader.js'), encoding='utf-8').read() + m = re.search(r"const LATEST_VER\s*=\s*'([^']+)'", loader) + ver = m.group(1) if m else '?' + n = html.count(ver) + add('PASS' if n == 3 else 'FAIL', f'version {ver} x{n} in main.html (expect 3)') + +def check_bundles(): + paths = ('modules/core/core-bundle.js', 'modules/ext/critical-ext-bundle.js') + before = {p: sh(f'md5sum {p}') for p in paths} + if not FIX: + add('PASS', 'bundle check skipped (use --fix)'); return + r = run([sys.executable, 'scripts/rebuild-bundles.py'], cwd=ROOT) + after = {p: sh(f'md5sum {p}') for p in paths} + changed = [p for p in paths if before[p] != after[p]] + add('FAIL' if r.returncode else ('WARN' if changed else 'PASS'), + 'bundles ' + ('FAILED' if r.returncode else ('rebuilt: ' + ', '.join(changed) if changed else 'clean'))) + +def check_syntax(): + bad = [] + for p in glob.glob(os.path.join(ROOT, 'modules/core/*.js')) + glob.glob(os.path.join(ROOT, 'modules/ext/*.js')): + if os.path.basename(p) in ('core-bundle.js', 'critical-ext-bundle.js'): continue + if run(['node', '--check', p]).returncode: bad.append(os.path.basename(p)) + add('PASS' if not bad else 'FAIL', 'syntax ' + ('OK' if not bad else 'fail: ' + ', '.join(bad))) + +def check_perms(): + targets = glob.glob(os.path.join(ROOT, 'course/*.json')) + glob.glob(os.path.join(ROOT, 'course/chapters/*.json')) \ + + [os.path.join(ROOT, 'modules/ext/course.js'), os.path.join(ROOT, 'course.html'), os.path.join(ROOT, 'api/course/event.php')] + bad = [] + for p in targets: + if not os.path.exists(p): continue + if (os.stat(p).st_mode & 0o777) != 0o644 or sh(f'stat -c %G {p}') != 'www-data': + if FIX: + os.chmod(p, 0o644); sh(f'chgrp www-data {p}') + else: bad.append(os.path.basename(p)) + add('PASS' if not bad else ('WARN' if FIX else 'FAIL'), 'perms ' + ('clean' if not bad else ('fixed' if FIX else 'bad: ' + ', '.join(bad[:5])))) + +def check_index(): + idxp = os.path.join(ROOT, 'modules-index.json') + atlas = json.load(open(os.path.join(ROOT, 'course/atlas.json'))) + idx = json.load(open(idxp)) + cur = len(set(c for m in idx.get('modules', {}).values() for c in m.get('commands', []))) + if cur == atlas['coverage']['commands']: + add('PASS', f'modules-index {cur} commands'); return + add('WARN', f'modules-index stale {cur} vs {atlas["coverage"]["commands"]}') + if not FIX: return + for name, m in atlas['modules'].items(): + e = idx['modules'].get(name, {'name': name, 'file': m['file'], 'type': 'core' if m['tier'] == 'core-bundle' else 'ext', + 'class': '', 'auto_load': m['tier'] != 'ext-manual', 'description': ''}) + e.update({'commands': m['commands'], 'file': m['file'], 'tier': m['tier']}) + idx['modules'][name] = e + idx['version'] = datetime.now(timezone.utc).strftime('%Y-%m-%d') + '-atlas' + tmp = idxp + '.tmp' + with open(tmp, 'w', encoding='utf-8') as f: json.dump(idx, f, ensure_ascii=False, indent=1) + os.replace(tmp, idxp); os.chmod(idxp, 0o644); sh(f'chgrp www-data {idxp}') + add('PASS', 'modules-index regenerated') + +def check_data(): + err = os.path.join(ROOT, 'data/debug/errors.json') + if os.path.exists(err) and os.path.getsize(err) > 5 * 1024 * 1024: + if FIX: + arc = os.path.join(ROOT, 'data/debug/archive'); os.makedirs(arc, exist_ok=True) + os.replace(err, os.path.join(arc, 'errors-' + datetime.now().strftime('%Y%m') + '.json')) + open(err, 'w').write('[]'); os.chmod(err, 0o664); sh(f'chgrp www-data {err}') + add('PASS', 'errors.json rotated') + else: add('WARN', 'errors.json >5MB') + else: add('PASS', 'debug data OK') + +def check_live(): + for url in (f'{WEB}/main.html', f'{WEB}/course.html', f'{WEB}/course/course.json', f'{WEB}/modules/core/core-bundle.js'): + code = sh(f"curl -s -o /dev/null -w '%{{http_code}}' '{url}'") + add('PASS' if code == '200' else 'FAIL', f'HTTP {code} {url.replace(WEB, "")}') + +def check_course(): + r = run([sys.executable, 'scripts/course-validate.py'], cwd=ROOT) + add('PASS' if r.returncode == 0 else 'FAIL', 'course ' + ('valid' if r.returncode == 0 else 'INVALID: ' + r.stdout[-160:])) + +def check_links(): + if not DEEP: return + broken = [] + for page in ('main.html', 'course.html', 'help.html', 'modules.html', 'guide.html'): + p = os.path.join(ROOT, page) + if not os.path.exists(p): continue + for href in re.findall(r'(?:href|src)="([^"#]+)"', open(p, encoding='utf-8').read()): + if any(x in href for x in ("'", '+', '{', '}')): continue + if href.startswith(('http', '//', 'mailto:', 'javascript:', 'data:', '/api/')): continue + target = os.path.normpath(os.path.join(ROOT, href.split('?')[0])) + if not os.path.exists(target): broken.append(f'{page}:{href}') + add('PASS' if not broken else 'WARN', 'links ' + ('OK' if not broken else f'{len(broken)} broken (first: {broken[0]})')) + +def main(): + os.chdir(ROOT) + check_versions(); check_bundles(); check_syntax(); check_perms(); check_index(); check_data(); check_live(); check_course(); check_links() + fails = [m for l, m in issues if l == 'FAIL'] + print('newOS4 maintenance — ' + datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%MZ') + (' [fix]' if FIX else '') + (' [deep]' if DEEP else '')) + for l, m in issues: print(f'{l}: {m}') + print('maintenance result: ' + ('FAIL' if fails else 'OK')) + print('needs OK: ' + ('none' if not fails else '; '.join(fails)[:400])) + return 1 if fails else 0 + +if __name__ == '__main__': + sys.exit(main()) -- cgit v1.2.3