aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-10-23 15:20:38 +0200
committerAgando <roduccomm@gmail.com>2026-10-23 15:20:38 +0200
commit7c48b16cb59a514217e275b0a32cece3c74d2a24 (patch)
treeaf5c8b3e4f89367e7b07e5d4a35801484125dab5
parent316a34d15d82b6c928bfb1e3670c052d55aa9875 (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-xscripts/newos4-steer.py99
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()