@matt82198/aesop 0.1.0-beta.3 → 0.1.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -95
- package/CLAUDE-TEMPLATE.md +115 -0
- package/README.md +126 -245
- package/aesop.config.example.json +34 -26
- package/bin/CLAUDE.md +67 -0
- package/bin/cli.js +593 -36
- package/daemons/CLAUDE.md +36 -0
- package/daemons/backup-fleet.sh +92 -21
- package/daemons/run-watchdog.sh +95 -12
- package/dash/CLAUDE.md +32 -0
- package/dash/dash-extra.mjs +194 -21
- package/dash/watchdog-gui.sh +23 -6
- package/docs/BEHAVIORAL-PR-REVIEW.md +86 -0
- package/docs/CHECKPOINTING.md +100 -0
- package/docs/DISPATCH-MODEL.md +11 -7
- package/docs/FORENSICS.md +204 -0
- package/docs/GOVERNANCE.md +4 -2
- package/docs/HOOK-INSTALL.md +307 -0
- package/docs/HOW-THE-LOOP-WORKS.md +175 -0
- package/docs/MEMORY-TEMPLATE.md +61 -0
- package/docs/README.md +44 -0
- package/docs/RELEASING.md +159 -0
- package/docs/RELIABILITY.md +61 -0
- package/docs/RESTORE.md +397 -0
- package/docs/SCRIPTS-POLICY.md +97 -0
- package/docs/archive/README.md +3 -0
- package/docs/archive/spikes/tiered-cognition/ACTIVATION.md +125 -0
- package/docs/archive/spikes/tiered-cognition/DESIGN.md +287 -0
- package/docs/archive/spikes/tiered-cognition/FINDINGS.md +113 -0
- package/docs/archive/spikes/tiered-cognition/README.md +27 -0
- package/docs/archive/spikes/tiered-cognition/aesop-cognition.example.md +32 -0
- package/docs/archive/spikes/tiered-cognition/force-model-policy.merged.mjs +673 -0
- package/docs/archive/spikes/tiered-cognition/strip-tools-hook.mjs +434 -0
- package/docs/case-study-portfolio.md +61 -0
- package/docs/self-stats-data.json +11 -0
- package/docs/templates/FLEET-OPS-ANALYSIS.example.md +27 -0
- package/docs/templates/FLEET-OPS-RECOMMENDATIONS.example.md +24 -0
- package/docs/templates/PROPOSALS-LOG.example.md +64 -0
- package/hooks/CLAUDE.md +61 -0
- package/hooks/claude/force-model-policy.mjs +159 -0
- package/hooks/pre-push-policy.sh +753 -0
- package/mcp/CLAUDE.md +213 -0
- package/mcp/package.json +26 -0
- package/mcp/server.mjs +543 -0
- package/monitor/.signal-state.json +3 -0
- package/monitor/ACTIONS.log +1 -0
- package/monitor/BRIEF.md +24 -0
- package/monitor/CHARTER.md +75 -88
- package/monitor/CLAUDE.md +40 -0
- package/monitor/SIGNALS.json +54 -0
- package/monitor/collect-signals.mjs +613 -61
- package/package.json +15 -3
- package/scan/fleet-scan.example.mjs +292 -0
- package/skills/CLAUDE.md +3 -0
- package/skills/healthcheck/SKILL.md +44 -0
- package/skills/power/SKILL.md +161 -0
- package/state_store/CLAUDE.md +39 -0
- package/state_store/__init__.py +24 -0
- package/state_store/__pycache__/__init__.cpython-314.pyc +0 -0
- package/state_store/__pycache__/api.cpython-314.pyc +0 -0
- package/state_store/__pycache__/export.cpython-314.pyc +0 -0
- package/state_store/__pycache__/ingest.cpython-314.pyc +0 -0
- package/state_store/__pycache__/projections.cpython-314.pyc +0 -0
- package/state_store/__pycache__/store.cpython-314.pyc +0 -0
- package/state_store/api.py +35 -0
- package/state_store/export.py +19 -0
- package/state_store/ingest.py +24 -0
- package/state_store/projections.py +52 -0
- package/state_store/store.py +102 -0
- package/tools/CLAUDE.md +72 -0
- package/tools/__pycache__/alert_bridge.cpython-314.pyc +0 -0
- package/tools/__pycache__/buildlog.cpython-314.pyc +0 -0
- package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
- package/tools/__pycache__/ensure_state.cpython-314.pyc +0 -0
- package/tools/__pycache__/eod_sweep.cpython-314.pyc +0 -0
- package/tools/__pycache__/fleet_ledger.cpython-314.pyc +0 -0
- package/tools/__pycache__/fleet_prompt_extractor.cpython-314.pyc +0 -0
- package/tools/__pycache__/healthcheck.cpython-314.pyc +0 -0
- package/tools/__pycache__/heartbeat.cpython-314.pyc +0 -0
- package/tools/__pycache__/inbox_drain.cpython-314.pyc +0 -0
- package/tools/__pycache__/launch_tui.cpython-314.pyc +0 -0
- package/tools/__pycache__/metrics_gate.cpython-314.pyc +0 -0
- package/tools/__pycache__/orchestrator_status.cpython-314.pyc +0 -0
- package/tools/__pycache__/power_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/prepublish_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/rotate_logs.cpython-314.pyc +0 -0
- package/tools/__pycache__/scanner_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/self_stats.cpython-314.pyc +0 -0
- package/tools/__pycache__/session_usage_summary.cpython-314.pyc +0 -0
- package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_replay.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_timeline.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
- package/tools/agent-forensics.sh +186 -0
- package/tools/alert_bridge.py +449 -0
- package/tools/buildlog.py +182 -0
- package/tools/ci_merge_wait.py +317 -0
- package/tools/ensure_state.py +94 -0
- package/tools/eod_sweep.py +239 -0
- package/tools/fleet_ledger.py +272 -0
- package/tools/fleet_prompt_extractor.py +134 -0
- package/tools/healthcheck.py +296 -0
- package/tools/heartbeat.py +131 -0
- package/tools/inbox_drain.py +249 -0
- package/tools/launch_tui.py +51 -3
- package/tools/lock.mjs +211 -0
- package/tools/metrics_gate.py +205 -0
- package/tools/orchestrator_status.py +99 -0
- package/tools/power_selftest.py +386 -0
- package/tools/prepublish_scan.py +84 -0
- package/tools/proposals.mjs +248 -0
- package/tools/reconstitute.sh +467 -0
- package/tools/rotate_logs.py +228 -0
- package/tools/scanner_selftest.py +303 -0
- package/tools/secret_scan.py +138 -31
- package/tools/self_stats.py +509 -0
- package/tools/session_usage_summary.py +198 -0
- package/tools/stall_check.py +172 -0
- package/tools/svg_to_png.mjs +50 -0
- package/tools/transcript_replay.py +236 -0
- package/tools/transcript_timeline.py +184 -0
- package/tools/verify_dash.py +513 -0
- package/tools/verify_submit_encoding.py +202 -0
- package/ui/CLAUDE.md +125 -0
- package/ui/__pycache__/agents.cpython-314.pyc +0 -0
- package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
- package/ui/__pycache__/config.cpython-314.pyc +0 -0
- package/ui/__pycache__/cost.cpython-314.pyc +0 -0
- package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
- package/ui/__pycache__/handler.cpython-314.pyc +0 -0
- package/ui/__pycache__/render.cpython-314.pyc +0 -0
- package/ui/__pycache__/serve.cpython-314.pyc +0 -0
- package/ui/__pycache__/sse.cpython-314.pyc +0 -0
- package/ui/agents.py +183 -0
- package/ui/api/__init__.py +84 -0
- package/ui/api/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/tracker.cpython-314.pyc +0 -0
- package/ui/api/submit.py +97 -0
- package/ui/api/tracker.py +116 -0
- package/ui/collectors.py +735 -0
- package/ui/config.py +123 -0
- package/ui/cost.py +260 -0
- package/ui/csrf.py +144 -0
- package/ui/handler.py +676 -0
- package/ui/render.py +49 -0
- package/ui/serve.py +68 -796
- package/ui/sse.py +183 -0
- package/ui/web/.gitattributes +13 -0
- package/ui/web/dist/assets/index-2LZDQirC.js +9 -0
- package/ui/web/dist/assets/index-D4M1qyOv.css +1 -0
- package/ui/web/dist/index.html +14 -0
- package/ui/web/index.html +13 -0
- package/ui/web/package-lock.json +2225 -0
- package/ui/web/package.json +26 -0
- package/ui/web/src/App.test.tsx +74 -0
- package/ui/web/src/App.tsx +142 -0
- package/ui/web/src/CONTRIBUTING-UI.md +49 -0
- package/ui/web/src/components/AgentRow.css +187 -0
- package/ui/web/src/components/AgentRow.test.tsx +209 -0
- package/ui/web/src/components/AgentRow.tsx +207 -0
- package/ui/web/src/components/AgentsPanel.css +108 -0
- package/ui/web/src/components/AgentsPanel.test.tsx +41 -0
- package/ui/web/src/components/AgentsPanel.tsx +58 -0
- package/ui/web/src/components/AlertsPanel.css +88 -0
- package/ui/web/src/components/AlertsPanel.test.tsx +51 -0
- package/ui/web/src/components/AlertsPanel.tsx +67 -0
- package/ui/web/src/components/BacklogPanel.test.tsx +126 -0
- package/ui/web/src/components/BacklogPanel.tsx +122 -0
- package/ui/web/src/components/CostChart.css +110 -0
- package/ui/web/src/components/CostChart.test.tsx +144 -0
- package/ui/web/src/components/CostChart.tsx +152 -0
- package/ui/web/src/components/CostTable.css +93 -0
- package/ui/web/src/components/CostTable.test.tsx +165 -0
- package/ui/web/src/components/CostTable.tsx +94 -0
- package/ui/web/src/components/EventsFeed.css +68 -0
- package/ui/web/src/components/EventsFeed.test.tsx +36 -0
- package/ui/web/src/components/EventsFeed.tsx +31 -0
- package/ui/web/src/components/HealthHeader.css +137 -0
- package/ui/web/src/components/HealthHeader.test.tsx +278 -0
- package/ui/web/src/components/HealthHeader.tsx +281 -0
- package/ui/web/src/components/InboxForm.css +135 -0
- package/ui/web/src/components/InboxForm.test.tsx +208 -0
- package/ui/web/src/components/InboxForm.tsx +116 -0
- package/ui/web/src/components/MessagesTail.module.css +144 -0
- package/ui/web/src/components/MessagesTail.test.tsx +176 -0
- package/ui/web/src/components/MessagesTail.tsx +94 -0
- package/ui/web/src/components/ReposPanel.css +90 -0
- package/ui/web/src/components/ReposPanel.test.tsx +45 -0
- package/ui/web/src/components/ReposPanel.tsx +67 -0
- package/ui/web/src/components/Scorecard.css +106 -0
- package/ui/web/src/components/Scorecard.test.tsx +117 -0
- package/ui/web/src/components/Scorecard.tsx +85 -0
- package/ui/web/src/components/Timeline.module.css +151 -0
- package/ui/web/src/components/Timeline.test.tsx +215 -0
- package/ui/web/src/components/Timeline.tsx +99 -0
- package/ui/web/src/components/TrackerBoard.test.tsx +121 -0
- package/ui/web/src/components/TrackerBoard.tsx +107 -0
- package/ui/web/src/components/TrackerCard.test.tsx +180 -0
- package/ui/web/src/components/TrackerCard.tsx +160 -0
- package/ui/web/src/components/TrackerForm.test.tsx +189 -0
- package/ui/web/src/components/TrackerForm.tsx +144 -0
- package/ui/web/src/lib/api.ts +218 -0
- package/ui/web/src/lib/format.test.ts +89 -0
- package/ui/web/src/lib/format.ts +103 -0
- package/ui/web/src/lib/sanitizeUrl.test.ts +84 -0
- package/ui/web/src/lib/sanitizeUrl.ts +38 -0
- package/ui/web/src/lib/types.ts +230 -0
- package/ui/web/src/lib/useHashRoute.test.ts +60 -0
- package/ui/web/src/lib/useHashRoute.ts +23 -0
- package/ui/web/src/lib/useSSE.ts +175 -0
- package/ui/web/src/main.tsx +10 -0
- package/ui/web/src/styles/global.css +179 -0
- package/ui/web/src/styles/theme.css +184 -0
- package/ui/web/src/styles/work.css +572 -0
- package/ui/web/src/test/fixtures.ts +385 -0
- package/ui/web/src/test/setup.ts +49 -0
- package/ui/web/src/views/Activity.module.css +43 -0
- package/ui/web/src/views/Activity.test.tsx +89 -0
- package/ui/web/src/views/Activity.tsx +31 -0
- package/ui/web/src/views/Cost.css +87 -0
- package/ui/web/src/views/Cost.test.tsx +142 -0
- package/ui/web/src/views/Cost.tsx +54 -0
- package/ui/web/src/views/Overview.css +51 -0
- package/ui/web/src/views/Overview.test.tsx +76 -0
- package/ui/web/src/views/Overview.tsx +46 -0
- package/ui/web/src/views/Work.test.tsx +82 -0
- package/ui/web/src/views/Work.tsx +79 -0
- package/ui/web/src/vite-env.d.ts +10 -0
- package/ui/web/tsconfig.json +22 -0
- package/ui/web/vite.config.ts +25 -0
- package/ui/web/vitest.config.ts +12 -0
package/ui/api/submit.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Aesop UI API — inbox-submit logic (wave-10 P0 split from handler.py).
|
|
4
|
+
|
|
5
|
+
Free function: given already-validated, non-empty text, append it to the UI
|
|
6
|
+
inbox file. UTF-8/LF-safe (matches the encoding used to read the file back
|
|
7
|
+
elsewhere) and guarded against a symlinked inbox file (TOCTOU defense --
|
|
8
|
+
reject rather than follow a symlink another local user/process may have
|
|
9
|
+
planted at config.INBOX_FILE).
|
|
10
|
+
|
|
11
|
+
Reads config.INBOX_FILE live via `import config` at call time, never
|
|
12
|
+
`from config import INBOX_FILE` -- a frozen import goes stale after
|
|
13
|
+
config.reload() (breaks test-fixture isolation). See ui/CLAUDE.md.
|
|
14
|
+
|
|
15
|
+
Detects both POSIX symlinks and Windows junctions/reparse points.
|
|
16
|
+
"""
|
|
17
|
+
import os
|
|
18
|
+
import stat
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
|
|
21
|
+
import config
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _is_link_or_reparse(path):
|
|
25
|
+
"""Check if path is a symlink or Windows reparse point (junction/etc).
|
|
26
|
+
|
|
27
|
+
Returns True if:
|
|
28
|
+
- On all platforms: path is a POSIX symlink (detected via st_mode)
|
|
29
|
+
- On Windows: path has the FILE_ATTRIBUTE_REPARSE_POINT attribute
|
|
30
|
+
(which includes junctions, symlinks, and other reparse points)
|
|
31
|
+
|
|
32
|
+
Uses os.lstat() to detect without following links (includes dangling ones).
|
|
33
|
+
On error (e.g., path doesn't exist), returns False to let caller's
|
|
34
|
+
exists() check handle it normally.
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
stat_result = os.lstat(str(path))
|
|
38
|
+
|
|
39
|
+
# On POSIX, check the symlink bit in st_mode
|
|
40
|
+
if stat.S_ISLNK(stat_result.st_mode):
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
# On Windows, check for reparse point attribute (junctions, symlinks, etc)
|
|
44
|
+
# FILE_ATTRIBUTE_REPARSE_POINT = 0x400 (1024 decimal)
|
|
45
|
+
# st_file_attributes only exists on Windows
|
|
46
|
+
if hasattr(stat_result, 'st_file_attributes'):
|
|
47
|
+
FILE_ATTRIBUTE_REPARSE_POINT = 0x400
|
|
48
|
+
if stat_result.st_file_attributes & FILE_ATTRIBUTE_REPARSE_POINT:
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
return False
|
|
52
|
+
except (OSError, AttributeError):
|
|
53
|
+
# If we can't stat it, let it fall through to the exists() check
|
|
54
|
+
# (file doesn't exist, which is fine)
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def append_to_inbox(text):
|
|
59
|
+
"""Append one inbox line for `text` to config.INBOX_FILE.
|
|
60
|
+
|
|
61
|
+
Creates the file (with a header comment) if it doesn't exist yet.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
text: non-empty, already-stripped text to record (caller is
|
|
65
|
+
responsible for validating it's non-empty -- this function does
|
|
66
|
+
not re-check).
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
(True, None) on success.
|
|
70
|
+
(False, (400, {"error": "..."})) if config.INBOX_FILE exists and is
|
|
71
|
+
a symlink (rejected for security).
|
|
72
|
+
"""
|
|
73
|
+
inbox_content = f"- [{datetime.now().isoformat()}] {text}\n"
|
|
74
|
+
|
|
75
|
+
# Security: reject symlinks and Windows junctions (TOCTOU defense) FIRST —
|
|
76
|
+
# check link/reparse INDEPENDENT of exists(). Path.exists() follows the link,
|
|
77
|
+
# so a DANGLING symlink (target not yet created) returns False and would
|
|
78
|
+
# otherwise skip this check, then open(...,'w') would follow the link and
|
|
79
|
+
# create the attacker's target. Windows junctions are not detected by
|
|
80
|
+
# os.path.islink(), so we use _is_link_or_reparse() instead.
|
|
81
|
+
if _is_link_or_reparse(config.INBOX_FILE):
|
|
82
|
+
return False, (400, {"error": "Inbox file is a symlink (rejected for security)"})
|
|
83
|
+
|
|
84
|
+
if not config.INBOX_FILE.exists():
|
|
85
|
+
config.INBOX_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
# Must match the encoding (utf-8) AND newline convention (LF) of the
|
|
87
|
+
# append below -- text-mode write_text() with no encoding= falls back
|
|
88
|
+
# to the locale-preferred encoding (cp1252 on Windows), which mangles
|
|
89
|
+
# non-ASCII bytes like the em-dash and leaves the file as a whole not
|
|
90
|
+
# valid UTF-8 for anything that reads it with encoding="utf-8".
|
|
91
|
+
with open(config.INBOX_FILE, 'w', encoding='utf-8', newline='\n') as f:
|
|
92
|
+
f.write("# UI Inbox — orchestrator reads each turn / on /power\n\n")
|
|
93
|
+
|
|
94
|
+
with open(config.INBOX_FILE, 'a', encoding='utf-8') as f:
|
|
95
|
+
f.write(inbox_content)
|
|
96
|
+
|
|
97
|
+
return True, None
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Aesop UI API — tracker CRUD logic (wave-10 P0 split from handler.py).
|
|
4
|
+
|
|
5
|
+
Free functions: take already-parsed request input (headers/body bytes/path
|
|
6
|
+
params extracted by ui/handler.py) and call collectors' tracker CRUD,
|
|
7
|
+
returning (status_code, dict). No self/HTTP coupling -- ui/handler.py owns
|
|
8
|
+
parsing the raw request and writing the HTTP response bytes.
|
|
9
|
+
|
|
10
|
+
Every mutation here is CSRF-gated via api.validate_mutation() (create) or a
|
|
11
|
+
direct csrf.validate_csrf_request() check performed by the caller before
|
|
12
|
+
delete (see ui/handler.py's handle_tracker_mutate -- CSRF is checked once,
|
|
13
|
+
before the path is even parsed, matching the pre-split handler's ordering).
|
|
14
|
+
|
|
15
|
+
Reads collectors' tracker CRUD, which itself reads config.X live via
|
|
16
|
+
`import config` -- see ui/CLAUDE.md load-bearing rule.
|
|
17
|
+
"""
|
|
18
|
+
import json
|
|
19
|
+
|
|
20
|
+
from . import validate_mutation
|
|
21
|
+
from collectors import (create_tracker_item, delete_tracker_item,
|
|
22
|
+
get_tracker_items, update_tracker_item)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def list_items(status=None, priority=None):
|
|
26
|
+
"""GET /api/tracker -- list items with optional status/priority filters.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
(200, items_list) on success.
|
|
30
|
+
(500, error_dict) on failure.
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
items = get_tracker_items(status=status, priority=priority)
|
|
34
|
+
return 200, items
|
|
35
|
+
except Exception as e:
|
|
36
|
+
return 500, {"error": str(e)}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def create(headers, body_bytes):
|
|
40
|
+
"""POST /api/tracker -- create a tracker item. CSRF-gated.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
headers: request headers (case-insensitive dict-like).
|
|
44
|
+
body_bytes: raw request body already read + bounded by the caller.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
(201, item_dict) on success.
|
|
48
|
+
(403, error_dict) on CSRF failure.
|
|
49
|
+
(400, error_dict) on invalid Content-Length or invalid JSON.
|
|
50
|
+
(500, error_dict) on any other failure.
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
ok, result = validate_mutation(headers, body_bytes)
|
|
54
|
+
if not ok:
|
|
55
|
+
status, error = result
|
|
56
|
+
return status, error
|
|
57
|
+
item = create_tracker_item(result)
|
|
58
|
+
return 201, item
|
|
59
|
+
except json.JSONDecodeError:
|
|
60
|
+
return 400, {"error": "Invalid JSON"}
|
|
61
|
+
except Exception as e:
|
|
62
|
+
return 500, {"error": str(e)}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def update(item_id, body_bytes):
|
|
66
|
+
"""POST /api/tracker/<id> (default action) -- update a tracker item.
|
|
67
|
+
|
|
68
|
+
CSRF must already have been validated by the caller before invoking this
|
|
69
|
+
(see ui/handler.py's handle_tracker_mutate: CSRF is checked once up front,
|
|
70
|
+
shared with the delete branch). This function only handles JSON parse +
|
|
71
|
+
the update itself, matching the pre-split handler's behavior where a
|
|
72
|
+
malformed JSON body on this path falls through to a generic error rather
|
|
73
|
+
than a dedicated "Invalid JSON" 400 (unlike create()).
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
item_id: tracker item id extracted from the URL path by the caller.
|
|
77
|
+
body_bytes: raw request body already read + bounded by the caller.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
(200, item_dict) on success.
|
|
81
|
+
(404, error_dict) if item_id is unknown.
|
|
82
|
+
(500, error_dict) on any other failure (including malformed JSON --
|
|
83
|
+
preserves the pre-split handler's behavior).
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
body_str = (body_bytes or b'').decode('utf-8', errors='ignore')
|
|
87
|
+
update_data = json.loads(body_str)
|
|
88
|
+
item = update_tracker_item(item_id, update_data)
|
|
89
|
+
return 200, item
|
|
90
|
+
except Exception as e:
|
|
91
|
+
if "404" in str(e):
|
|
92
|
+
return 404, {"error": str(e)}
|
|
93
|
+
return 500, {"error": str(e)}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def delete(item_id):
|
|
97
|
+
"""POST /api/tracker/<id>?action=delete -- soft-delete (archive) an item.
|
|
98
|
+
|
|
99
|
+
CSRF must already have been validated by the caller before invoking this
|
|
100
|
+
(see update()'s docstring -- same shared up-front check).
|
|
101
|
+
|
|
102
|
+
Idempotent: deleting an already-archived item succeeds again (re-sets
|
|
103
|
+
status to "archived"), it does not error.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
(200, item_dict) on success.
|
|
107
|
+
(404, error_dict) if item_id is unknown.
|
|
108
|
+
(500, error_dict) on any other failure.
|
|
109
|
+
"""
|
|
110
|
+
try:
|
|
111
|
+
item = delete_tracker_item(item_id)
|
|
112
|
+
return 200, item
|
|
113
|
+
except Exception as e:
|
|
114
|
+
if "404" in str(e):
|
|
115
|
+
return 404, {"error": str(e)}
|
|
116
|
+
return 500, {"error": str(e)}
|