aboutsummaryrefslogtreecommitdiff
path: root/scripts/newos4-steer.py
blob: 1e387f8a0fcea8ac9213e3d09fa67f09b0224bc2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
#!/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 subprocess
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'); d.add_argument('--commit', default='')
    r = sub.add_parser('reopen'); r.add_argument('id'); r.add_argument('--reason', default='')
    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("no such id: " + args.id)
            if not args.commit:
                raise SystemExit('refusing: --commit <sha> required - a written plan is not a ship')
            chk = subprocess.run(['git', '-C', '/var/www/webx11.duckdns.org', 'cat-file', '-e',
                                  args.commit + '^{commit}'], capture_output=True, text=True)
            if chk.returncode != 0:
                raise SystemExit('refusing: ' + args.commit + ' is not a commit in the repo')
            hit[0]['done'] = True
            hit[0]['done_at'] = time.strftime('%Y-%m-%dT%H:%M:%S+00:00', time.gmtime())
            hit[0]['done_commit'] = args.commit
            _save(data)
            print('done:', args.id, 'at', args.commit)
        elif args.cmd == 'reopen':
            hit = [r for r in data if r.get('id') == args.id]
            if not hit:
                raise SystemExit("no such id: " + args.id)
            hit[0]['done'] = False
            hit[0].pop('done_at', None)
            hit[0].pop('done_commit', None)
            hit[0]['reopened_at'] = time.strftime('%Y-%m-%dT%H:%M:%S+00:00', time.gmtime())
            hit[0]['reopen_reason'] = (args.reason or '')[:300]
            _save(data)
            print('reopened:', 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()