Phase 9: Admin completion, reconcile, notifications, integrity, backups
Admin: Departments (name and stations; add shows the three groups IT must create), Profiles (welder stamp, station limits, active) with a CSV import that previews and applies only when clean, Checklists (weld and plain), Security (session, lockout and rate-limit settings with bounds, alerts for the last 24 hours, the integrity result), Access log with filters and a guarded CSV, Who is in, Stamp check, Notifications, and the employeeID flags. Every Admin change is audited. Worker: ad-reconcile (nightly and on "refresh from AD now"), session-refresh (15 minutes), integrity (seals, asset hashes, missing and orphan files, the audit chain), backup-check, notify-send and notify-digest. Notifications route per event type to AD groups, users, mailboxes or the department Supervisor group, immediately or in a daily digest, over STARTTLS verified against the PESCO CA, with dry run on the test stack. Backups: scripts/backup/ps-backup.sh and ps-restore.sh with a systemd timer at 02:00 America/Denver. Restore rehearsed on the test stack 2026-10-21. CI: pip-audit and npm audit. pypdf 6.21.1 and certifi 2026.10.19. Migration 0010. Closes the app part of I6, M3, O1, O2 and S3. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
"""notify-send, every 60 seconds: seed the rules, note a new build, run the watch checks (at most every 15 minutes)
|
||||
and deliver the events nobody has handled yet. Not pausable."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time as clock
|
||||
from datetime import datetime
|
||||
|
||||
from ...feed.config import FeedSettings
|
||||
from ...notify import seed as seed_rules
|
||||
from ...notify import send, watch
|
||||
from . import Counts, JobContext, JobSpec, directory, reload_departments
|
||||
|
||||
EVERY_SECONDS = 60
|
||||
WATCH_SECONDS = 15 * 60
|
||||
WATCH_KEY = "notify.watch_at"
|
||||
BUILD_KEY = "notify.build_noted"
|
||||
|
||||
|
||||
def _due(ctx: JobContext, key: str, seconds: float) -> bool:
|
||||
"""True once per `seconds` per worker process; marks the time when it says so."""
|
||||
now = clock.monotonic()
|
||||
with ctx.stack.lock:
|
||||
last = ctx.stack.cache.get(key)
|
||||
if last is not None and now - float(last) < seconds:
|
||||
return False
|
||||
ctx.stack.cache[key] = now
|
||||
return True
|
||||
|
||||
|
||||
def run(ctx: JobContext) -> Counts:
|
||||
conn, s = ctx.conn, ctx.settings
|
||||
counts: Counts = {}
|
||||
added = seed_rules(conn, s)
|
||||
if added:
|
||||
counts["seeded"] = len(added)
|
||||
if _due(ctx, BUILD_KEY, float("inf")):
|
||||
try:
|
||||
if watch.note_build(conn, s.ps_build) is not None:
|
||||
counts["version_deployed"] = s.ps_build
|
||||
except Exception as e:
|
||||
ctx.stack.drop(BUILD_KEY) # try again on the next run
|
||||
ctx.error(f"version check: {type(e).__name__}", degrade=False)
|
||||
if _due(ctx, WATCH_KEY, WATCH_SECONDS):
|
||||
for name, check in (("drawings", watch.check_drawings), ("change orders", watch.check_change_orders)):
|
||||
try:
|
||||
counts.update(check(conn))
|
||||
except Exception as e:
|
||||
ctx.log.exception("watch check %s failed", name)
|
||||
ctx.error(f"watch {name}: {type(e).__name__}")
|
||||
pending = conn.execute(
|
||||
"SELECT EXISTS (SELECT 1 FROM notification_events WHERE dispatched_at IS NULL AND event_type IS NOT NULL)"
|
||||
).fetchone()[0]
|
||||
if pending:
|
||||
reload_departments(conn)
|
||||
out = send.dispatch_pending(conn, s, directory(ctx))
|
||||
counts.update(out)
|
||||
if out.get("failed") or out.get("retry"):
|
||||
ctx.degrade(f"{out.get('failed', 0)} failed, {out.get('retry', 0)} waiting for the directory")
|
||||
return counts
|
||||
|
||||
|
||||
def _cadence(_: FeedSettings, __: datetime) -> int:
|
||||
return EVERY_SECONDS
|
||||
|
||||
|
||||
SPEC = JobSpec(name="notify-send", run=run, cadence=_cadence, pausable=False)
|
||||
Reference in New Issue
Block a user