ccdeck 3.41.1 → 3.42.0
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/README.md +44 -25
- package/bin/deck.js +4 -0
- package/dist/web/assets/{BrowserWatchModal-xoHNkWWF.js → BrowserWatchModal-TdSBv7_T.js} +1 -1
- package/dist/web/assets/{RadioPanel-Bii2jN0n.js → RadioPanel-bJc004IK.js} +1 -1
- package/dist/web/assets/TrafficRadar-pRTWTOpJ.js +1 -0
- package/dist/web/assets/UsageHistoryModal-BBAEDRj9.js +1 -0
- package/dist/web/assets/index-BPLXy9SO.css +1 -0
- package/dist/web/assets/index-DzDnLqDX.js +307 -0
- package/dist/web/index.html +15 -4
- package/package.json +1 -1
- package/release-notes.json +50 -21
- package/src/server/agent-activity.mjs +33 -10
- package/src/server/browser-watch-decks.mjs +3 -2
- package/src/server/browser-watch.mjs +3 -4
- package/src/server/codex-watch.mjs +3 -3
- package/src/server/event-pipeline.mjs +2 -0
- package/src/server/feedback-form.mjs +1 -0
- package/src/server/index.mjs +4 -0
- package/src/server/pinned-build.mjs +1 -0
- package/src/server/reports-routes.mjs +31 -12
- package/src/server/request-gates.mjs +2 -0
- package/src/server/tool-audit-operations.mjs +112 -0
- package/src/server/tool-audit-report.mjs +115 -0
- package/src/server/tool-audit-routes.mjs +38 -0
- package/src/server/tool-audit.mjs +126 -0
- package/dist/web/assets/TrafficRadar-CXRcWYZ8.js +0 -1
- package/dist/web/assets/UsageHistoryModal-Ceg-2Dm9.js +0 -1
- package/dist/web/assets/index-2Z0Tr6cq.js +0 -305
- package/dist/web/assets/index-BgSnAnP6.css +0 -1
package/dist/web/index.html
CHANGED
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
<script>
|
|
54
54
|
(function () {
|
|
55
55
|
// Generated from JSON alongside the CSS and Settings catalog.
|
|
56
|
-
var themes = /* theme-ids:start */ ["light","dark","rider-black","vscode-black","omarchy","matrix","black-contrast","white-contrast","catppuccin-mocha"] /* theme-ids:end */;
|
|
57
|
-
var lightThemes = /* theme-light-ids:start */ ["light","white-contrast"] /* theme-light-ids:end */;
|
|
56
|
+
var themes = /* theme-ids:start */ ["light","dark","rider-black","vscode-black","omarchy","matrix","black-contrast","white-contrast","catppuccin-mocha","gruvbox-dark","gruvbox-light","gruvbox-material"] /* theme-ids:end */;
|
|
57
|
+
var lightThemes = /* theme-light-ids:start */ ["light","white-contrast","gruvbox-light"] /* theme-light-ids:end */;
|
|
58
58
|
var stored = null;
|
|
59
59
|
// `window.localStorage` is a GETTER that throws where site data is
|
|
60
60
|
// blocked, so the property read is inside the try, not just getItem.
|
|
@@ -66,8 +66,19 @@
|
|
|
66
66
|
document.documentElement.setAttribute("data-color-scheme", lightThemes.indexOf(theme) >= 0 ? "light" : "dark");
|
|
67
67
|
})();
|
|
68
68
|
</script>
|
|
69
|
-
<script
|
|
70
|
-
|
|
69
|
+
<script>
|
|
70
|
+
(function () {
|
|
71
|
+
var size = null, density = null;
|
|
72
|
+
try {
|
|
73
|
+
size = window.localStorage.getItem("agent-dag.text-size");
|
|
74
|
+
density = window.localStorage.getItem("agent-dag.density");
|
|
75
|
+
} catch (e) {}
|
|
76
|
+
document.documentElement.setAttribute("data-text-size", size === "smallest" || size === "smaller" || size === "larger" || size === "largest" ? size : "default");
|
|
77
|
+
document.documentElement.setAttribute("data-density", density === "tightest" || density === "tighter" || density === "compact" || density === "comfortable" ? density : "default");
|
|
78
|
+
})();
|
|
79
|
+
</script>
|
|
80
|
+
<script type="module" crossorigin src="/assets/index-DzDnLqDX.js"></script>
|
|
81
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BPLXy9SO.css">
|
|
71
82
|
</head>
|
|
72
83
|
<body>
|
|
73
84
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccdeck",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.42.0",
|
|
4
4
|
"description": "Local dashboard for Claude Code and Codex CLI sessions: which Claude Code session needs input, every tool call and Claude Code subagent live, token cost and quota, several Claude accounts. Run npx ccdeck.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/release-notes.json
CHANGED
|
@@ -75,7 +75,26 @@
|
|
|
75
75
|
"3.38.2",
|
|
76
76
|
"3.38.6",
|
|
77
77
|
"3.39.4",
|
|
78
|
-
"3.40.1",
|
|
78
|
+
"3.40.1",
|
|
79
|
+
"3.41.3"
|
|
80
|
+
],
|
|
81
|
+
"3.42.0": [
|
|
82
|
+
{
|
|
83
|
+
"title": "🔠 Text size and density, in Settings",
|
|
84
|
+
"body": "Five text sizes and five spacing levels, under Settings › Text & density."
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"title": "🛠️ Tool activity: which tools your agents use, how long they take and what fails",
|
|
88
|
+
"body": "A new page on the stripe on the right. Its history is kept on this device only."
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"title": "🎨 Gruvbox Dark, Light and Material themes",
|
|
92
|
+
"body": "Pick one in Settings › General. Switching themes now fades instead of snapping."
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"title": "✨ A session-list drawer on phones, whole dates in usage history, the waiting session in the minimap",
|
|
96
|
+
"body": "Smaller fixes for the phone layout, the usage history chart and the canvas minimap."
|
|
97
|
+
}
|
|
79
98
|
],
|
|
80
99
|
"3.41.1": [
|
|
81
100
|
{
|
|
@@ -121,6 +140,12 @@
|
|
|
121
140
|
"body": "Change individual telemetry flags and use a simpler Monitor view."
|
|
122
141
|
}
|
|
123
142
|
],
|
|
143
|
+
"3.40.2": [
|
|
144
|
+
{
|
|
145
|
+
"title": "📻 Music is in Radio now, not in Settings",
|
|
146
|
+
"body": "Settings no longer has a Music section. Claude FM, Lofi Girl and the streams you added are under Workspace streams in Radio, on the right sidebar, and Radio’s player has the volume and mute. Your station, volume and streams are kept. Adding, renaming or removing a stream link is no longer offered."
|
|
147
|
+
}
|
|
148
|
+
],
|
|
124
149
|
"3.40.0": [
|
|
125
150
|
{
|
|
126
151
|
"title": "📻 Explore radio around the world",
|
|
@@ -128,7 +153,7 @@
|
|
|
128
153
|
},
|
|
129
154
|
{
|
|
130
155
|
"title": "🦊 Choose your canvas companion",
|
|
131
|
-
"body": "Choose Deck companion, Fox or Penguin in Settings ›
|
|
156
|
+
"body": "Choose Deck companion, Fox or Penguin in Settings › General. Your companion shares the existing canvas movements and music controls, and your choice stays saved."
|
|
132
157
|
},
|
|
133
158
|
{
|
|
134
159
|
"title": "📊 See what each session used",
|
|
@@ -147,12 +172,16 @@
|
|
|
147
172
|
{
|
|
148
173
|
"title": "Clearer account usage and Telemetry Radar",
|
|
149
174
|
"body": "Account usage shows reset times together with capacity. Telemetry Radar has a clearer monitoring workspace and keeps history for up to 24 hours. Radar remains Beta."
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"title": "🔎 Telemetry Radar on Linux and Windows, with no Terminal step",
|
|
178
|
+
"body": "Radar now works on macOS, Linux and Windows. Press Start monitoring and ccdeck runs the capture itself, with no command to paste into Terminal, and keeps listening until you stop it. Capture needs a one-time setup for your user: Wireshark’s ChmodBPF helper on macOS, tcpdump with capture permission on Linux, or Wireshark with Npcap on Windows. Radar says what is missing, and once permission is granted it starts listening by itself."
|
|
150
179
|
}
|
|
151
180
|
],
|
|
152
181
|
"3.39.1": [
|
|
153
182
|
{
|
|
154
183
|
"title": "🎨 More themes, with clearer contrast",
|
|
155
|
-
"body": "Choose Omarchy, Matrix, Black Contrast, White Contrast or Catppuccin Mocha in Settings ›
|
|
184
|
+
"body": "Choose Omarchy, Matrix, Black Contrast, White Contrast or Catppuccin Mocha in Settings › General. Fresh installations start with Rider Black; your saved theme choice is preserved."
|
|
156
185
|
},
|
|
157
186
|
{
|
|
158
187
|
"title": "A little more room on the canvas",
|
|
@@ -162,7 +191,7 @@
|
|
|
162
191
|
"3.39.0": [
|
|
163
192
|
{
|
|
164
193
|
"title": "🔎 Inspect Claude Code telemetry on macOS",
|
|
165
|
-
"body": "Telemetry Radar shows Claude Code's configured telemetry and can inspect traffic at destinations you choose. When settings are absent, it can show observed connections separately; a connection alone does not prove telemetry or reveal message contents.
|
|
194
|
+
"body": "Telemetry Radar shows Claude Code's configured telemetry and can inspect traffic at destinations you choose. When settings are absent, it can show observed connections separately; a connection alone does not prove telemetry or reveal message contents. Captured telemetry can contain prompt or response content, and other apps sending to the same destination may appear too. Captured messages stay in ccdeck's memory, and no capture file is written."
|
|
166
195
|
},
|
|
167
196
|
{
|
|
168
197
|
"title": "🧭 Open panels from the edges",
|
|
@@ -174,13 +203,13 @@
|
|
|
174
203
|
},
|
|
175
204
|
{
|
|
176
205
|
"title": "🎨 Two new dark themes",
|
|
177
|
-
"body": "Choose Rider Black or VS Code Black in Settings ›
|
|
206
|
+
"body": "Choose Rider Black or VS Code Black in Settings › General. Both add a darker canvas with flatter panels and distinct focus accents; your existing Light or Dark choice stays as it was."
|
|
178
207
|
}
|
|
179
208
|
],
|
|
180
209
|
"3.38.5": [
|
|
181
210
|
{
|
|
182
211
|
"title": "🔧 Settings are in one place now",
|
|
183
|
-
"body": "The theme, the sounds, notifications
|
|
212
|
+
"body": "The theme, the sounds, notifications and the minimap character are all in Settings, behind the gear at the right of the topbar, or Cmd+, (Ctrl+, on Windows and Linux). V opens it at Sounds, with each tone's volume and sound, and M turns sounds off or on from anywhere. Nothing you had set has changed, it has only moved, and T still switches the theme. The auto-switch threshold also takes a custom value now: pick Custom… and type a whole percent from 50 to 99."
|
|
184
213
|
},
|
|
185
214
|
{
|
|
186
215
|
"title": "Windows: no more temperature stuck at 28 °C",
|
|
@@ -190,7 +219,7 @@
|
|
|
190
219
|
"3.38.4": [
|
|
191
220
|
{
|
|
192
221
|
"title": "🔔 A notification when Auto-switch changes your account",
|
|
193
|
-
"body": "When Auto-switch moves Claude Code to another account, ccdeck now tells you once, with where it went and why, for example \"Switched to work · personal@example.com reached 90%\". You can turn it off in
|
|
222
|
+
"body": "When Auto-switch moves Claude Code to another account, ccdeck now tells you once, with where it went and why, for example \"Switched to work · personal@example.com reached 90%\". You can turn it off in Settings › Notifications, where two more wait, off until you turn them on: one when a quota window of the Claude or Codex account you are on reaches 90% and again at 100%, and one when a window that had reached 90% is available again. Each is said once per window and a restart does not repeat it. With either quota notification on, the deck reads quota about every two minutes even while no page is open."
|
|
194
223
|
}
|
|
195
224
|
],
|
|
196
225
|
"3.38.3": [
|
|
@@ -228,7 +257,7 @@
|
|
|
228
257
|
},
|
|
229
258
|
{
|
|
230
259
|
"title": "💬 Send feedback from inside the deck",
|
|
231
|
-
"body": "
|
|
260
|
+
"body": "Feedback, beside Settings at the right of the topbar (under More on a phone), opens a short form: pick what kind it is, write a few lines, and add a way to reach you under Add details if you want an answer. It goes to the people who make ccdeck, who may open a public issue from it, never with your contact in it. If it cannot be sent, the form stays filled in and says why."
|
|
232
261
|
}
|
|
233
262
|
],
|
|
234
263
|
"3.33.0": [
|
|
@@ -382,7 +411,7 @@
|
|
|
382
411
|
},
|
|
383
412
|
{
|
|
384
413
|
"title": "📅 Projects opens on Today",
|
|
385
|
-
"body": "The Projects report per account starts with today's work, and an empty window offers the next wider one. The
|
|
414
|
+
"body": "The Projects report per account starts with today's work, and an empty window offers the next wider one. The desktop app can also restart into an update from its own window."
|
|
386
415
|
}
|
|
387
416
|
],
|
|
388
417
|
"3.27.0": [
|
|
@@ -440,7 +469,7 @@
|
|
|
440
469
|
},
|
|
441
470
|
{
|
|
442
471
|
"title": "🎨 See each theme before you pick it",
|
|
443
|
-
"body": "
|
|
472
|
+
"body": "Settings › General shows a small preview of the deck in each theme. Claude FM also keeps its volume when a notification chime plays, instead of dipping under it."
|
|
444
473
|
}
|
|
445
474
|
],
|
|
446
475
|
"3.24.1": [
|
|
@@ -598,11 +627,11 @@
|
|
|
598
627
|
},
|
|
599
628
|
{
|
|
600
629
|
"title": "⎋ Esc leaves the machine panel where it is",
|
|
601
|
-
"body": "The × on **This machine** read `Close (Esc)`, so Escape put the panel away. Escape belongs to the canvas — it deselects and hands the keyboard back — and pressing it while you are reading your instruments should not close them.\n\nThe panel now closes the
|
|
630
|
+
"body": "The × on **This machine** read `Close (Esc)`, so Escape put the panel away. Escape belongs to the canvas — it deselects and hands the keyboard back — and pressing it while you are reading your instruments should not close them.\n\nThe panel now closes the ways it opens: its ×, its button on the window's right edge, and S. The × says `Close`, and names no key it does not answer."
|
|
602
631
|
},
|
|
603
632
|
{
|
|
604
633
|
"title": "🖥 The machine moved out of the topbar and into a button",
|
|
605
|
-
"body": "The small CPU sparkline and memory bar in the topbar are gone. A chip glyph
|
|
634
|
+
"body": "The small CPU sparkline and memory bar in the topbar are gone. A chip glyph, on the window's right edge now, opens the same **This machine** panel — cores, memory, swap, load, temperature, busiest processes — and the bar stops moving in the corner of your eye.\n\nIt also stops asking. The deck read your machine every three seconds for the life of every tab to feed that meter; it now reads only while the panel is open, and the history behind the readings is kept by the deck itself, so a panel opened an hour from now still has the hour behind it."
|
|
606
635
|
},
|
|
607
636
|
{
|
|
608
637
|
"title": "🔎 The panel's eight process rows became one way in",
|
|
@@ -660,7 +689,7 @@
|
|
|
660
689
|
"3.9.0": [
|
|
661
690
|
{
|
|
662
691
|
"title": "🎛️ Five switches that were not where the thing they control is",
|
|
663
|
-
"body": "**The session list has a button again.** It lost one a while back and only the `L` key was left — and `L` is written down in one place, a shortcuts sheet behind a small `?` in the corner of the canvas. If you use a mouse there was no way to open the list at all.
|
|
692
|
+
"body": "**The session list has a button again.** It lost one a while back and only the `L` key was left — and `L` is written down in one place, a shortcuts sheet behind a small `?` in the corner of the canvas. If you use a mouse there was no way to open the list at all. Its button is on the window's left edge now.\n\n**The Notifications switch now tells you whether the browser will actually show one.** It said `on` while this browser had never been asked for permission, which is not the same thing. It now says so on that row and offers the ask right there. Before, the only place to grant it was a button that appears solely while a session is stuck waiting — so on a machine where that rarely happens you could not switch notifications on at all.\n\n**Started with `--workspace` or `--scope` and seeing nothing?** The empty screen used to tell everyone to \"run `claude` or `codex` in any folder\", which is the one thing that cannot work for you. It now names the folder the deck is actually watching.\n\n**Browser Watch says when a setting did not save.** Flipping the switch and having it fail showed a red line reading \"Could not read\" — about reading history, not about your press — and ten seconds later the panel wiped it and looked perfectly healthy with the setting unchanged. A failed save now says so, in its own words, and stays until you dismiss it. The \"nobody browsing for\" menu also follows the stored value again: change it in a second tab and this one never caught up.\n\n**The deck no longer restarts itself when nothing on screen offers to stop it.** Thirty seconds after the last agent went quiet, with a new version on disk, the server would exit, come back and reload the page. The `auto when idle` switch that controls that lives inside the update banner — so if you had closed the banner with its `×`, it happened with no visible setting anywhere. Closing the banner now also stands down the restart; clicking the version chip brings back both together."
|
|
664
693
|
}
|
|
665
694
|
],
|
|
666
695
|
"3.8.9": [
|
|
@@ -714,25 +743,25 @@
|
|
|
714
743
|
"3.8.1": [
|
|
715
744
|
{
|
|
716
745
|
"title": "🔒 Security: update if the deck runs on a Mac",
|
|
717
|
-
"body": "A local process able to reach the deck's port — no token, no browser, no tab — could get a string of its choosing compiled as AppleScript when the deck raised a desktop notification with no page open. On macOS that is code running as you.\n\nThe strings were already passed as arguments rather than pasted into the script, which is normally the whole defence. It is not enough for `osascript`: its option parser reads any argument starting with a dash as an option, so a name beginning with `-e` was taken as more script instead of as text. Every place the deck hands `osascript` or `notify-send` something from outside now carries an explicit end-of-options marker, so nothing from outside can be read as an instruction again.\n\nNothing to change on your side beyond updating. If you would rather keep the deck off the desktop entirely, the Notifications switch in
|
|
746
|
+
"body": "A local process able to reach the deck's port — no token, no browser, no tab — could get a string of its choosing compiled as AppleScript when the deck raised a desktop notification with no page open. On macOS that is code running as you.\n\nThe strings were already passed as arguments rather than pasted into the script, which is normally the whole defence. It is not enough for `osascript`: its option parser reads any argument starting with a dash as an option, so a name beginning with `-e` was taken as more script instead of as text. Every place the deck hands `osascript` or `notify-send` something from outside now carries an explicit end-of-options marker, so nothing from outside can be read as an instruction again.\n\nNothing to change on your side beyond updating. If you would rather keep the deck off the desktop entirely, the Notifications switch in Settings › Notifications does that, and `AGENTS_DECK_NO_NOTIFY=1` does it at launch — but the fix is the thing to have."
|
|
718
747
|
}
|
|
719
748
|
],
|
|
720
749
|
"3.8.0": [
|
|
721
750
|
{
|
|
722
751
|
"title": "🔕 Notifications now have an off switch",
|
|
723
|
-
"body": "3.7.0 gave you a \"notify me\" button and no way back. Turning them off meant digging into your browser's site settings — which no page can do for you — and that still left the other notifier, the one the deck raises when no page is open at all. Silencing that one meant quitting the deck and starting it again with `AGENTS_DECK_NO_NOTIFY=1` in front. The person who wanted quiet was the one doing the work.\n\nThere is a **Notifications** switch in
|
|
752
|
+
"body": "3.7.0 gave you a \"notify me\" button and no way back. Turning them off meant digging into your browser's site settings — which no page can do for you — and that still left the other notifier, the one the deck raises when no page is open at all. Silencing that one meant quitting the deck and starting it again with `AGENTS_DECK_NO_NOTIFY=1` in front. The person who wanted quiet was the one doing the work.\n\nThere is a **Notifications** switch in Settings › Notifications now. One switch governs both notifiers, it takes effect immediately without a restart, and it is remembered across restarts.\n\nIt is kept by the deck rather than by your browser, because the second notifier runs when no page exists and a setting living in a page could never reach it. `AGENTS_DECK_NO_NOTIFY=1` still wins over the switch — a machine told at launch to stay off the desktop stays off it — and when that is the case Settings says so instead of showing an \"on\" that will not happen."
|
|
724
753
|
}
|
|
725
754
|
],
|
|
726
755
|
"3.7.1": [
|
|
727
756
|
{
|
|
728
757
|
"title": "🔔 The notifications now fire on the thing that actually stops your agent",
|
|
729
|
-
"body": "3.7.0 could tell you a session was waiting — but only when Claude Code asked permission to run a tool. If you work with permissions bypassed, that never happens, and the whole feature was silent for you. Measured on one real log: 1683 events, every one of them in bypassPermissions, and a single permission prompt in the entire history.\n\nWhat fires constantly on that setup is the other kind: the agent asking you a question and stopping for the answer. The deck was throwing those away — no amber chip, no row in the sidebar, no notification, and no \"notify me\" button, because the button only
|
|
758
|
+
"body": "3.7.0 could tell you a session was waiting — but only when Claude Code asked permission to run a tool. If you work with permissions bypassed, that never happens, and the whole feature was silent for you. Measured on one real log: 1683 events, every one of them in bypassPermissions, and a single permission prompt in the entire history.\n\nWhat fires constantly on that setup is the other kind: the agent asking you a question and stopping for the answer. The deck was throwing those away — no amber chip, no row in the sidebar, no notification, and no \"notify me\" button, because the button only appeared while something was actually blocked.\n\nNow they count. The card carries the question itself rather than a generic \"Your turn\" — \"paycore needs your input: merge both branches to main, or just one?\" — and so does the notification, on the desktop, whether the tab is behind something or not open at all.\n\nAn idle prompt still stays quiet: a turn that ended is not a session that is stuck, and a channel that cries wolf is one you switch off."
|
|
730
759
|
}
|
|
731
760
|
],
|
|
732
761
|
"3.7.0": [
|
|
733
762
|
{
|
|
734
763
|
"title": "🔔 The deck can reach you when you are not looking at it",
|
|
735
|
-
"body": "The deck's whole point is telling you which agent is waiting on you, and until now it could only say so on screen — the amber chip, the tab title, the favicon. Every one of those is drawn inside a page, so the one moment it matters, when you have walked away and four agents have gone quiet, the deck was silent.\n\nNow it reaches the desktop, two ways that never overlap. With the deck open behind something, the page raises the notification itself:
|
|
764
|
+
"body": "The deck's whole point is telling you which agent is waiting on you, and until now it could only say so on screen — the amber chip, the tab title, the favicon. Every one of those is drawn inside a page, so the one moment it matters, when you have walked away and four agents have gone quiet, the deck was silent.\n\nNow it reaches the desktop, two ways that never overlap. With the deck open behind something, the page raises the notification itself: the browser asks for permission when you turn notifications on in Settings › Notifications — nothing is requested before you do. With no page open at all, the server raises it, because it can see that nobody is connected and therefore that no other surface is saying anything.\n\nOne notification per session, replaced rather than stacked if a second deck raises it too. Clicking it brings the tab forward. `AGENTS_DECK_NO_NOTIFY=1` turns the desktop side off entirely, and if you refuse the browser prompt the deck says so plainly instead of leaving you thinking you switched something on — after a refusal only your browser's site settings can undo it, which no page is allowed to touch."
|
|
736
765
|
},
|
|
737
766
|
{
|
|
738
767
|
"title": "🧩 A blocked session now says what it is asking for",
|
|
@@ -762,7 +791,7 @@
|
|
|
762
791
|
},
|
|
763
792
|
{
|
|
764
793
|
"title": "🧹 The board's own figure is out of the Usage panel",
|
|
765
|
-
"body": "3.3.0 put the canvas's live spend on its own labelled line, under whichever period you had chosen. In front of a panel that answers today, this month and all time from the logs on disk, that second money figure answers a question nobody asked at that moment — and it invited a comparison it could never win, $7,385 on the board beneath $170 for today.\n\nIt is gone. The
|
|
794
|
+
"body": "3.3.0 put the canvas's live spend on its own labelled line, under whichever period you had chosen. In front of a panel that answers today, this month and all time from the logs on disk, that second money figure answers a question nobody asked at that moment — and it invited a comparison it could never win, $7,385 on the board beneath $170 for today.\n\nIt is gone. The panel still shows the board's tokens and cost when ccusage cannot answer at all."
|
|
766
795
|
}
|
|
767
796
|
],
|
|
768
797
|
"3.3.0": [
|
|
@@ -776,7 +805,7 @@
|
|
|
776
805
|
},
|
|
777
806
|
{
|
|
778
807
|
"title": "👁 Browser Watch reads nothing while it is switched off",
|
|
779
|
-
"body": "The switch used to control only what was KEPT. The read happened anyway — every five minutes, on every deck, the panel's badge poll discovered each Chromium profile and copied its whole History database to a temp file to query it. A switch that says off while that continues is not a switch.\n\nWith the watch off, nothing touches a browser now. Opening the panel still reads live, because that is you looking, and a watch that is ON still records in the background — that is the feature. The trade is that the
|
|
808
|
+
"body": "The switch used to control only what was KEPT. The read happened anyway — every five minutes, on every deck, the panel's badge poll discovered each Chromium profile and copied its whole History database to a temp file to query it. A switch that says off while that continues is not a switch.\n\nWith the watch off, nothing touches a browser now. Opening the panel still reads live, because that is you looking, and a watch that is ON still records in the background — that is the feature. The trade is that the Browser Watch badge no longer counts findings while the watch is off; open the panel to see them.\n\nThe README has a Browser Watch section now: what is read, when, where the copy goes, and that only visits after the deck started are ever considered."
|
|
780
809
|
},
|
|
781
810
|
{
|
|
782
811
|
"title": "🔒 The deck's own data is no longer readable by anything on the machine",
|
|
@@ -846,7 +875,7 @@
|
|
|
846
875
|
"1.48.0": [
|
|
847
876
|
{
|
|
848
877
|
"title": "👁 Browser Watch — see what a program opened while you were away",
|
|
849
|
-
"body": "
|
|
878
|
+
"body": "Browser Watch is new, on the window's right edge (or press B). It watches the browsers on this machine and tells you when a program — an extension, an agent, a command — opened pages while nobody was touching the browser.\n\nUsually that is your own agent doing what you asked, and the panel says so rather than crying intrusion. What it gives you is the evidence: which pages, in which browser, at what time, with every address written out in full.\n\nIt is off until you switch it on, and nothing leaves this machine. It reads a copy of the browser's own history and only pages newer than the moment the deck started — never anything from before, never cookies, never saved passwords. Open ⚙ inside the panel and \"What Browser Watch can access\" says exactly what it reads, keeps and sends.\n\nWhen it finds something, its button shows a count. You can dismiss a finding once you have looked at it and it stays gone, while the log file on disk keeps the record. On a Mac it can also close the tab it found, or quit the browser."
|
|
850
879
|
},
|
|
851
880
|
{
|
|
852
881
|
"title": "🔑 Move all your accounts to another machine in one paste",
|
|
@@ -866,7 +895,7 @@
|
|
|
866
895
|
"1.46.0": [
|
|
867
896
|
{
|
|
868
897
|
"title": "🔊 Pick a volume and a sound for each of the two tones",
|
|
869
|
-
"body": "
|
|
898
|
+
"body": "Open Settings › Sounds, or press V. There are two tones — one when a turn finishes, one when Claude is waiting for you — and each has its own volume and its own sound, six to choose from. Press \"Hear it\" to try one straight away, without waiting for a real turn.\n\nThe two lists are separate on purpose, so you can never end up with both tones sounding the same. A finish tone always falls; an asking tone always rises. That is how you tell them apart without looking at the screen.\n\nM still turns the sound on and off from anywhere."
|
|
870
899
|
},
|
|
871
900
|
{
|
|
872
901
|
"title": "✨ This box is new",
|
|
@@ -33,10 +33,16 @@
|
|
|
33
33
|
// the same GitLab project — settings, jobs, branches. Fourteen rows
|
|
34
34
|
// describe that worse than one card does. That is what toEpisodes is for.
|
|
35
35
|
// * Without exclusions the deck reports ITSELF: 41 visits to 127.0.0.1:4317
|
|
36
|
-
// and 34 to 127.0.0.1:4399 carry FROM_API,
|
|
37
|
-
// through `open` on every start (bin/deck.js) and the listen fallback range
|
|
36
|
+
// and 34 to 127.0.0.1:4399 carry FROM_API, and the listen fallback range
|
|
38
37
|
// means it is not always the same port. defaultExclusions() is that, and
|
|
39
|
-
// only that.
|
|
38
|
+
// only that. This note used to say `open` was the reason — ccdeck opens its
|
|
39
|
+
// own tab with it on every start (bin/deck.js). Retested on 2026-10-14, a
|
|
40
|
+
// URL that `open` (macOS, Chrome 155) or `xdg-open` (Linux, Brave 1.95)
|
|
41
|
+
// hands to a running Chromium is recorded as AUTO_TOPLEVEL WITHOUT the bit,
|
|
42
|
+
// so those 75 visits most likely came from something else that loaded the
|
|
43
|
+
// deck's page (they were FROM_API over LINK, the shape an extension's
|
|
44
|
+
// tabs.update leaves). The exclusion stands either way: the deck's own
|
|
45
|
+
// origin does collect FROM_API visits.
|
|
40
46
|
//
|
|
41
47
|
// WHAT "NOBODY WAS AT THE BROWSER" MEANS HERE. It is derived from the visit
|
|
42
48
|
// list itself: a human visit is any visit WITHOUT the bit. There is no OS idle
|
|
@@ -65,8 +71,25 @@
|
|
|
65
71
|
// reports program navigation; the person reading the card decides what it was.
|
|
66
72
|
|
|
67
73
|
/** Chrome's PAGE_TRANSITION_FROM_API qualifier — bit 27 of `visits.transition`.
|
|
68
|
-
*
|
|
69
|
-
*
|
|
74
|
+
* Chromium defines it only as "originated from an external application", so
|
|
75
|
+
* what sets it is Chrome's own code, and it is narrower than "any API":
|
|
76
|
+
*
|
|
77
|
+
* SET by a page loaded with the DevTools protocol's `Page.navigate`, whatever
|
|
78
|
+
* transition type was asked for (content/browser/devtools/protocol/
|
|
79
|
+
* page_handler.cc), which is how Playwright and Puppeteer go to an address,
|
|
80
|
+
* and by an extension's `chrome.tabs.update` with a URL
|
|
81
|
+
* (chrome/browser/extensions/api/tabs/tabs_api.cc).
|
|
82
|
+
*
|
|
83
|
+
* NOT SET by `Target.createTarget` with a URL (AUTO_TOPLEVEL,
|
|
84
|
+
* chrome/browser/devtools/chrome_devtools_manager_delegate.cc), an
|
|
85
|
+
* extension's `chrome.tabs.create` (LINK, extensions/open_tab_helper.cc), a
|
|
86
|
+
* link a program clicks, or a URL handed over by `open` or `xdg-open`
|
|
87
|
+
* (AUTO_TOPLEVEL, chrome/browser/ui/startup/startup_browser_creator_impl.cc).
|
|
88
|
+
*
|
|
89
|
+
* Tested on 2026-10-14 on Brave 1.95 (Chromium 153, Linux) and Chrome 155
|
|
90
|
+
* (macOS 26.6) with throwaway profiles. The NOT SET ones are invisible to this
|
|
91
|
+
* module, and since a visit without the bit counts as a person (classify),
|
|
92
|
+
* they can also cancel a program's flagged page within the quiet window. */
|
|
70
93
|
export const FROM_API = 0x08000000;
|
|
71
94
|
|
|
72
95
|
/** The same bit for the arbitrary-precision path below. */
|
|
@@ -227,11 +250,11 @@ function hostParts(text) {
|
|
|
227
250
|
* Exclusion matchers for the deck's own pages, from the origins it is listening
|
|
228
251
|
* on — `["http://127.0.0.1:4317"]`, or whatever the fallback range settled on.
|
|
229
252
|
*
|
|
230
|
-
* This is not politeness.
|
|
231
|
-
*
|
|
232
|
-
* exactly where the rule is most sensitive: nobody is
|
|
233
|
-
*
|
|
234
|
-
* frequent finding is the feature itself.
|
|
253
|
+
* This is not politeness. The measured profile held FROM_API visits to the
|
|
254
|
+
* deck's own origin (the header's note says why `open` is not their cause),
|
|
255
|
+
* and those visits land exactly where the rule is most sensitive: nobody is
|
|
256
|
+
* browsing while a program loads the deck's page. Unfiltered, the feature's
|
|
257
|
+
* loudest and most frequent finding is the feature itself.
|
|
235
258
|
*
|
|
236
259
|
* Each origin expands to its port on all three loopback spellings, because the
|
|
237
260
|
* deck opens 127.0.0.1 and a returning user may have typed localhost, and one
|
|
@@ -34,8 +34,9 @@ import { isProcessAlive } from "./deck-probe.mjs";
|
|
|
34
34
|
* `portRange`), so a machine that has been running decks for a month has tabs
|
|
35
35
|
* on several. The real profile this feature was tuned against carried 41 visits
|
|
36
36
|
* to 127.0.0.1:4317 and 34 to 127.0.0.1:4399 — two ports, both this deck, both
|
|
37
|
-
* FROM_API
|
|
38
|
-
* panel would have shown
|
|
37
|
+
* FROM_API (not from `open`, which left no FROM_API when tested: see FROM_API in
|
|
38
|
+
* agent-activity.mjs), and every one of them a card the panel would have shown
|
|
39
|
+
* its owner about itself.
|
|
39
40
|
*
|
|
40
41
|
* The whole range rather than the ports seen: the alternative is to remember
|
|
41
42
|
* which ports past decks used, which is a file to keep, a file to migrate, and
|
|
@@ -664,10 +664,9 @@ async function readProfile(profile, { sinceChromeTime, copyDir, deps, quietMs, o
|
|
|
664
664
|
* Everything the panel draws, in one object.
|
|
665
665
|
*
|
|
666
666
|
* `deckOrigins` are the addresses this deck is listening on. They are excluded
|
|
667
|
-
* by default and it is not an optimisation:
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
* time it launches.
|
|
667
|
+
* by default and it is not an optimisation: the deck's own origin collects
|
|
668
|
+
* FROM_API visits like any other program's page (see defaultExclusions), so a
|
|
669
|
+
* watch without them reports the deck as the intruder.
|
|
671
670
|
*/
|
|
672
671
|
export async function browserWatchSnapshot({
|
|
673
672
|
deckOrigins = [],
|
|
@@ -244,8 +244,8 @@ async function readCodexHeader(path) {
|
|
|
244
244
|
// write it to the log they share. The event is still buffered and broadcast —
|
|
245
245
|
// every deck watching the session draws it — it is only the second copy on disk
|
|
246
246
|
// that is dropped. See writesCodexLog in log-election.mjs.
|
|
247
|
-
function emitCodexEvent(payload, persist) {
|
|
248
|
-
pushEvent(payload, "codex", { persist });
|
|
247
|
+
function emitCodexEvent(payload, persist, toolAuditAt) {
|
|
248
|
+
pushEvent(payload, "codex", { persist, toolAuditAt });
|
|
249
249
|
}
|
|
250
250
|
|
|
251
251
|
/**
|
|
@@ -405,7 +405,7 @@ function emitCodexLines(state, consume, persist) {
|
|
|
405
405
|
}
|
|
406
406
|
if (payload) {
|
|
407
407
|
ensureCodexRoot(state, persist);
|
|
408
|
-
emitCodexEvent(payload, persist);
|
|
408
|
+
emitCodexEvent(payload, persist, typeof obj.timestamp === "string" ? Date.parse(obj.timestamp) : undefined);
|
|
409
409
|
}
|
|
410
410
|
}
|
|
411
411
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// imports reach pushEvent through event-sink.mjs, connected below as this
|
|
10
10
|
// module loads; index.mjs's routes import it from here. The bodies are
|
|
11
11
|
// unchanged.
|
|
12
|
+
import { noteToolAudit } from "./tool-audit.mjs";
|
|
12
13
|
import { PRODUCT } from "./brand.mjs";
|
|
13
14
|
import { createBlockNotifier } from "./block-notify.mjs";
|
|
14
15
|
import { notify as osNotify } from "./browser-react.mjs";
|
|
@@ -122,6 +123,7 @@ export function pushEvent(raw, source, opts = {}) {
|
|
|
122
123
|
// Numbered, charged, held, and the ring evicted behind it — see admitEvent.
|
|
123
124
|
const evt = admitEvent(raw, source, opts.receivedAt ?? Date.now());
|
|
124
125
|
const seq = evt.seq;
|
|
126
|
+
noteToolAudit(raw, source, opts);
|
|
125
127
|
|
|
126
128
|
// Does this event reach the log at all? Not on a replay (it came from
|
|
127
129
|
// there), not when the hook told us another deck owns this session's log,
|
|
@@ -139,6 +139,7 @@ export function upstreamForm({ fields, images }, facts) {
|
|
|
139
139
|
if (contact) text("contact", contact);
|
|
140
140
|
text("appVersion", facts.appVersion);
|
|
141
141
|
text("platform", facts.platform);
|
|
142
|
+
if (facts.deviceId) text("deviceId", facts.deviceId);
|
|
142
143
|
for (const [index, { data, format }] of images.entries()) {
|
|
143
144
|
const { type, extension } = FORMAT[format];
|
|
144
145
|
parts.push(`${head(IMAGES_FIELD, `; filename="image-${index + 1}.${extension}"`)}Content-Type: ${type}\r\n\r\n`, data, "\r\n");
|
package/src/server/index.mjs
CHANGED
|
@@ -164,6 +164,8 @@ import { handleBrowserWatch, handleBrowserWatchDismiss, handleBrowserWatchSettin
|
|
|
164
164
|
// The music routes, every one behind AGENTS_DECK_NO_MUSIC — see music-routes.mjs.
|
|
165
165
|
import { handleBestOfNostalgia, handleCafeMusicBgm, handleClaudeFm, handleFmStation, handleGoodLifeRadio, handleLiveRadioMix, handleLofiGirl, handleRadioStations, handleRadioClick, handleMusicConfig } from "./music-routes.mjs";
|
|
166
166
|
// The usage panel's quota and history reads — see usage-routes.mjs.
|
|
167
|
+
import { startToolAudit } from "./tool-audit.mjs";
|
|
168
|
+
import { handleToolAudit } from "./tool-audit-routes.mjs";
|
|
167
169
|
import { startQuotaHistory } from "./quota-history.mjs";
|
|
168
170
|
import { handleCcusage, handleCcusageSession, handleCodexQuota, handleCodexUsage, handleProviderStatus, handleQuota, handleQuotaHistory } from "./usage-routes.mjs";
|
|
169
171
|
// The port fallback, one listen attempt, and the words for a failed one — see
|
|
@@ -192,6 +194,7 @@ export function requestUrl(rawUrl) {
|
|
|
192
194
|
|
|
193
195
|
export async function startServer({ port = 4317, host = "127.0.0.1", persist = null, portRange = [4318, 4400], workspace = "", codex = true, claude = true, onRestart = null, onStop = null, cswapQuiet = null } = {}) {
|
|
194
196
|
startQuotaHistory();
|
|
197
|
+
await startToolAudit();
|
|
195
198
|
armLifecycle({ onRestart, onStop, persist, onSelfUpdate: () => reporter.noteSelfUpdate() });
|
|
196
199
|
// Which tree and which CLIs, normalised once — see setDeckScope.
|
|
197
200
|
setDeckScope({ workspace, claude, codex });
|
|
@@ -318,6 +321,7 @@ export async function startServer({ port = 4317, host = "127.0.0.1", persist = n
|
|
|
318
321
|
if (req.method === "POST" && url.pathname === "/api/restart") return guard(handleRestart(req, res), res);
|
|
319
322
|
if (req.method === "POST" && url.pathname === "/api/presence") return guard(handlePresence(req, res), res);
|
|
320
323
|
if (req.method === "POST" && url.pathname === "/api/shutdown") return guard(handleStop(req, res), res);
|
|
324
|
+
if (["GET", "POST"].includes(req.method) && ["/api/tool-audit", "/api/tool-audit/storage"].includes(url.pathname)) return guard(handleToolAudit(req, res), res);
|
|
321
325
|
if (req.method === "GET" && url.pathname === "/api/quota/history") return guard(handleQuotaHistory(req, res), res);
|
|
322
326
|
if (req.method === "GET" && url.pathname === "/api/quota") return guard(handleQuota(req, res), res);
|
|
323
327
|
if (req.method === "GET" && url.pathname === "/api/codex-usage") return guard(handleCodexUsage(req, res), res);
|
|
@@ -7,7 +7,7 @@ import { reportsVetoed } from "./deck-prefs.mjs";
|
|
|
7
7
|
import { MAX_REQUEST_BYTES, isMultipart, readFeedbackForm, upstreamForm } from "./feedback-form.mjs";
|
|
8
8
|
import { readBody, readBytes, send } from "./http-io.mjs";
|
|
9
9
|
import { heldPrefs } from "./prefs-state.mjs";
|
|
10
|
-
import { REPORTS_API, installFacts, reporter } from "./reports.mjs";
|
|
10
|
+
import { REPORTS_API, deviceIdToken, installFacts, reporter, reportsOn } from "./reports.mjs";
|
|
11
11
|
|
|
12
12
|
async function readJson(req, res, limit) {
|
|
13
13
|
const raw = await readBody(req, res, limit).catch(() => null);
|
|
@@ -72,35 +72,54 @@ export async function handleClientError(req, res, { report = reporter } = {}) {
|
|
|
72
72
|
* one message. AGENTS_DECK_NO_INSTALL=1 still wins, because the README promises
|
|
73
73
|
* that it "turns off everything but the quota reads", and a launch script's
|
|
74
74
|
* word about the machine outranks a button on the page.
|
|
75
|
+
*
|
|
76
|
+
* `device` is the fingerprint feedbackDeviceId picks; a test passes its own,
|
|
77
|
+
* or null for none.
|
|
75
78
|
*/
|
|
76
|
-
export async function handleFeedback(req, res, { fetchImpl = globalThis.fetch, env = process.env } = {}) {
|
|
79
|
+
export async function handleFeedback(req, res, { fetchImpl = globalThis.fetch, env = process.env, device = feedbackDeviceId({ env }) } = {}) {
|
|
77
80
|
if (env.AGENTS_DECK_NO_INSTALL === "1") return send(res, 403, { ok: false, reason: "vetoed" });
|
|
81
|
+
// Left out of the post entirely when there is none: JSON drops an undefined.
|
|
82
|
+
const deviceId = device || undefined;
|
|
78
83
|
const contentType = req.headers?.["content-type"];
|
|
79
|
-
if (isMultipart(contentType)) return forwardFeedbackForm(req, res, contentType, { fetchImpl, env });
|
|
84
|
+
if (isMultipart(contentType)) return forwardFeedbackForm(req, res, contentType, { fetchImpl, env, deviceId });
|
|
80
85
|
const body = await readJson(req, res, 32_000);
|
|
81
86
|
if (!body) return send(res, 400, { ok: false, reason: "bad_request" });
|
|
82
87
|
const facts = feedbackFacts({ env });
|
|
83
88
|
const contact = typeof body.contact === "string" && body.contact.trim() ? body.contact.trim() : undefined;
|
|
84
89
|
return forwardFeedback(res, fetchImpl, {
|
|
85
90
|
headers: { "content-type": "application/json", "user-agent": `ccdeck/${facts.appVersion}` },
|
|
86
|
-
body: JSON.stringify({ kind: body.kind, title: body.title, body: body.body, contact, ...facts }),
|
|
91
|
+
body: JSON.stringify({ kind: body.kind, title: body.title, body: body.body, contact, ...facts, deviceId }),
|
|
87
92
|
timeoutMs: JSON_TIMEOUT_MS,
|
|
88
93
|
});
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
/**
|
|
92
|
-
* What this deck adds to a piece of
|
|
93
|
-
* version, and the system as `os-arch` — `darwin-arm64`.
|
|
94
|
-
*
|
|
95
|
-
* and
|
|
96
|
-
*
|
|
97
|
-
*
|
|
97
|
+
* What the dialog says, before Send, that this deck adds to a piece of
|
|
98
|
+
* feedback: the ccdeck version, and the system as `os-arch` — `darwin-arm64`.
|
|
99
|
+
* Both posts carry these two, from this one function, so what the line shows
|
|
100
|
+
* and what the API gets for them cannot drift apart.
|
|
101
|
+
*
|
|
102
|
+
* The posts also carry the device fingerprint (feedbackDeviceId), and the line
|
|
103
|
+
* does not show it: the owner's decision of 2026-10-14, so a report can be
|
|
104
|
+
* tied to the install that sent it. installFacts' other fields (the channel,
|
|
105
|
+
* the runtime, a locale) never go with feedback.
|
|
98
106
|
*/
|
|
99
107
|
export function feedbackFacts({ env = process.env } = {}) {
|
|
100
108
|
const facts = installFacts({ env });
|
|
101
109
|
return { appVersion: facts.version, platform: `${facts.os}-${facts.arch}` };
|
|
102
110
|
}
|
|
103
111
|
|
|
112
|
+
/**
|
|
113
|
+
* The device fingerprint a piece of feedback carries: the same one-way hash
|
|
114
|
+
* every usage report sends (deviceIdToken), and under the same rule — only
|
|
115
|
+
* while reports are on. Switched off, or vetoed with AGENTS_DECK_NO_REPORTS=1,
|
|
116
|
+
* the fingerprint stays home here as it does everywhere else, and the feedback
|
|
117
|
+
* still goes. It is read here, never taken from the page.
|
|
118
|
+
*/
|
|
119
|
+
export function feedbackDeviceId({ env = process.env, prefs = heldPrefs.current() } = {}) {
|
|
120
|
+
return reportsOn(prefs, env) ? deviceIdToken() : undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
104
123
|
/** `GET /api/feedback`: what a report sent from here would carry besides its
|
|
105
124
|
* words, for the dialog to say before Send. Nothing leaves the machine. */
|
|
106
125
|
export function handleFeedbackFacts(_req, res, { env = process.env } = {}) {
|
|
@@ -113,7 +132,7 @@ const JSON_TIMEOUT_MS = 20_000;
|
|
|
113
132
|
* seconds of upload before the API has read a byte of it. */
|
|
114
133
|
const FORM_TIMEOUT_MS = 120_000;
|
|
115
134
|
|
|
116
|
-
async function forwardFeedbackForm(req, res, contentType, { fetchImpl, env }) {
|
|
135
|
+
async function forwardFeedbackForm(req, res, contentType, { fetchImpl, env, deviceId }) {
|
|
117
136
|
// Over the cap, readBytes has already answered 413 too_large and the 400
|
|
118
137
|
// below is a no-op; this is the answer to a read that failed some other way.
|
|
119
138
|
const bytes = await readBytes(req, res, MAX_REQUEST_BYTES, { ok: false, reason: "too_large" }).catch(() => null);
|
|
@@ -125,7 +144,7 @@ async function forwardFeedbackForm(req, res, contentType, { fetchImpl, env }) {
|
|
|
125
144
|
return forwardFeedback(res, fetchImpl, {
|
|
126
145
|
// No content type: fetch writes the form's own, boundary and all.
|
|
127
146
|
headers: { "user-agent": `ccdeck/${facts.appVersion}` },
|
|
128
|
-
body: upstreamForm(form, facts),
|
|
147
|
+
body: upstreamForm(form, { ...facts, deviceId }),
|
|
129
148
|
timeoutMs: FORM_TIMEOUT_MS,
|
|
130
149
|
});
|
|
131
150
|
}
|
|
@@ -430,6 +430,8 @@ export function isAuthorizedDataRead(req) {
|
|
|
430
430
|
* costs nothing: every fetch of both is in src/web, and a page's own GET
|
|
431
431
|
* carries Sec-Fetch-Site: same-origin. */
|
|
432
432
|
export const GUARDED_READS = new Set([
|
|
433
|
+
"/api/tool-audit",
|
|
434
|
+
"/api/tool-audit/storage",
|
|
433
435
|
"/api/system/traffic-radar",
|
|
434
436
|
"/api/system/traffic-radar/capture",
|
|
435
437
|
"/api/system/traffic-radar/capture/event",
|