diff options
| author | Agando <roduccomm@gmail.com> | 2026-10-23 15:20:38 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-10-23 15:20:38 +0200 |
| commit | 7c48b16cb59a514217e275b0a32cece3c74d2a24 (patch) | |
| tree | af5c8b3e4f89367e7b07e5d4a35801484125dab5 | |
| parent | 316a34d15d82b6c928bfb1e3670c052d55aa9875 (diff) | |
feat(newos4): operator steering queue for the maintenance jobs
newos4-steer.py add|list|done|clear appends directives under flock with unique ids; both cron jobs read it first and mark entries consumed. Verified: two adds with distinct ids, done only affects the named entry.
| -rwxr-xr-x | scripts/newos4-steer.py | 99 |
1 files changed, 99 insertions, 0 deletions
diff --git a/scripts/newos4-steer.py b/scripts/newos4-steer.py new file mode 100755 index 0000000..de3b87c --- /dev/null +++ b/scripts/newos4-steer.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""Operator steering queue for the newOS4 maintenance/implementation jobs. + +The operator (or Hermes) appends directives here; both cron jobs read this file FIRST and +prioritise whatever it says over their own findings. Consumed entries are marked, not deleted, +so the history of "what was asked for and when" survives. + + newos4-steer.py add "fix the download links on mobile" + newos4-steer.py list # pending only + newos4-steer.py list --all + newos4-steer.py done <id> # mark consumed (the jobs do this themselves) + newos4-steer.py clear # mark all pending as consumed +""" +import argparse +import fcntl +import json +import os +import time + +ROOT = '/var/www/webx11.duckdns.org/spaces/newos4' +DIR = os.path.join(ROOT, 'data', 'maintenance') +FILE = os.path.join(DIR, 'steering.json') +MAX = 100 + + +def _load(): + os.makedirs(DIR, exist_ok=True) + try: + with open(FILE, encoding='utf-8') as f: + data = json.load(f) + return data if isinstance(data, list) else [] + except Exception: + return [] + + +def _save(data): + tmp = FILE + '.tmp' + with open(tmp, 'w', encoding='utf-8') as f: + json.dump(data[-MAX:], f, indent=2) + f.flush() + os.fsync(f.fileno()) + os.replace(tmp, FILE) + os.chmod(FILE, 0o664) + + +def main(): + ap = argparse.ArgumentParser() + sub = ap.add_subparsers(dest='cmd', required=True) + a = sub.add_parser('add'); a.add_argument('text'); a.add_argument('--priority', default='normal', choices=['high', 'normal', 'low']) + sub.add_parser('list').add_argument('--all', action='store_true') + d = sub.add_parser('done'); d.add_argument('id') + sub.add_parser('clear') + args = ap.parse_args() + + lock = open(FILE + '.lock', 'a+') + fcntl.flock(lock, fcntl.LOCK_EX) + try: + data = _load() + if args.cmd == 'add': + entry = { + 'id': 'STEER-' + time.strftime('%Y%m%d-%H%M%S') + '-' + os.urandom(2).hex(), + 'created_at': time.strftime('%Y-%m-%dT%H:%M:%S+00:00', time.gmtime()), + 'priority': args.priority, + 'text': args.text.strip()[:1000], + 'done': False, + } + data.append(entry) + _save(data) + print(entry['id']) + elif args.cmd == 'list': + rows = [r for r in data if args.all or not r.get('done')] + if not rows: + print('(no pending steering directives)') + for r in rows: + print(f"{r['id']} [{r.get('priority','normal')}] {r.get('text','')}") + elif args.cmd == 'done': + hit = [r for r in data if r.get('id') == args.id] + if not hit: + raise SystemExit(f"no such id: {args.id}") + hit[0]['done'] = True + hit[0]['done_at'] = time.strftime('%Y-%m-%dT%H:%M:%S+00:00', time.gmtime()) + _save(data) + print('done:', args.id) + elif args.cmd == 'clear': + n = 0 + for r in data: + if not r.get('done'): + r['done'] = True + r['done_at'] = time.strftime('%Y-%m-%dT%H:%M:%S+00:00', time.gmtime()) + n += 1 + _save(data) + print('cleared:', n) + finally: + fcntl.flock(lock, fcntl.LOCK_UN) + lock.close() + + +if __name__ == '__main__': + main() |