davinci-resolve-mcp 2.24.0 → 2.25.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/CHANGELOG.md +130 -4
- package/README.md +6 -1
- package/bin/davinci-resolve-mcp.mjs +4 -1
- package/docs/SKILL.md +176 -18
- package/docs/guides/color-decision-guide.md +40 -0
- package/docs/guides/control-panel.md +128 -0
- package/docs/guides/editorial-decision-guide.md +29 -0
- package/docs/reference/api-coverage.md +8 -2
- package/install.py +291 -24
- package/package.json +1 -1
- package/src/analysis_dashboard.py +2535 -35
- package/src/granular/common.py +1 -1
- package/src/granular/media_pool_item.py +54 -4
- package/src/server.py +2319 -86
- package/src/utils/analysis_caps.py +669 -0
- package/src/utils/analysis_memory.py +1 -1
- package/src/utils/analysis_runs.py +302 -0
- package/src/utils/brain_edits.py +380 -0
- package/src/utils/destructive_hook.py +615 -0
- package/src/utils/failure_tracker.py +119 -0
- package/src/utils/media_analysis.py +1687 -56
- package/src/utils/media_analysis_jobs.py +3 -0
- package/src/utils/media_pool_changes.py +116 -0
- package/src/utils/timeline_brain_db.py +475 -0
- package/src/utils/timeline_versioning.py +648 -0
- package/src/utils/update_check.py +72 -6
- package/docs/design/control-panel-polish-gameplan.md +0 -238
- package/docs/design/v2-control-panel-design.md +0 -184
- package/docs/design/v2-implementation-gameplan.md +0 -593
- package/docs/design/v2-shot-schema-spec.md +0 -500
|
@@ -14,10 +14,11 @@ import webbrowser
|
|
|
14
14
|
from http import HTTPStatus
|
|
15
15
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
16
16
|
from typing import Any, Dict, List, Optional, Tuple
|
|
17
|
-
from urllib.parse import parse_qs, urlparse
|
|
17
|
+
from urllib.parse import parse_qs, unquote, urlparse
|
|
18
18
|
|
|
19
19
|
from src.utils.media_analysis import (
|
|
20
20
|
analysis_index_status,
|
|
21
|
+
analysis_root_coverage,
|
|
21
22
|
build_analysis_index,
|
|
22
23
|
detect_capabilities,
|
|
23
24
|
query_analysis_index,
|
|
@@ -36,6 +37,9 @@ from src.utils.media_analysis_jobs import (
|
|
|
36
37
|
)
|
|
37
38
|
from src.utils.platform import setup_environment
|
|
38
39
|
from src.utils.analysis_memory import read_panel_state, write_panel_state
|
|
40
|
+
from src.utils import brain_edits as _brain_edits
|
|
41
|
+
from src.utils import timeline_versioning as _timeline_versioning
|
|
42
|
+
from src.utils import timeline_brain_db as _timeline_brain_db
|
|
39
43
|
|
|
40
44
|
|
|
41
45
|
HTML = r"""<!doctype html>
|
|
@@ -1360,6 +1364,76 @@ HTML = r"""<!doctype html>
|
|
|
1360
1364
|
color: var(--text-tertiary);
|
|
1361
1365
|
min-height: 14px;
|
|
1362
1366
|
}
|
|
1367
|
+
/* ─── Readiness card (coverage_report rollup) ─────────────────────── */
|
|
1368
|
+
.readiness-card {
|
|
1369
|
+
margin: var(--space-4) 0;
|
|
1370
|
+
padding: var(--space-3) var(--space-4);
|
|
1371
|
+
border: 1px solid var(--border-default);
|
|
1372
|
+
background: var(--lab-panel-elevated);
|
|
1373
|
+
border-radius: var(--radius-md);
|
|
1374
|
+
display: flex;
|
|
1375
|
+
flex-direction: column;
|
|
1376
|
+
gap: var(--space-2);
|
|
1377
|
+
}
|
|
1378
|
+
.readiness-card-header {
|
|
1379
|
+
display: flex;
|
|
1380
|
+
align-items: center;
|
|
1381
|
+
gap: var(--space-3);
|
|
1382
|
+
}
|
|
1383
|
+
.readiness-title {
|
|
1384
|
+
font-weight: 600;
|
|
1385
|
+
color: var(--text-primary);
|
|
1386
|
+
letter-spacing: 0.02em;
|
|
1387
|
+
text-transform: uppercase;
|
|
1388
|
+
font-size: 11px;
|
|
1389
|
+
}
|
|
1390
|
+
.readiness-evidence {
|
|
1391
|
+
flex: 1;
|
|
1392
|
+
color: var(--text-secondary);
|
|
1393
|
+
font-size: 13px;
|
|
1394
|
+
line-height: 1.4;
|
|
1395
|
+
}
|
|
1396
|
+
.readiness-summary-row {
|
|
1397
|
+
display: flex;
|
|
1398
|
+
flex-wrap: wrap;
|
|
1399
|
+
gap: var(--space-3);
|
|
1400
|
+
}
|
|
1401
|
+
.readiness-stat {
|
|
1402
|
+
display: flex;
|
|
1403
|
+
flex-direction: column;
|
|
1404
|
+
gap: 2px;
|
|
1405
|
+
min-width: 84px;
|
|
1406
|
+
padding: var(--space-2) var(--space-3);
|
|
1407
|
+
border-radius: var(--radius-sm);
|
|
1408
|
+
background: var(--bg-subtle);
|
|
1409
|
+
}
|
|
1410
|
+
.readiness-stat .stat-value {
|
|
1411
|
+
font-size: 22px;
|
|
1412
|
+
font-weight: 600;
|
|
1413
|
+
color: var(--text-primary);
|
|
1414
|
+
}
|
|
1415
|
+
.readiness-stat .stat-label {
|
|
1416
|
+
font-size: 10.5px;
|
|
1417
|
+
color: var(--text-tertiary);
|
|
1418
|
+
text-transform: uppercase;
|
|
1419
|
+
letter-spacing: 0.05em;
|
|
1420
|
+
}
|
|
1421
|
+
.readiness-stat.warn .stat-value { color: var(--accent-warn, #d18b00); }
|
|
1422
|
+
.readiness-stat.danger .stat-value { color: var(--accent-danger, #d24c4c); }
|
|
1423
|
+
.readiness-stat.good .stat-value { color: var(--accent-success, #4caf50); }
|
|
1424
|
+
.readiness-details {
|
|
1425
|
+
color: var(--text-tertiary);
|
|
1426
|
+
font-size: 11.5px;
|
|
1427
|
+
display: flex;
|
|
1428
|
+
flex-wrap: wrap;
|
|
1429
|
+
gap: var(--space-3);
|
|
1430
|
+
}
|
|
1431
|
+
.readiness-details .chip {
|
|
1432
|
+
padding: 2px 8px;
|
|
1433
|
+
border-radius: 999px;
|
|
1434
|
+
background: var(--bg-subtle);
|
|
1435
|
+
color: var(--text-secondary);
|
|
1436
|
+
}
|
|
1363
1437
|
/* ─── V2 Review surface ───────────────────────────────────────────── */
|
|
1364
1438
|
.review-grid {
|
|
1365
1439
|
display: grid;
|
|
@@ -2084,9 +2158,817 @@ HTML = r"""<!doctype html>
|
|
|
2084
2158
|
flex: 0 0 160px;
|
|
2085
2159
|
aspect-ratio: 16 / 9;
|
|
2086
2160
|
}
|
|
2087
|
-
.review-list .review-clip-card-name { flex: 1 1 auto; }
|
|
2088
|
-
.review-list .review-clip-card-meta { flex-shrink: 0; }
|
|
2089
|
-
.review-list .review-clip-card-oneliner { flex: 2 1 0; }
|
|
2161
|
+
.review-list .review-clip-card-name { flex: 1 1 auto; }
|
|
2162
|
+
.review-list .review-clip-card-meta { flex-shrink: 0; }
|
|
2163
|
+
.review-list .review-clip-card-oneliner { flex: 2 1 0; }
|
|
2164
|
+
|
|
2165
|
+
/* ─── C6 Timeline history view ──────────────────────────────────── */
|
|
2166
|
+
.history-layout {
|
|
2167
|
+
display: grid;
|
|
2168
|
+
grid-template-columns: 280px 1fr;
|
|
2169
|
+
gap: var(--space-3);
|
|
2170
|
+
align-items: start;
|
|
2171
|
+
}
|
|
2172
|
+
.history-sidebar {
|
|
2173
|
+
border: 1px solid var(--border-default);
|
|
2174
|
+
border-radius: var(--radius-md);
|
|
2175
|
+
background: var(--lab-panel-elevated);
|
|
2176
|
+
padding: var(--space-2);
|
|
2177
|
+
display: flex;
|
|
2178
|
+
flex-direction: column;
|
|
2179
|
+
gap: var(--space-2);
|
|
2180
|
+
max-height: 70vh;
|
|
2181
|
+
overflow-y: auto;
|
|
2182
|
+
}
|
|
2183
|
+
.history-sidebar-header {
|
|
2184
|
+
display: flex;
|
|
2185
|
+
align-items: center;
|
|
2186
|
+
justify-content: space-between;
|
|
2187
|
+
gap: var(--space-2);
|
|
2188
|
+
font-size: var(--text-sm);
|
|
2189
|
+
}
|
|
2190
|
+
.history-timeline-list {
|
|
2191
|
+
display: flex;
|
|
2192
|
+
flex-direction: column;
|
|
2193
|
+
gap: var(--space-1);
|
|
2194
|
+
}
|
|
2195
|
+
.history-timeline-row {
|
|
2196
|
+
cursor: pointer;
|
|
2197
|
+
padding: var(--space-1) var(--space-2);
|
|
2198
|
+
border-radius: var(--radius-sm);
|
|
2199
|
+
border: 1px solid transparent;
|
|
2200
|
+
display: flex;
|
|
2201
|
+
justify-content: space-between;
|
|
2202
|
+
align-items: center;
|
|
2203
|
+
gap: var(--space-2);
|
|
2204
|
+
font-size: var(--text-sm);
|
|
2205
|
+
}
|
|
2206
|
+
.history-timeline-row:hover { background: var(--bg-muted); }
|
|
2207
|
+
.history-timeline-row.is-selected {
|
|
2208
|
+
border-color: var(--accent-primary);
|
|
2209
|
+
background: var(--bg-muted);
|
|
2210
|
+
}
|
|
2211
|
+
.history-timeline-row .name { font-weight: 500; }
|
|
2212
|
+
.history-timeline-row .count {
|
|
2213
|
+
font-size: var(--text-xs);
|
|
2214
|
+
color: var(--text-muted);
|
|
2215
|
+
}
|
|
2216
|
+
.history-archive-now {
|
|
2217
|
+
display: flex;
|
|
2218
|
+
flex-direction: column;
|
|
2219
|
+
gap: var(--space-1);
|
|
2220
|
+
margin-top: auto;
|
|
2221
|
+
padding-top: var(--space-2);
|
|
2222
|
+
border-top: 1px solid var(--border-default);
|
|
2223
|
+
}
|
|
2224
|
+
.history-archive-now button { width: 100%; }
|
|
2225
|
+
.history-archive-now input {
|
|
2226
|
+
width: 100%;
|
|
2227
|
+
padding: var(--space-1) var(--space-2);
|
|
2228
|
+
border: 1px solid var(--border-default);
|
|
2229
|
+
background: var(--bg-base);
|
|
2230
|
+
color: var(--text-primary);
|
|
2231
|
+
border-radius: var(--radius-sm);
|
|
2232
|
+
font-size: var(--text-xs);
|
|
2233
|
+
}
|
|
2234
|
+
.history-detail {
|
|
2235
|
+
border: 1px solid var(--border-default);
|
|
2236
|
+
border-radius: var(--radius-md);
|
|
2237
|
+
background: var(--lab-panel-elevated);
|
|
2238
|
+
padding: var(--space-3);
|
|
2239
|
+
min-height: 300px;
|
|
2240
|
+
}
|
|
2241
|
+
.history-detail-header {
|
|
2242
|
+
display: flex;
|
|
2243
|
+
justify-content: space-between;
|
|
2244
|
+
align-items: baseline;
|
|
2245
|
+
gap: var(--space-2);
|
|
2246
|
+
margin-bottom: var(--space-3);
|
|
2247
|
+
padding-bottom: var(--space-2);
|
|
2248
|
+
border-bottom: 1px solid var(--border-default);
|
|
2249
|
+
}
|
|
2250
|
+
.history-detail-header .timeline-name {
|
|
2251
|
+
font-size: var(--text-base);
|
|
2252
|
+
font-weight: 600;
|
|
2253
|
+
}
|
|
2254
|
+
.history-detail-body {
|
|
2255
|
+
display: flex;
|
|
2256
|
+
flex-direction: column;
|
|
2257
|
+
gap: var(--space-3);
|
|
2258
|
+
}
|
|
2259
|
+
.history-version-card {
|
|
2260
|
+
border: 1px solid var(--border-default);
|
|
2261
|
+
border-radius: var(--radius-sm);
|
|
2262
|
+
padding: var(--space-2);
|
|
2263
|
+
background: var(--bg-base);
|
|
2264
|
+
display: grid;
|
|
2265
|
+
grid-template-columns: 160px 1fr;
|
|
2266
|
+
gap: var(--space-3);
|
|
2267
|
+
}
|
|
2268
|
+
.history-version-card .thumb {
|
|
2269
|
+
aspect-ratio: 16/9;
|
|
2270
|
+
background: var(--bg-muted);
|
|
2271
|
+
border-radius: var(--radius-sm);
|
|
2272
|
+
overflow: hidden;
|
|
2273
|
+
display: flex;
|
|
2274
|
+
align-items: center;
|
|
2275
|
+
justify-content: center;
|
|
2276
|
+
color: var(--text-muted);
|
|
2277
|
+
font-size: var(--text-xs);
|
|
2278
|
+
}
|
|
2279
|
+
.history-version-card .thumb img {
|
|
2280
|
+
width: 100%;
|
|
2281
|
+
height: 100%;
|
|
2282
|
+
object-fit: cover;
|
|
2283
|
+
display: block;
|
|
2284
|
+
}
|
|
2285
|
+
.history-version-card .body {
|
|
2286
|
+
display: flex;
|
|
2287
|
+
flex-direction: column;
|
|
2288
|
+
gap: var(--space-2);
|
|
2289
|
+
min-width: 0;
|
|
2290
|
+
}
|
|
2291
|
+
.history-version-card .version-row {
|
|
2292
|
+
display: flex;
|
|
2293
|
+
align-items: center;
|
|
2294
|
+
gap: var(--space-2);
|
|
2295
|
+
justify-content: space-between;
|
|
2296
|
+
}
|
|
2297
|
+
.history-version-card .version-label {
|
|
2298
|
+
font-size: var(--text-sm);
|
|
2299
|
+
font-weight: 600;
|
|
2300
|
+
}
|
|
2301
|
+
.history-version-card .archived-name {
|
|
2302
|
+
font-size: var(--text-xs);
|
|
2303
|
+
color: var(--text-muted);
|
|
2304
|
+
font-family: var(--font-mono);
|
|
2305
|
+
}
|
|
2306
|
+
.history-version-card .timestamp {
|
|
2307
|
+
font-size: var(--text-xs);
|
|
2308
|
+
color: var(--text-muted);
|
|
2309
|
+
}
|
|
2310
|
+
.history-version-card .reason {
|
|
2311
|
+
font-size: var(--text-xs);
|
|
2312
|
+
color: var(--text-muted);
|
|
2313
|
+
font-style: italic;
|
|
2314
|
+
}
|
|
2315
|
+
.history-version-card .drt-collapsed {
|
|
2316
|
+
font-size: var(--text-xs);
|
|
2317
|
+
color: var(--accent-warning);
|
|
2318
|
+
}
|
|
2319
|
+
.history-edits-table {
|
|
2320
|
+
width: 100%;
|
|
2321
|
+
border-collapse: collapse;
|
|
2322
|
+
font-size: var(--text-xs);
|
|
2323
|
+
}
|
|
2324
|
+
.history-edits-table th, .history-edits-table td {
|
|
2325
|
+
padding: var(--space-1) var(--space-2);
|
|
2326
|
+
border-bottom: 1px solid var(--border-default);
|
|
2327
|
+
text-align: left;
|
|
2328
|
+
vertical-align: top;
|
|
2329
|
+
}
|
|
2330
|
+
.history-edits-table th { color: var(--text-muted); font-weight: 500; }
|
|
2331
|
+
.history-edits-table .edit-type {
|
|
2332
|
+
font-family: var(--font-mono);
|
|
2333
|
+
color: var(--text-primary);
|
|
2334
|
+
}
|
|
2335
|
+
.history-edits-table .metric { color: var(--text-muted); }
|
|
2336
|
+
.history-edits-table .delta-pos { color: var(--accent-success); }
|
|
2337
|
+
.history-edits-table .delta-neg { color: var(--accent-error); }
|
|
2338
|
+
.history-edits-table .delta-zero { color: var(--text-muted); }
|
|
2339
|
+
.history-rollback-btn {
|
|
2340
|
+
background: var(--accent-warning-muted);
|
|
2341
|
+
color: var(--accent-warning);
|
|
2342
|
+
border: 1px solid var(--accent-warning);
|
|
2343
|
+
padding: var(--space-1) var(--space-2);
|
|
2344
|
+
border-radius: var(--radius-sm);
|
|
2345
|
+
cursor: pointer;
|
|
2346
|
+
font-size: var(--text-xs);
|
|
2347
|
+
}
|
|
2348
|
+
.history-rollback-btn:hover { background: var(--accent-warning); color: var(--bg-base); }
|
|
2349
|
+
|
|
2350
|
+
/* ─── Analysis caps preferences widget ──────────────────────────── */
|
|
2351
|
+
.caps-section {
|
|
2352
|
+
margin-top: var(--space-4);
|
|
2353
|
+
padding: var(--space-3);
|
|
2354
|
+
border: 1px solid var(--border-default);
|
|
2355
|
+
border-radius: var(--radius-md);
|
|
2356
|
+
background: var(--lab-panel-elevated, var(--bg-base));
|
|
2357
|
+
}
|
|
2358
|
+
.caps-section + .caps-section { margin-top: var(--space-3); }
|
|
2359
|
+
.caps-section-head {
|
|
2360
|
+
display: flex;
|
|
2361
|
+
flex-direction: column;
|
|
2362
|
+
gap: 2px;
|
|
2363
|
+
margin-bottom: var(--space-2);
|
|
2364
|
+
}
|
|
2365
|
+
.caps-section-title {
|
|
2366
|
+
font-size: var(--text-sm);
|
|
2367
|
+
font-weight: 600;
|
|
2368
|
+
color: var(--text-primary);
|
|
2369
|
+
}
|
|
2370
|
+
.caps-section-hint {
|
|
2371
|
+
font-size: var(--text-xs);
|
|
2372
|
+
color: var(--text-muted);
|
|
2373
|
+
}
|
|
2374
|
+
.caps-section-subtitle {
|
|
2375
|
+
font-size: var(--text-xs);
|
|
2376
|
+
color: var(--text-muted);
|
|
2377
|
+
margin-top: var(--space-2);
|
|
2378
|
+
margin-bottom: var(--space-1);
|
|
2379
|
+
text-transform: uppercase;
|
|
2380
|
+
letter-spacing: 0.04em;
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
/* Preset cards */
|
|
2384
|
+
.caps-preset-cards {
|
|
2385
|
+
display: grid;
|
|
2386
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
2387
|
+
gap: var(--space-2);
|
|
2388
|
+
}
|
|
2389
|
+
.caps-preset-card {
|
|
2390
|
+
display: flex;
|
|
2391
|
+
flex-direction: column;
|
|
2392
|
+
gap: var(--space-2);
|
|
2393
|
+
padding: var(--space-2) var(--space-3);
|
|
2394
|
+
border: 1px solid var(--border-default);
|
|
2395
|
+
border-radius: var(--radius-sm);
|
|
2396
|
+
background: var(--bg-base);
|
|
2397
|
+
cursor: pointer;
|
|
2398
|
+
text-align: left;
|
|
2399
|
+
transition: border-color 150ms ease, background 150ms ease, transform 150ms ease;
|
|
2400
|
+
color: inherit;
|
|
2401
|
+
font: inherit;
|
|
2402
|
+
}
|
|
2403
|
+
.caps-preset-card:hover {
|
|
2404
|
+
border-color: var(--accent-primary);
|
|
2405
|
+
background: var(--bg-muted);
|
|
2406
|
+
}
|
|
2407
|
+
.caps-preset-card.is-active {
|
|
2408
|
+
border-color: var(--accent-primary);
|
|
2409
|
+
background: var(--accent-primary-muted, rgba(64, 156, 255, 0.12));
|
|
2410
|
+
box-shadow: 0 0 0 1px var(--accent-primary) inset;
|
|
2411
|
+
}
|
|
2412
|
+
.caps-preset-card-head {
|
|
2413
|
+
display: flex;
|
|
2414
|
+
justify-content: space-between;
|
|
2415
|
+
align-items: baseline;
|
|
2416
|
+
}
|
|
2417
|
+
.caps-preset-card-name {
|
|
2418
|
+
font-size: var(--text-sm);
|
|
2419
|
+
font-weight: 600;
|
|
2420
|
+
text-transform: capitalize;
|
|
2421
|
+
}
|
|
2422
|
+
.caps-preset-card-badge {
|
|
2423
|
+
font-size: 10px;
|
|
2424
|
+
color: var(--accent-primary);
|
|
2425
|
+
text-transform: uppercase;
|
|
2426
|
+
letter-spacing: 0.04em;
|
|
2427
|
+
}
|
|
2428
|
+
.caps-preset-card-tag {
|
|
2429
|
+
font-size: var(--text-xs);
|
|
2430
|
+
color: var(--text-muted);
|
|
2431
|
+
}
|
|
2432
|
+
.caps-preset-card-stats {
|
|
2433
|
+
display: grid;
|
|
2434
|
+
grid-template-columns: 1fr auto;
|
|
2435
|
+
gap: 2px var(--space-1);
|
|
2436
|
+
font-size: 11px;
|
|
2437
|
+
font-family: var(--font-mono);
|
|
2438
|
+
color: var(--text-muted);
|
|
2439
|
+
}
|
|
2440
|
+
.caps-preset-card-stats .stat-label { color: var(--text-muted); }
|
|
2441
|
+
.caps-preset-card-stats .stat-value { color: var(--text-primary); text-align: right; }
|
|
2442
|
+
.caps-preset-card.is-active .stat-value { color: var(--accent-primary); }
|
|
2443
|
+
|
|
2444
|
+
/* Gauges */
|
|
2445
|
+
.caps-usage-block {
|
|
2446
|
+
padding: 0;
|
|
2447
|
+
background: transparent;
|
|
2448
|
+
border: 0;
|
|
2449
|
+
}
|
|
2450
|
+
.caps-usage-gauges {
|
|
2451
|
+
display: grid;
|
|
2452
|
+
grid-template-columns: repeat(3, 1fr);
|
|
2453
|
+
gap: var(--space-2);
|
|
2454
|
+
}
|
|
2455
|
+
.caps-gauge {
|
|
2456
|
+
display: flex;
|
|
2457
|
+
flex-direction: column;
|
|
2458
|
+
gap: 4px;
|
|
2459
|
+
padding: var(--space-2);
|
|
2460
|
+
border: 1px solid var(--border-default);
|
|
2461
|
+
border-radius: var(--radius-sm);
|
|
2462
|
+
background: var(--bg-base);
|
|
2463
|
+
}
|
|
2464
|
+
.caps-gauge-row {
|
|
2465
|
+
display: flex;
|
|
2466
|
+
justify-content: space-between;
|
|
2467
|
+
align-items: baseline;
|
|
2468
|
+
gap: var(--space-1);
|
|
2469
|
+
}
|
|
2470
|
+
.caps-gauge-label {
|
|
2471
|
+
font-size: 11px;
|
|
2472
|
+
color: var(--text-muted);
|
|
2473
|
+
text-transform: uppercase;
|
|
2474
|
+
letter-spacing: 0.04em;
|
|
2475
|
+
}
|
|
2476
|
+
.caps-gauge-bar {
|
|
2477
|
+
height: 6px;
|
|
2478
|
+
background: var(--bg-muted);
|
|
2479
|
+
border-radius: 3px;
|
|
2480
|
+
overflow: hidden;
|
|
2481
|
+
}
|
|
2482
|
+
.caps-gauge-fill {
|
|
2483
|
+
display: block;
|
|
2484
|
+
height: 100%;
|
|
2485
|
+
width: 0%;
|
|
2486
|
+
background: var(--accent-success);
|
|
2487
|
+
transition: width 200ms ease, background 200ms ease;
|
|
2488
|
+
}
|
|
2489
|
+
.caps-gauge[data-state="warn"] .caps-gauge-fill { background: var(--accent-warning); }
|
|
2490
|
+
.caps-gauge[data-state="over"] .caps-gauge-fill { background: var(--accent-error); }
|
|
2491
|
+
.caps-gauge-numbers {
|
|
2492
|
+
font-size: 11px;
|
|
2493
|
+
font-family: var(--font-mono);
|
|
2494
|
+
color: var(--text-primary);
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
/* Advanced overrides */
|
|
2498
|
+
.caps-advanced > summary,
|
|
2499
|
+
.caps-inspector-block > summary {
|
|
2500
|
+
cursor: pointer;
|
|
2501
|
+
display: flex;
|
|
2502
|
+
flex-direction: column;
|
|
2503
|
+
gap: 2px;
|
|
2504
|
+
list-style: none;
|
|
2505
|
+
padding: 0;
|
|
2506
|
+
margin: 0 0 var(--space-2);
|
|
2507
|
+
}
|
|
2508
|
+
.caps-advanced > summary::-webkit-details-marker,
|
|
2509
|
+
.caps-inspector-block > summary::-webkit-details-marker { display: none; }
|
|
2510
|
+
.caps-advanced > summary::before,
|
|
2511
|
+
.caps-inspector-block > summary::before {
|
|
2512
|
+
content: '▸';
|
|
2513
|
+
display: inline-block;
|
|
2514
|
+
margin-right: var(--space-1);
|
|
2515
|
+
color: var(--text-muted);
|
|
2516
|
+
transition: transform 120ms ease;
|
|
2517
|
+
}
|
|
2518
|
+
.caps-advanced[open] > summary::before,
|
|
2519
|
+
.caps-inspector-block[open] > summary::before { transform: rotate(90deg); }
|
|
2520
|
+
.caps-override-grid input {
|
|
2521
|
+
width: 100%;
|
|
2522
|
+
padding: var(--space-1) var(--space-2);
|
|
2523
|
+
border: 1px solid var(--border-default);
|
|
2524
|
+
background: var(--bg-base);
|
|
2525
|
+
color: var(--text-primary);
|
|
2526
|
+
border-radius: var(--radius-sm);
|
|
2527
|
+
font-size: var(--text-xs);
|
|
2528
|
+
font-family: var(--font-mono);
|
|
2529
|
+
}
|
|
2530
|
+
.caps-override-grid input::placeholder {
|
|
2531
|
+
color: var(--text-muted);
|
|
2532
|
+
opacity: 0.7;
|
|
2533
|
+
font-style: italic;
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
/* Safety subsection */
|
|
2537
|
+
.safety-strict-block {
|
|
2538
|
+
margin-top: var(--space-3);
|
|
2539
|
+
padding-top: var(--space-2);
|
|
2540
|
+
border-top: 1px dashed var(--border-default);
|
|
2541
|
+
}
|
|
2542
|
+
.safety-strict-title {
|
|
2543
|
+
font-size: var(--text-xs);
|
|
2544
|
+
color: var(--text-muted);
|
|
2545
|
+
margin-bottom: var(--space-1);
|
|
2546
|
+
text-transform: uppercase;
|
|
2547
|
+
letter-spacing: 0.04em;
|
|
2548
|
+
}
|
|
2549
|
+
.safety-strict-note {
|
|
2550
|
+
font-size: var(--text-xs);
|
|
2551
|
+
color: var(--text-muted);
|
|
2552
|
+
margin-top: var(--space-2);
|
|
2553
|
+
line-height: 1.5;
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
/* ─── Caps history chart + inspector + refusals ──────────────── */
|
|
2557
|
+
.caps-history-block { margin-top: var(--space-3); }
|
|
2558
|
+
.caps-history-title {
|
|
2559
|
+
font-size: var(--text-xs);
|
|
2560
|
+
color: var(--text-muted);
|
|
2561
|
+
margin-bottom: var(--space-1);
|
|
2562
|
+
}
|
|
2563
|
+
.caps-history-chart {
|
|
2564
|
+
border: 1px solid var(--border-default);
|
|
2565
|
+
background: var(--bg-base);
|
|
2566
|
+
border-radius: var(--radius-sm);
|
|
2567
|
+
padding: var(--space-2);
|
|
2568
|
+
min-height: 100px;
|
|
2569
|
+
font-size: var(--text-xs);
|
|
2570
|
+
color: var(--text-muted);
|
|
2571
|
+
}
|
|
2572
|
+
.caps-history-chart svg { width: 100%; height: 100px; display: block; }
|
|
2573
|
+
.caps-history-chart .axis { stroke: var(--border-default); stroke-width: 0.5; }
|
|
2574
|
+
.caps-history-chart .line { stroke: var(--accent-primary); stroke-width: 1.5; fill: none; }
|
|
2575
|
+
.caps-history-chart .point { fill: var(--accent-primary); }
|
|
2576
|
+
.caps-history-chart .label { fill: var(--text-muted); font-size: 9px; }
|
|
2577
|
+
|
|
2578
|
+
.caps-inspector { margin-top: var(--space-3); }
|
|
2579
|
+
.caps-inspector-row {
|
|
2580
|
+
display: flex;
|
|
2581
|
+
gap: var(--space-2);
|
|
2582
|
+
align-items: end;
|
|
2583
|
+
flex-wrap: wrap;
|
|
2584
|
+
}
|
|
2585
|
+
.caps-inspector-row label { flex: 1 1 200px; }
|
|
2586
|
+
.caps-inspector-row input {
|
|
2587
|
+
width: 100%;
|
|
2588
|
+
padding: var(--space-1) var(--space-2);
|
|
2589
|
+
border: 1px solid var(--border-default);
|
|
2590
|
+
background: var(--bg-base);
|
|
2591
|
+
color: var(--text-primary);
|
|
2592
|
+
border-radius: var(--radius-sm);
|
|
2593
|
+
font-size: var(--text-xs);
|
|
2594
|
+
font-family: var(--font-mono);
|
|
2595
|
+
}
|
|
2596
|
+
.caps-inspect-result {
|
|
2597
|
+
margin-top: var(--space-2);
|
|
2598
|
+
font-size: var(--text-xs);
|
|
2599
|
+
font-family: var(--font-mono);
|
|
2600
|
+
padding: var(--space-2);
|
|
2601
|
+
background: var(--bg-base);
|
|
2602
|
+
border: 1px solid var(--border-default);
|
|
2603
|
+
border-radius: var(--radius-sm);
|
|
2604
|
+
min-height: 30px;
|
|
2605
|
+
color: var(--text-muted);
|
|
2606
|
+
}
|
|
2607
|
+
.caps-inspect-result.has-data { color: var(--text-primary); }
|
|
2608
|
+
|
|
2609
|
+
.caps-refusals { margin-top: var(--space-3); }
|
|
2610
|
+
.caps-refusals summary {
|
|
2611
|
+
cursor: pointer;
|
|
2612
|
+
font-size: var(--text-sm);
|
|
2613
|
+
color: var(--text-muted);
|
|
2614
|
+
padding: var(--space-1) 0;
|
|
2615
|
+
}
|
|
2616
|
+
.caps-refusals-list {
|
|
2617
|
+
font-size: var(--text-xs);
|
|
2618
|
+
font-family: var(--font-mono);
|
|
2619
|
+
padding: var(--space-2);
|
|
2620
|
+
background: var(--bg-base);
|
|
2621
|
+
border: 1px solid var(--border-default);
|
|
2622
|
+
border-radius: var(--radius-sm);
|
|
2623
|
+
max-height: 240px;
|
|
2624
|
+
overflow-y: auto;
|
|
2625
|
+
}
|
|
2626
|
+
.caps-refusal-row {
|
|
2627
|
+
display: grid;
|
|
2628
|
+
grid-template-columns: auto 1fr auto;
|
|
2629
|
+
gap: var(--space-2);
|
|
2630
|
+
padding: var(--space-1) 0;
|
|
2631
|
+
border-bottom: 1px solid var(--border-default);
|
|
2632
|
+
}
|
|
2633
|
+
.caps-refusal-row:last-child { border-bottom: 0; }
|
|
2634
|
+
.caps-refusal-row .reason { color: var(--accent-error); }
|
|
2635
|
+
.caps-refusal-row .when { color: var(--text-muted); }
|
|
2636
|
+
|
|
2637
|
+
/* ─── Safety subpage ────────────────────────────────────────── */
|
|
2638
|
+
.checkbox-row {
|
|
2639
|
+
display: flex;
|
|
2640
|
+
flex-direction: column;
|
|
2641
|
+
gap: var(--space-1);
|
|
2642
|
+
}
|
|
2643
|
+
.checkbox-row .hint {
|
|
2644
|
+
font-size: var(--text-xs);
|
|
2645
|
+
color: var(--text-muted);
|
|
2646
|
+
margin-left: 24px;
|
|
2647
|
+
}
|
|
2648
|
+
.strict-actions-list {
|
|
2649
|
+
list-style: none;
|
|
2650
|
+
padding: 0;
|
|
2651
|
+
margin: var(--space-2) 0;
|
|
2652
|
+
}
|
|
2653
|
+
.strict-actions-list li {
|
|
2654
|
+
padding: var(--space-1) var(--space-2);
|
|
2655
|
+
background: var(--bg-base);
|
|
2656
|
+
border-left: 3px solid var(--accent-warning);
|
|
2657
|
+
border-radius: var(--radius-sm);
|
|
2658
|
+
margin-bottom: var(--space-1);
|
|
2659
|
+
font-family: var(--font-mono);
|
|
2660
|
+
font-size: var(--text-xs);
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
/* ─── Updates subpage rebuild ───────────────────────────────── */
|
|
2664
|
+
.restart-banner {
|
|
2665
|
+
display: flex;
|
|
2666
|
+
justify-content: space-between;
|
|
2667
|
+
align-items: center;
|
|
2668
|
+
gap: var(--space-3);
|
|
2669
|
+
padding: var(--space-2) var(--space-3);
|
|
2670
|
+
background: var(--accent-warning-muted);
|
|
2671
|
+
color: var(--accent-warning);
|
|
2672
|
+
border: 1px solid var(--accent-warning);
|
|
2673
|
+
border-radius: var(--radius-sm);
|
|
2674
|
+
margin-bottom: var(--space-3);
|
|
2675
|
+
}
|
|
2676
|
+
.restart-banner-text strong { display: block; }
|
|
2677
|
+
.restart-banner-text span {
|
|
2678
|
+
font-size: var(--text-xs);
|
|
2679
|
+
opacity: 0.8;
|
|
2680
|
+
}
|
|
2681
|
+
.update-actions-block { margin-top: var(--space-2); }
|
|
2682
|
+
.update-status-badge {
|
|
2683
|
+
display: inline-block;
|
|
2684
|
+
padding: var(--space-1) var(--space-2);
|
|
2685
|
+
background: var(--bg-muted);
|
|
2686
|
+
border-radius: var(--radius-sm);
|
|
2687
|
+
font-size: var(--text-xs);
|
|
2688
|
+
margin-bottom: var(--space-2);
|
|
2689
|
+
}
|
|
2690
|
+
.update-status-badge[data-status="update_available"] {
|
|
2691
|
+
background: var(--accent-warning-muted);
|
|
2692
|
+
color: var(--accent-warning);
|
|
2693
|
+
}
|
|
2694
|
+
.update-status-badge[data-status="up_to_date"] {
|
|
2695
|
+
background: var(--accent-success-muted);
|
|
2696
|
+
color: var(--accent-success);
|
|
2697
|
+
}
|
|
2698
|
+
.update-action-row {
|
|
2699
|
+
display: flex;
|
|
2700
|
+
gap: var(--space-2);
|
|
2701
|
+
align-items: center;
|
|
2702
|
+
flex-wrap: wrap;
|
|
2703
|
+
}
|
|
2704
|
+
.checkbox-inline {
|
|
2705
|
+
display: inline-flex;
|
|
2706
|
+
gap: var(--space-1);
|
|
2707
|
+
align-items: center;
|
|
2708
|
+
font-size: var(--text-sm);
|
|
2709
|
+
}
|
|
2710
|
+
.update-result {
|
|
2711
|
+
margin-top: var(--space-2);
|
|
2712
|
+
padding: var(--space-2);
|
|
2713
|
+
font-size: var(--text-xs);
|
|
2714
|
+
font-family: var(--font-mono);
|
|
2715
|
+
background: var(--bg-base);
|
|
2716
|
+
border: 1px solid var(--border-default);
|
|
2717
|
+
border-radius: var(--radius-sm);
|
|
2718
|
+
max-height: 240px;
|
|
2719
|
+
overflow-y: auto;
|
|
2720
|
+
white-space: pre-wrap;
|
|
2721
|
+
}
|
|
2722
|
+
.update-result:empty { display: none; }
|
|
2723
|
+
.update-history-table {
|
|
2724
|
+
font-size: var(--text-xs);
|
|
2725
|
+
border: 1px solid var(--border-default);
|
|
2726
|
+
border-radius: var(--radius-sm);
|
|
2727
|
+
overflow: hidden;
|
|
2728
|
+
}
|
|
2729
|
+
.update-history-row {
|
|
2730
|
+
display: grid;
|
|
2731
|
+
grid-template-columns: 140px 90px 1fr 1fr 100px auto;
|
|
2732
|
+
gap: var(--space-2);
|
|
2733
|
+
padding: var(--space-1) var(--space-2);
|
|
2734
|
+
border-bottom: 1px solid var(--border-default);
|
|
2735
|
+
align-items: center;
|
|
2736
|
+
}
|
|
2737
|
+
.update-history-row:last-child { border-bottom: 0; }
|
|
2738
|
+
.update-history-row.header {
|
|
2739
|
+
background: var(--bg-muted);
|
|
2740
|
+
color: var(--text-muted);
|
|
2741
|
+
text-transform: uppercase;
|
|
2742
|
+
font-size: 10px;
|
|
2743
|
+
letter-spacing: 0.04em;
|
|
2744
|
+
}
|
|
2745
|
+
.update-history-row .kind { font-family: var(--font-mono); }
|
|
2746
|
+
.update-history-row .status-ok { color: var(--accent-success); }
|
|
2747
|
+
.update-history-row .status-fail { color: var(--accent-error); }
|
|
2748
|
+
.update-history-row .integrity-ok { color: var(--accent-success); }
|
|
2749
|
+
.update-history-row .integrity-bad { color: var(--accent-error); }
|
|
2750
|
+
.update-history-row .integrity-unknown { color: var(--text-muted); }
|
|
2751
|
+
|
|
2752
|
+
/* ─── Run scoping bar ──────────────────────────────────────── */
|
|
2753
|
+
.run-scope-bar {
|
|
2754
|
+
display: flex;
|
|
2755
|
+
justify-content: space-between;
|
|
2756
|
+
align-items: center;
|
|
2757
|
+
gap: var(--space-3);
|
|
2758
|
+
padding: var(--space-2);
|
|
2759
|
+
background: var(--lab-panel-elevated);
|
|
2760
|
+
border: 1px solid var(--border-default);
|
|
2761
|
+
border-radius: var(--radius-sm);
|
|
2762
|
+
margin-bottom: var(--space-2);
|
|
2763
|
+
}
|
|
2764
|
+
.run-scope-status {
|
|
2765
|
+
display: flex;
|
|
2766
|
+
gap: var(--space-2);
|
|
2767
|
+
align-items: center;
|
|
2768
|
+
font-size: var(--text-sm);
|
|
2769
|
+
}
|
|
2770
|
+
.run-scope-indicator {
|
|
2771
|
+
font-family: var(--font-mono);
|
|
2772
|
+
padding: 2px var(--space-2);
|
|
2773
|
+
border-radius: var(--radius-sm);
|
|
2774
|
+
background: var(--bg-muted);
|
|
2775
|
+
color: var(--text-muted);
|
|
2776
|
+
}
|
|
2777
|
+
.run-scope-indicator.active {
|
|
2778
|
+
background: var(--accent-success-muted);
|
|
2779
|
+
color: var(--accent-success);
|
|
2780
|
+
}
|
|
2781
|
+
.run-scope-actions {
|
|
2782
|
+
display: flex;
|
|
2783
|
+
gap: var(--space-2);
|
|
2784
|
+
align-items: center;
|
|
2785
|
+
}
|
|
2786
|
+
.run-scope-actions input {
|
|
2787
|
+
padding: var(--space-1) var(--space-2);
|
|
2788
|
+
border: 1px solid var(--border-default);
|
|
2789
|
+
background: var(--bg-base);
|
|
2790
|
+
color: var(--text-primary);
|
|
2791
|
+
border-radius: var(--radius-sm);
|
|
2792
|
+
font-size: var(--text-xs);
|
|
2793
|
+
min-width: 220px;
|
|
2794
|
+
}
|
|
2795
|
+
.history-sidebar-section {
|
|
2796
|
+
margin-top: var(--space-2);
|
|
2797
|
+
padding-top: var(--space-2);
|
|
2798
|
+
border-top: 1px solid var(--border-default);
|
|
2799
|
+
}
|
|
2800
|
+
.history-recent-runs {
|
|
2801
|
+
font-size: var(--text-xs);
|
|
2802
|
+
color: var(--text-muted);
|
|
2803
|
+
max-height: 200px;
|
|
2804
|
+
overflow-y: auto;
|
|
2805
|
+
}
|
|
2806
|
+
.history-recent-run-row {
|
|
2807
|
+
padding: var(--space-1) var(--space-2);
|
|
2808
|
+
margin-bottom: 2px;
|
|
2809
|
+
background: var(--bg-base);
|
|
2810
|
+
border-radius: var(--radius-sm);
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
/* ─── History diff view ────────────────────────────────────── */
|
|
2814
|
+
.history-diff-view {
|
|
2815
|
+
margin-top: var(--space-3);
|
|
2816
|
+
padding: var(--space-3);
|
|
2817
|
+
background: var(--lab-panel-elevated);
|
|
2818
|
+
border: 1px solid var(--border-default);
|
|
2819
|
+
border-radius: var(--radius-sm);
|
|
2820
|
+
}
|
|
2821
|
+
.history-diff-header {
|
|
2822
|
+
display: flex;
|
|
2823
|
+
justify-content: space-between;
|
|
2824
|
+
align-items: center;
|
|
2825
|
+
margin-bottom: var(--space-2);
|
|
2826
|
+
}
|
|
2827
|
+
.history-diff-section {
|
|
2828
|
+
margin-top: var(--space-2);
|
|
2829
|
+
padding: var(--space-2);
|
|
2830
|
+
background: var(--bg-base);
|
|
2831
|
+
border-radius: var(--radius-sm);
|
|
2832
|
+
}
|
|
2833
|
+
.history-diff-section.added { border-left: 3px solid var(--accent-success); }
|
|
2834
|
+
.history-diff-section.removed { border-left: 3px solid var(--accent-error); }
|
|
2835
|
+
.history-diff-section.moved { border-left: 3px solid var(--accent-warning); }
|
|
2836
|
+
.history-diff-section h4 {
|
|
2837
|
+
margin: 0 0 var(--space-1) 0;
|
|
2838
|
+
font-size: var(--text-sm);
|
|
2839
|
+
}
|
|
2840
|
+
.history-diff-section ul {
|
|
2841
|
+
font-family: var(--font-mono);
|
|
2842
|
+
font-size: var(--text-xs);
|
|
2843
|
+
margin: 0;
|
|
2844
|
+
padding-left: var(--space-3);
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
.history-version-card .diff-against {
|
|
2848
|
+
font-size: var(--text-xs);
|
|
2849
|
+
padding: var(--space-1);
|
|
2850
|
+
border: 1px solid var(--border-default);
|
|
2851
|
+
background: var(--bg-base);
|
|
2852
|
+
border-radius: var(--radius-sm);
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
/* ─── Media Pool History (was: Provenance) ─────────────────── */
|
|
2856
|
+
.mpc-toolbar {
|
|
2857
|
+
display: flex;
|
|
2858
|
+
flex-wrap: wrap;
|
|
2859
|
+
align-items: end;
|
|
2860
|
+
gap: var(--space-2);
|
|
2861
|
+
margin-top: var(--space-2);
|
|
2862
|
+
margin-bottom: var(--space-2);
|
|
2863
|
+
}
|
|
2864
|
+
.mpc-toolbar-field {
|
|
2865
|
+
display: flex;
|
|
2866
|
+
flex-direction: column;
|
|
2867
|
+
gap: 2px;
|
|
2868
|
+
font-size: var(--text-xs);
|
|
2869
|
+
color: var(--text-muted);
|
|
2870
|
+
}
|
|
2871
|
+
.mpc-toolbar-field select,
|
|
2872
|
+
.mpc-toolbar-field input {
|
|
2873
|
+
padding: var(--space-1) var(--space-2);
|
|
2874
|
+
border: 1px solid var(--border-default);
|
|
2875
|
+
background: var(--bg-base);
|
|
2876
|
+
color: var(--text-primary);
|
|
2877
|
+
border-radius: var(--radius-sm);
|
|
2878
|
+
font-size: var(--text-xs);
|
|
2879
|
+
min-width: 160px;
|
|
2880
|
+
}
|
|
2881
|
+
.mpc-toolbar-field input { font-family: var(--font-mono); min-width: 100px; }
|
|
2882
|
+
.mpc-toolbar-toggle {
|
|
2883
|
+
display: flex;
|
|
2884
|
+
align-items: center;
|
|
2885
|
+
gap: var(--space-1);
|
|
2886
|
+
font-size: var(--text-xs);
|
|
2887
|
+
color: var(--text-primary);
|
|
2888
|
+
padding-bottom: 4px;
|
|
2889
|
+
}
|
|
2890
|
+
.mpc-meta {
|
|
2891
|
+
flex: 1;
|
|
2892
|
+
text-align: right;
|
|
2893
|
+
font-size: var(--text-xs);
|
|
2894
|
+
color: var(--text-muted);
|
|
2895
|
+
padding-bottom: 4px;
|
|
2896
|
+
}
|
|
2897
|
+
.mpc-table {
|
|
2898
|
+
font-size: var(--text-xs);
|
|
2899
|
+
border: 1px solid var(--border-default);
|
|
2900
|
+
border-radius: var(--radius-sm);
|
|
2901
|
+
overflow: hidden;
|
|
2902
|
+
margin-top: var(--space-2);
|
|
2903
|
+
}
|
|
2904
|
+
.mpc-row {
|
|
2905
|
+
display: grid;
|
|
2906
|
+
grid-template-columns: 150px 170px 1fr 120px 110px;
|
|
2907
|
+
gap: var(--space-2);
|
|
2908
|
+
padding: var(--space-1) var(--space-2);
|
|
2909
|
+
border-bottom: 1px solid var(--border-default);
|
|
2910
|
+
align-items: center;
|
|
2911
|
+
}
|
|
2912
|
+
.mpc-row:last-child { border-bottom: 0; }
|
|
2913
|
+
.mpc-row:hover:not(.header):not(.group) { background: var(--bg-muted); }
|
|
2914
|
+
.mpc-row.header {
|
|
2915
|
+
background: var(--bg-muted);
|
|
2916
|
+
color: var(--text-muted);
|
|
2917
|
+
text-transform: uppercase;
|
|
2918
|
+
font-size: 10px;
|
|
2919
|
+
letter-spacing: 0.04em;
|
|
2920
|
+
}
|
|
2921
|
+
.mpc-row.group {
|
|
2922
|
+
grid-template-columns: 1fr auto;
|
|
2923
|
+
background: var(--bg-muted);
|
|
2924
|
+
font-weight: 600;
|
|
2925
|
+
color: var(--text-primary);
|
|
2926
|
+
font-size: var(--text-xs);
|
|
2927
|
+
}
|
|
2928
|
+
.mpc-row.group .group-count {
|
|
2929
|
+
font-size: 10px;
|
|
2930
|
+
color: var(--text-muted);
|
|
2931
|
+
text-transform: uppercase;
|
|
2932
|
+
letter-spacing: 0.04em;
|
|
2933
|
+
}
|
|
2934
|
+
.mpc-row .action-name {
|
|
2935
|
+
font-family: var(--font-mono);
|
|
2936
|
+
color: var(--accent-primary);
|
|
2937
|
+
}
|
|
2938
|
+
.mpc-row .target-cell {
|
|
2939
|
+
display: flex;
|
|
2940
|
+
flex-direction: column;
|
|
2941
|
+
gap: 2px;
|
|
2942
|
+
min-width: 0;
|
|
2943
|
+
}
|
|
2944
|
+
.mpc-row .target-cell .target-name {
|
|
2945
|
+
overflow: hidden;
|
|
2946
|
+
text-overflow: ellipsis;
|
|
2947
|
+
white-space: nowrap;
|
|
2948
|
+
}
|
|
2949
|
+
.mpc-row .target-cell .target-id {
|
|
2950
|
+
font-family: var(--font-mono);
|
|
2951
|
+
font-size: 10px;
|
|
2952
|
+
color: var(--text-muted);
|
|
2953
|
+
overflow: hidden;
|
|
2954
|
+
text-overflow: ellipsis;
|
|
2955
|
+
white-space: nowrap;
|
|
2956
|
+
}
|
|
2957
|
+
.mpc-row .clip-link {
|
|
2958
|
+
color: var(--accent-primary);
|
|
2959
|
+
cursor: pointer;
|
|
2960
|
+
text-decoration: none;
|
|
2961
|
+
}
|
|
2962
|
+
.mpc-row .clip-link:hover { text-decoration: underline; }
|
|
2963
|
+
.mpc-row .run-id {
|
|
2964
|
+
font-family: var(--font-mono);
|
|
2965
|
+
color: var(--text-muted);
|
|
2966
|
+
font-size: 10px;
|
|
2967
|
+
overflow: hidden;
|
|
2968
|
+
text-overflow: ellipsis;
|
|
2969
|
+
white-space: nowrap;
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2090
2972
|
.review-search-results {
|
|
2091
2973
|
display: flex;
|
|
2092
2974
|
flex-direction: column;
|
|
@@ -2895,6 +3777,7 @@ HTML = r"""<!doctype html>
|
|
|
2895
3777
|
<button class="nav-dropdown-item" data-panel-target="diagnostics" data-subpage-target="mcp" role="menuitem"><span class="nav-dropdown-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7v10l8 5 8-5V7l-8-5z"></path><path d="m4 7 8 5 8-5"></path><path d="M12 12v10"></path></svg></span>MCP</button>
|
|
2896
3778
|
<button class="nav-dropdown-item" data-panel-target="diagnostics" data-subpage-target="storage" role="menuitem"><span class="nav-dropdown-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="5" rx="9" ry="3"></ellipse><path d="M3 5v14c0 1.66 4.03 3 9 3s9-1.34 9-3V5"></path><path d="M3 12c0 1.66 4.03 3 9 3s9-1.34 9-3"></path></svg></span>Storage</button>
|
|
2897
3779
|
<button class="nav-dropdown-item" data-panel-target="diagnostics" data-subpage-target="tools" role="menuitem"><span class="nav-dropdown-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l2.1-2.1a6 6 0 0 1-7.6 7.6l-4 4a2.1 2.1 0 0 1-3-3l4-4a6 6 0 0 1 7.6-7.6z"></path></svg></span>Tools</button>
|
|
3780
|
+
<button class="nav-dropdown-item" data-panel-target="diagnostics" data-subpage-target="media-pool-history" role="menuitem"><span class="nav-dropdown-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12h18"></path><path d="M3 6h18"></path><path d="M3 18h18"></path></svg></span>Media Pool History</button>
|
|
2898
3781
|
</div>
|
|
2899
3782
|
</div>
|
|
2900
3783
|
<div class="control-nav-item">
|
|
@@ -2910,6 +3793,7 @@ HTML = r"""<!doctype html>
|
|
|
2910
3793
|
<button class="control-tab has-menu" data-panel-target="preferences">Preferences <span class="tab-chevron" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"></path></svg></span></button>
|
|
2911
3794
|
<div class="nav-dropdown" role="menu" aria-label="Preference pages">
|
|
2912
3795
|
<button class="nav-dropdown-item" data-panel-target="preferences" data-subpage-target="analysis" role="menuitem"><span class="nav-dropdown-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3v18h18"></path><path d="m19 9-5 5-4-4-3 3"></path></svg></span>Analysis</button>
|
|
3796
|
+
<button class="nav-dropdown-item" data-panel-target="preferences" data-subpage-target="caps" role="menuitem"><span class="nav-dropdown-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"></circle><path d="M12 3v9l6 3"></path></svg></span>Caps + Safety</button>
|
|
2913
3797
|
<button class="nav-dropdown-item" data-panel-target="preferences" data-subpage-target="metadata" role="menuitem"><span class="nav-dropdown-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 7h-9"></path><path d="M14 17H5"></path><circle cx="17" cy="17" r="3"></circle><circle cx="7" cy="7" r="3"></circle></svg></span>Metadata And Markers</button>
|
|
2914
3798
|
<button class="nav-dropdown-item" data-panel-target="preferences" data-subpage-target="paths" role="menuitem"><span class="nav-dropdown-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 7h5l2 3h11v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path></svg></span>Paths And Workflow</button>
|
|
2915
3799
|
<button class="nav-dropdown-item" data-panel-target="preferences" data-subpage-target="updates" role="menuitem"><span class="nav-dropdown-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-2.64-6.36"></path><path d="M21 3v6h-6"></path></svg></span>MCP Updates</button>
|
|
@@ -3110,10 +3994,20 @@ HTML = r"""<!doctype html>
|
|
|
3110
3994
|
</div>
|
|
3111
3995
|
<div class="controls" id="reviewControls">
|
|
3112
3996
|
<button class="secondary" id="reviewRefreshBtn">Refresh</button>
|
|
3997
|
+
<button class="secondary" id="reviewHistoryBtn" title="Timeline edit history (C6)">History</button>
|
|
3113
3998
|
<button class="secondary" id="reviewBackBtn" style="display:none">← Back</button>
|
|
3114
3999
|
</div>
|
|
3115
4000
|
</div>
|
|
3116
4001
|
<div id="reviewBinView">
|
|
4002
|
+
<div id="reviewReadinessCard" class="readiness-card" hidden>
|
|
4003
|
+
<div class="readiness-card-header">
|
|
4004
|
+
<span class="readiness-title">Project readiness</span>
|
|
4005
|
+
<span id="readinessEvidenceBase" class="readiness-evidence"></span>
|
|
4006
|
+
<button id="readinessRefreshBtn" class="secondary" type="button" title="Refresh readiness summary">Refresh</button>
|
|
4007
|
+
</div>
|
|
4008
|
+
<div id="readinessSummaryRow" class="readiness-summary-row"></div>
|
|
4009
|
+
<div id="readinessDetails" class="readiness-details"></div>
|
|
4010
|
+
</div>
|
|
3117
4011
|
<div class="review-bin-filters">
|
|
3118
4012
|
<input id="reviewSearchInput" type="search" placeholder="Search clips, summaries, tags, transcripts…" autocomplete="off">
|
|
3119
4013
|
<select id="reviewBinFilter" aria-label="Filter by bin">
|
|
@@ -3170,6 +4064,51 @@ HTML = r"""<!doctype html>
|
|
|
3170
4064
|
<div class="empty">Loading combined review…</div>
|
|
3171
4065
|
</div>
|
|
3172
4066
|
</div>
|
|
4067
|
+
<div id="reviewHistoryView" style="display:none">
|
|
4068
|
+
<div id="runScopeBar" class="run-scope-bar">
|
|
4069
|
+
<div class="run-scope-status">
|
|
4070
|
+
<span class="run-scope-label">Active run:</span>
|
|
4071
|
+
<span id="runScopeIndicator" class="run-scope-indicator none">none</span>
|
|
4072
|
+
</div>
|
|
4073
|
+
<div class="run-scope-actions">
|
|
4074
|
+
<input id="runScopeLabel" type="text" placeholder="run label (e.g. 'rough cut tighten')">
|
|
4075
|
+
<button id="runBeginBtn" type="button">Begin run</button>
|
|
4076
|
+
<button id="runEndBtn" class="secondary" type="button">End run</button>
|
|
4077
|
+
</div>
|
|
4078
|
+
</div>
|
|
4079
|
+
<div class="history-layout">
|
|
4080
|
+
<aside class="history-sidebar">
|
|
4081
|
+
<div class="history-sidebar-header">
|
|
4082
|
+
<strong>Timelines</strong>
|
|
4083
|
+
<button id="historyRefreshBtn" class="secondary" type="button">Refresh</button>
|
|
4084
|
+
</div>
|
|
4085
|
+
<div id="historyTimelineList" class="history-timeline-list">
|
|
4086
|
+
<div class="empty">Loading…</div>
|
|
4087
|
+
</div>
|
|
4088
|
+
<div class="history-archive-now">
|
|
4089
|
+
<button id="historyArchiveCurrentBtn" type="button">Archive current timeline</button>
|
|
4090
|
+
<input id="historyArchiveReason" type="text" placeholder="reason (optional)">
|
|
4091
|
+
</div>
|
|
4092
|
+
<div class="history-sidebar-section">
|
|
4093
|
+
<strong>Recent runs</strong>
|
|
4094
|
+
<div id="historyRecentRuns" class="history-recent-runs">loading…</div>
|
|
4095
|
+
</div>
|
|
4096
|
+
</aside>
|
|
4097
|
+
<section class="history-detail">
|
|
4098
|
+
<div id="historyDetailHeader" class="history-detail-header">
|
|
4099
|
+
<span class="empty">Select a timeline.</span>
|
|
4100
|
+
</div>
|
|
4101
|
+
<div id="historyDetailBody" class="history-detail-body"></div>
|
|
4102
|
+
<div id="historyDiffView" class="history-diff-view" hidden>
|
|
4103
|
+
<div class="history-diff-header">
|
|
4104
|
+
<strong>Structural diff</strong>
|
|
4105
|
+
<button id="historyDiffCloseBtn" class="secondary" type="button">Close</button>
|
|
4106
|
+
</div>
|
|
4107
|
+
<div id="historyDiffBody"></div>
|
|
4108
|
+
</div>
|
|
4109
|
+
</section>
|
|
4110
|
+
</div>
|
|
4111
|
+
</div>
|
|
3173
4112
|
</section>
|
|
3174
4113
|
</main>
|
|
3175
4114
|
|
|
@@ -3217,6 +4156,28 @@ HTML = r"""<!doctype html>
|
|
|
3217
4156
|
<div class="empty">Tool diagnostics pending.</div>
|
|
3218
4157
|
</div>
|
|
3219
4158
|
</section>
|
|
4159
|
+
|
|
4160
|
+
<section class="span-12 subpage" data-subpage-scope="diagnostics" data-subpage="media-pool-history">
|
|
4161
|
+
<h2>Media Pool History</h2>
|
|
4162
|
+
<p class="section-copy">Provenance log for destructive media-pool operations — deletes, replaces, relinks. Each row captures the action, the target clip or folder, the analysis run that triggered it, and the initiator. Kept separate from timeline <code>brain_edits</code> because the addressable entity is a media pool item, not a timeline.</p>
|
|
4163
|
+
<div class="mpc-toolbar">
|
|
4164
|
+
<label class="mpc-toolbar-field">Action
|
|
4165
|
+
<select id="mpcActionFilter">
|
|
4166
|
+
<option value="">all actions</option>
|
|
4167
|
+
</select>
|
|
4168
|
+
</label>
|
|
4169
|
+
<label class="mpc-toolbar-field">Limit
|
|
4170
|
+
<input id="mpcLimit" type="number" min="10" max="500" value="50">
|
|
4171
|
+
</label>
|
|
4172
|
+
<label class="mpc-toolbar-toggle">
|
|
4173
|
+
<input id="mpcGroupByClip" type="checkbox">
|
|
4174
|
+
<span>Group by target</span>
|
|
4175
|
+
</label>
|
|
4176
|
+
<button id="mpcRefreshBtn" class="secondary" type="button">Refresh</button>
|
|
4177
|
+
<span id="mpcMeta" class="mpc-meta"></span>
|
|
4178
|
+
</div>
|
|
4179
|
+
<div id="mpcTable" class="mpc-table">loading…</div>
|
|
4180
|
+
</section>
|
|
3220
4181
|
</main>
|
|
3221
4182
|
|
|
3222
4183
|
<main id="panel-docs" class="panel control-grid">
|
|
@@ -3336,6 +4297,117 @@ HTML = r"""<!doctype html>
|
|
|
3336
4297
|
</div>
|
|
3337
4298
|
</section>
|
|
3338
4299
|
|
|
4300
|
+
<section class="span-12 subpage" data-subpage-scope="preferences" data-subpage="caps">
|
|
4301
|
+
<div class="settings-subhead">Caps + Safety</div>
|
|
4302
|
+
<p class="settings-subtitle">Token + frame budgets for analysis, plus safety rails for destructive edits. Usage is tracked per project in <code>_soul/timeline_brain.sqlite</code>.</p>
|
|
4303
|
+
|
|
4304
|
+
<div class="caps-section">
|
|
4305
|
+
<div class="caps-section-head">
|
|
4306
|
+
<div class="caps-section-title">Budget preset</div>
|
|
4307
|
+
<div class="caps-section-hint">Pick the preset that matches the job. Override individual fields below if needed.</div>
|
|
4308
|
+
</div>
|
|
4309
|
+
<div id="capsPresetCards" class="caps-preset-cards" role="radiogroup" aria-label="Analysis caps preset">
|
|
4310
|
+
<!-- cards rendered by JS -->
|
|
4311
|
+
</div>
|
|
4312
|
+
<input id="prefCapsPreset" type="hidden" value="standard">
|
|
4313
|
+
</div>
|
|
4314
|
+
|
|
4315
|
+
<div class="caps-section">
|
|
4316
|
+
<div class="caps-section-head">
|
|
4317
|
+
<div class="caps-section-title">Vision token usage</div>
|
|
4318
|
+
<div class="caps-section-hint">Live consumption against the active caps. Green = comfortable, amber = approaching, red = at or over budget.</div>
|
|
4319
|
+
</div>
|
|
4320
|
+
<div id="capsUsageBlock" class="caps-usage-block">
|
|
4321
|
+
<div id="capsUsageGauges" class="caps-usage-gauges">
|
|
4322
|
+
<div class="caps-gauge" data-scope="clip">
|
|
4323
|
+
<div class="caps-gauge-row">
|
|
4324
|
+
<span class="caps-gauge-label">Per‑clip</span>
|
|
4325
|
+
<span class="caps-gauge-numbers">—</span>
|
|
4326
|
+
</div>
|
|
4327
|
+
<div class="caps-gauge-bar"><span class="caps-gauge-fill"></span></div>
|
|
4328
|
+
</div>
|
|
4329
|
+
<div class="caps-gauge" data-scope="job">
|
|
4330
|
+
<div class="caps-gauge-row">
|
|
4331
|
+
<span class="caps-gauge-label">Per‑job</span>
|
|
4332
|
+
<span class="caps-gauge-numbers">—</span>
|
|
4333
|
+
</div>
|
|
4334
|
+
<div class="caps-gauge-bar"><span class="caps-gauge-fill"></span></div>
|
|
4335
|
+
</div>
|
|
4336
|
+
<div class="caps-gauge" data-scope="day">
|
|
4337
|
+
<div class="caps-gauge-row">
|
|
4338
|
+
<span class="caps-gauge-label">Today</span>
|
|
4339
|
+
<span class="caps-gauge-numbers">—</span>
|
|
4340
|
+
</div>
|
|
4341
|
+
<div class="caps-gauge-bar"><span class="caps-gauge-fill"></span></div>
|
|
4342
|
+
</div>
|
|
4343
|
+
</div>
|
|
4344
|
+
<div class="caps-history-block">
|
|
4345
|
+
<div class="caps-history-title">Daily vision-token usage (last 30 days)</div>
|
|
4346
|
+
<div id="capsHistoryChart" class="caps-history-chart">loading…</div>
|
|
4347
|
+
</div>
|
|
4348
|
+
</div>
|
|
4349
|
+
</div>
|
|
4350
|
+
|
|
4351
|
+
<div class="caps-section">
|
|
4352
|
+
<div class="caps-section-head">
|
|
4353
|
+
<div class="caps-section-title">Safety</div>
|
|
4354
|
+
<div class="caps-section-hint">Auto-archive timelines before destructive edits and refuse high-blast-radius ops when archive fails.</div>
|
|
4355
|
+
</div>
|
|
4356
|
+
<div class="settings-grid">
|
|
4357
|
+
<label class="checkbox-row">
|
|
4358
|
+
<input id="prefAutoSaveAfterArchive" type="checkbox">
|
|
4359
|
+
<span>Auto-save Resolve project after each archive</span>
|
|
4360
|
+
<small class="hint">When on, <code>project.SaveProject()</code> fires after every version-on-mutate archive. Protects against Resolve crashes losing history.</small>
|
|
4361
|
+
</label>
|
|
4362
|
+
</div>
|
|
4363
|
+
<div class="safety-strict-block">
|
|
4364
|
+
<div class="safety-strict-title">Strict mode — always on for:</div>
|
|
4365
|
+
<ul class="strict-actions-list">
|
|
4366
|
+
<li><code>timeline.delete_timelines</code></li>
|
|
4367
|
+
<li><code>timeline.delete_track</code></li>
|
|
4368
|
+
<li><code>timeline.delete_clips</code> with <code>ripple=true</code></li>
|
|
4369
|
+
</ul>
|
|
4370
|
+
<p class="safety-strict-note">Strict mode REFUSES the underlying call if the pre-mutation archive can't be created. Other destructive ops degrade silently — pass <code>strict=true</code> in any action's params to opt in to refusal for that call.</p>
|
|
4371
|
+
</div>
|
|
4372
|
+
</div>
|
|
4373
|
+
|
|
4374
|
+
<details class="caps-section caps-advanced">
|
|
4375
|
+
<summary>
|
|
4376
|
+
<span class="caps-section-title">Advanced — per-field overrides</span>
|
|
4377
|
+
<span class="caps-section-hint">Leave blank to use the preset value shown in the placeholder. Type a number or <code>unlimited</code> to override.</span>
|
|
4378
|
+
</summary>
|
|
4379
|
+
<div class="settings-grid caps-override-grid">
|
|
4380
|
+
<label>Response chars <input id="capsOvResponseChars" type="text" placeholder="(preset)" data-cap-key="response_chars"></label>
|
|
4381
|
+
<label>Vision tokens / clip <input id="capsOvVisionClip" type="text" placeholder="(preset)" data-cap-key="vision_tokens_per_clip"></label>
|
|
4382
|
+
<label>Frames / clip <input id="capsOvFramesClip" type="text" placeholder="(preset)" data-cap-key="frames_per_clip"></label>
|
|
4383
|
+
<label>Vision tokens / job <input id="capsOvVisionJob" type="text" placeholder="(preset)" data-cap-key="vision_tokens_per_job"></label>
|
|
4384
|
+
<label>Vision tokens / day <input id="capsOvVisionDay" type="text" placeholder="(preset)" data-cap-key="vision_tokens_per_day"></label>
|
|
4385
|
+
<label>Wall clock seconds / call <input id="capsOvWallClock" type="text" placeholder="(preset)" data-cap-key="wall_clock_seconds_per_call"></label>
|
|
4386
|
+
<label>Max frame dimension (px) <input id="capsOvFrameDim" type="text" placeholder="(preset)" data-cap-key="max_frame_dim_pixels"></label>
|
|
4387
|
+
</div>
|
|
4388
|
+
</details>
|
|
4389
|
+
|
|
4390
|
+
<details class="caps-section caps-inspector-block">
|
|
4391
|
+
<summary>
|
|
4392
|
+
<span class="caps-section-title">Inspector & debug</span>
|
|
4393
|
+
<span class="caps-section-hint">Look up usage for a specific clip or batch, browse refusals, or reset today's day-scope rollup.</span>
|
|
4394
|
+
</summary>
|
|
4395
|
+
<div class="caps-inspector">
|
|
4396
|
+
<div class="caps-inspector-row">
|
|
4397
|
+
<label>Clip id <input id="capsInspectClipId" type="text" placeholder="abc123"></label>
|
|
4398
|
+
<label>or Job id <input id="capsInspectJobId" type="text" placeholder="batch_xyz"></label>
|
|
4399
|
+
<button id="capsInspectBtn" type="button">Look up</button>
|
|
4400
|
+
<button id="capsResetDayBtn" class="secondary" type="button" title="Delete today's day-scope usage rows (admin)">Reset today's usage</button>
|
|
4401
|
+
</div>
|
|
4402
|
+
<div id="capsInspectResult" class="caps-inspect-result">enter a clip_id or job_id and press Look up</div>
|
|
4403
|
+
</div>
|
|
4404
|
+
<div class="caps-refusals">
|
|
4405
|
+
<div class="caps-section-subtitle">Recent caps refusals</div>
|
|
4406
|
+
<div id="capsRefusalsList" class="caps-refusals-list">loading…</div>
|
|
4407
|
+
</div>
|
|
4408
|
+
</details>
|
|
4409
|
+
</section>
|
|
4410
|
+
|
|
3339
4411
|
<section class="span-12 subpage" data-subpage-scope="preferences" data-subpage="metadata">
|
|
3340
4412
|
<div class="settings-subhead">Metadata And Markers</div>
|
|
3341
4413
|
<p class="settings-subtitle">Controls for Resolve metadata writes and source-time marker suggestions. Source media remains read-only.</p>
|
|
@@ -3430,6 +4502,15 @@ HTML = r"""<!doctype html>
|
|
|
3430
4502
|
</section>
|
|
3431
4503
|
|
|
3432
4504
|
<section class="span-12 subpage" data-subpage-scope="preferences" data-subpage="updates">
|
|
4505
|
+
<div id="restartNeededBanner" class="restart-banner" hidden>
|
|
4506
|
+
<div class="restart-banner-text">
|
|
4507
|
+
<strong>MCP server restart needed</strong>
|
|
4508
|
+
<span id="restartBannerDetail"></span>
|
|
4509
|
+
</div>
|
|
4510
|
+
<div class="restart-banner-actions">
|
|
4511
|
+
<button id="restartBannerAck" class="secondary" type="button">Got it</button>
|
|
4512
|
+
</div>
|
|
4513
|
+
</div>
|
|
3433
4514
|
<div class="settings-subhead">MCP Updates</div>
|
|
3434
4515
|
<p class="settings-subtitle">Server-wide update-check behavior for the MCP package. Checks are best-effort and should not block startup.</p>
|
|
3435
4516
|
<div class="settings-grid">
|
|
@@ -3441,9 +4522,38 @@ HTML = r"""<!doctype html>
|
|
|
3441
4522
|
<option value="never">never</option>
|
|
3442
4523
|
</select>
|
|
3443
4524
|
</label>
|
|
4525
|
+
<label>Channel
|
|
4526
|
+
<select id="prefUpdateChannel">
|
|
4527
|
+
<option value="stable">stable</option>
|
|
4528
|
+
<option value="beta">beta</option>
|
|
4529
|
+
<option value="dev">dev</option>
|
|
4530
|
+
</select>
|
|
4531
|
+
</label>
|
|
3444
4532
|
<label>Check interval hours <input id="prefUpdateIntervalHours" type="number" min="0.1" step="0.1" value="24"></label>
|
|
3445
4533
|
<label>Snooze hours <input id="prefUpdateSnoozeHours" type="number" min="0.1" step="0.1" value="24"></label>
|
|
3446
4534
|
</div>
|
|
4535
|
+
|
|
4536
|
+
<div class="update-actions-block">
|
|
4537
|
+
<div class="settings-subhead" style="margin-top:24px">Apply update</div>
|
|
4538
|
+
<div id="updateStatusBadge" class="update-status-badge">Loading status…</div>
|
|
4539
|
+
<div class="update-action-row">
|
|
4540
|
+
<button id="updatePreviewBtn" class="secondary" type="button">Preview release notes</button>
|
|
4541
|
+
<label class="checkbox-inline">
|
|
4542
|
+
<input id="updateStashCheckbox" type="checkbox">
|
|
4543
|
+
<span>Stash local changes if dirty</span>
|
|
4544
|
+
</label>
|
|
4545
|
+
<label class="checkbox-inline">
|
|
4546
|
+
<input id="updateForceJobsCheckbox" type="checkbox">
|
|
4547
|
+
<span>Override active-job lock</span>
|
|
4548
|
+
</label>
|
|
4549
|
+
<button id="updateApplyBtn" type="button">Apply update</button>
|
|
4550
|
+
<button id="updateRollbackBtn" class="secondary" type="button">Rollback last update</button>
|
|
4551
|
+
</div>
|
|
4552
|
+
<div id="updateActionResult" class="update-result"></div>
|
|
4553
|
+
</div>
|
|
4554
|
+
|
|
4555
|
+
<div class="settings-subhead" style="margin-top:24px">Update history</div>
|
|
4556
|
+
<div id="updateHistoryTable" class="update-history-table">loading…</div>
|
|
3447
4557
|
</section>
|
|
3448
4558
|
|
|
3449
4559
|
</main>
|
|
@@ -3589,10 +4699,12 @@ HTML = r"""<!doctype html>
|
|
|
3589
4699
|
mcp: 'MCP',
|
|
3590
4700
|
storage: 'Storage',
|
|
3591
4701
|
tools: 'Tools',
|
|
4702
|
+
'media-pool-history': 'Media Pool History',
|
|
3592
4703
|
},
|
|
3593
4704
|
docs: DOC_LABELS,
|
|
3594
4705
|
preferences: {
|
|
3595
4706
|
analysis: 'Analysis',
|
|
4707
|
+
caps: 'Caps + Safety',
|
|
3596
4708
|
metadata: 'Metadata And Markers',
|
|
3597
4709
|
paths: 'Paths And Workflow',
|
|
3598
4710
|
updates: 'MCP Updates',
|
|
@@ -5422,46 +6534,816 @@ HTML = r"""<!doctype html>
|
|
|
5422
6534
|
el.textContent = `${prefix} · last ${last} · ${visible}${extra ? ` · ${extra}` : ''}`;
|
|
5423
6535
|
}
|
|
5424
6536
|
|
|
5425
|
-
function rerenderResolveMedia() {
|
|
5426
|
-
if (state.resolveMedia) {
|
|
5427
|
-
renderResolveMedia(state.resolveMedia);
|
|
6537
|
+
function rerenderResolveMedia() {
|
|
6538
|
+
if (state.resolveMedia) {
|
|
6539
|
+
renderResolveMedia(state.resolveMedia);
|
|
6540
|
+
} else {
|
|
6541
|
+
updateMediaPollStatus();
|
|
6542
|
+
}
|
|
6543
|
+
}
|
|
6544
|
+
|
|
6545
|
+
async function refreshIndex() {
|
|
6546
|
+
const status = await api('/api/index/status');
|
|
6547
|
+
state.indexStatus = status;
|
|
6548
|
+
renderControlPanels();
|
|
6549
|
+
}
|
|
6550
|
+
|
|
6551
|
+
async function buildIndex() {
|
|
6552
|
+
const built = await api('/api/index/build', { method: 'POST' });
|
|
6553
|
+
state.indexStatus = { ...built, exists: true };
|
|
6554
|
+
renderControlPanels();
|
|
6555
|
+
}
|
|
6556
|
+
|
|
6557
|
+
async function refreshAll() {
|
|
6558
|
+
await refreshIndex();
|
|
6559
|
+
await refreshResolveMedia();
|
|
6560
|
+
}
|
|
6561
|
+
|
|
6562
|
+
// ─── V2 Review surface ──────────────────────────────────────────────
|
|
6563
|
+
function formatDuration(seconds) {
|
|
6564
|
+
if (seconds == null || !isFinite(seconds)) return '—';
|
|
6565
|
+
const s = Math.max(0, Math.round(Number(seconds)));
|
|
6566
|
+
const m = Math.floor(s / 60);
|
|
6567
|
+
const r = s % 60;
|
|
6568
|
+
return `${m}:${String(r).padStart(2, '0')}`;
|
|
6569
|
+
}
|
|
6570
|
+
|
|
6571
|
+
function selectChipClass(selectPotential) {
|
|
6572
|
+
const v = String(selectPotential || '').toLowerCase();
|
|
6573
|
+
if (v === 'high') return 'select-high';
|
|
6574
|
+
if (v === 'low') return 'select-low';
|
|
6575
|
+
if (v === 'medium' || v === 'med') return 'select-medium';
|
|
6576
|
+
return '';
|
|
6577
|
+
}
|
|
6578
|
+
|
|
6579
|
+
// ─── C6 timeline-history view ────────────────────────────────────────
|
|
6580
|
+
state.history = state.history || { timelines: [], selectedTimeline: null, payload: null };
|
|
6581
|
+
|
|
6582
|
+
function escapeHtml(s) {
|
|
6583
|
+
return String(s == null ? '' : s)
|
|
6584
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
6585
|
+
.replace(/"/g, '"').replace(/'/g, ''');
|
|
6586
|
+
}
|
|
6587
|
+
|
|
6588
|
+
function formatDelta(edit) {
|
|
6589
|
+
const d = edit && edit.delta;
|
|
6590
|
+
if (d === null || d === undefined) return '<span class="delta-zero">—</span>';
|
|
6591
|
+
const cls = d > 0 ? 'delta-pos' : (d < 0 ? 'delta-neg' : 'delta-zero');
|
|
6592
|
+
const sign = d > 0 ? '+' : '';
|
|
6593
|
+
return `<span class="${cls}">${sign}${d}</span>`;
|
|
6594
|
+
}
|
|
6595
|
+
|
|
6596
|
+
function formatVersionDate(iso) {
|
|
6597
|
+
if (!iso) return '';
|
|
6598
|
+
try { return new Date(iso).toLocaleString(); } catch (e) { return iso; }
|
|
6599
|
+
}
|
|
6600
|
+
|
|
6601
|
+
async function refreshHistoryTimelines() {
|
|
6602
|
+
const list = $('historyTimelineList');
|
|
6603
|
+
if (list) list.innerHTML = '<div class="empty">Loading…</div>';
|
|
6604
|
+
const data = await api('/api/timeline_versions').catch(err => ({ success: false, error: String(err) }));
|
|
6605
|
+
if (!data || !data.success) {
|
|
6606
|
+
if (list) list.innerHTML = `<div class="empty">Error: ${escapeHtml(data?.error || 'unknown')}</div>`;
|
|
6607
|
+
return;
|
|
6608
|
+
}
|
|
6609
|
+
state.history.timelines = data.timelines || [];
|
|
6610
|
+
renderHistoryTimelineList();
|
|
6611
|
+
// Auto-select first timeline if nothing's selected yet.
|
|
6612
|
+
if (!state.history.selectedTimeline && state.history.timelines.length > 0) {
|
|
6613
|
+
await selectHistoryTimeline(state.history.timelines[0].timeline_name);
|
|
6614
|
+
} else if (state.history.selectedTimeline) {
|
|
6615
|
+
await loadHistoryDetail(state.history.selectedTimeline);
|
|
6616
|
+
} else {
|
|
6617
|
+
renderHistoryDetailEmpty();
|
|
6618
|
+
}
|
|
6619
|
+
}
|
|
6620
|
+
|
|
6621
|
+
function renderHistoryTimelineList() {
|
|
6622
|
+
const list = $('historyTimelineList');
|
|
6623
|
+
if (!list) return;
|
|
6624
|
+
if (!state.history.timelines.length) {
|
|
6625
|
+
list.innerHTML = '<div class="empty">No archived timelines yet. Make a destructive edit (or click Archive current).</div>';
|
|
6626
|
+
return;
|
|
6627
|
+
}
|
|
6628
|
+
list.innerHTML = state.history.timelines.map(tl => {
|
|
6629
|
+
const selected = tl.timeline_name === state.history.selectedTimeline ? ' is-selected' : '';
|
|
6630
|
+
return `
|
|
6631
|
+
<div class="history-timeline-row${selected}" data-tl-name="${escapeHtml(tl.timeline_name)}">
|
|
6632
|
+
<span class="name" title="${escapeHtml(tl.timeline_name)}">${escapeHtml(tl.timeline_name)}</span>
|
|
6633
|
+
<span class="count">v${tl.latest_version} · ${tl.version_count}×</span>
|
|
6634
|
+
</div>`;
|
|
6635
|
+
}).join('');
|
|
6636
|
+
list.querySelectorAll('.history-timeline-row').forEach(row => {
|
|
6637
|
+
row.addEventListener('click', () => {
|
|
6638
|
+
selectHistoryTimeline(row.dataset.tlName).catch(alertError);
|
|
6639
|
+
});
|
|
6640
|
+
});
|
|
6641
|
+
}
|
|
6642
|
+
|
|
6643
|
+
async function selectHistoryTimeline(timelineName) {
|
|
6644
|
+
state.history.selectedTimeline = timelineName;
|
|
6645
|
+
renderHistoryTimelineList();
|
|
6646
|
+
await loadHistoryDetail(timelineName);
|
|
6647
|
+
}
|
|
6648
|
+
|
|
6649
|
+
async function loadHistoryDetail(timelineName) {
|
|
6650
|
+
const body = $('historyDetailBody');
|
|
6651
|
+
const header = $('historyDetailHeader');
|
|
6652
|
+
if (body) body.innerHTML = '<div class="empty">Loading…</div>';
|
|
6653
|
+
if (header) header.innerHTML = `<span class="timeline-name">${escapeHtml(timelineName)}</span>`;
|
|
6654
|
+
const data = await api(`/api/timeline_versions/${encodeURIComponent(timelineName)}`)
|
|
6655
|
+
.catch(err => ({ success: false, error: String(err) }));
|
|
6656
|
+
if (!data || !data.success) {
|
|
6657
|
+
if (body) body.innerHTML = `<div class="empty">Error: ${escapeHtml(data?.error || 'unknown')}</div>`;
|
|
6658
|
+
return;
|
|
6659
|
+
}
|
|
6660
|
+
state.history.payload = data;
|
|
6661
|
+
renderHistoryDetail(data);
|
|
6662
|
+
}
|
|
6663
|
+
|
|
6664
|
+
function renderHistoryDetailEmpty() {
|
|
6665
|
+
const body = $('historyDetailBody');
|
|
6666
|
+
const header = $('historyDetailHeader');
|
|
6667
|
+
if (header) header.innerHTML = '<span class="empty">Select a timeline.</span>';
|
|
6668
|
+
if (body) body.innerHTML = '';
|
|
6669
|
+
}
|
|
6670
|
+
|
|
6671
|
+
function renderHistoryDetail(data) {
|
|
6672
|
+
const body = $('historyDetailBody');
|
|
6673
|
+
if (!body) return;
|
|
6674
|
+
const versions = (data.versions || []).slice().reverse(); // newest first
|
|
6675
|
+
const edits = data.edits || [];
|
|
6676
|
+
|
|
6677
|
+
// Bucket edits by archived_timeline_name (timeline_after for the version
|
|
6678
|
+
// they produced). Edits with no matching archive are shown in an "Unbucketed
|
|
6679
|
+
// edits" section at the bottom.
|
|
6680
|
+
const editsByArchive = {};
|
|
6681
|
+
const unbucketed = [];
|
|
6682
|
+
edits.forEach(edit => {
|
|
6683
|
+
const key = edit.timeline_after || edit.timeline_before;
|
|
6684
|
+
if (key && versions.some(v => v.archived_timeline_name === key)) {
|
|
6685
|
+
(editsByArchive[key] = editsByArchive[key] || []).push(edit);
|
|
6686
|
+
} else {
|
|
6687
|
+
unbucketed.push(edit);
|
|
6688
|
+
}
|
|
6689
|
+
});
|
|
6690
|
+
|
|
6691
|
+
const sections = [];
|
|
6692
|
+
if (!versions.length) {
|
|
6693
|
+
sections.push('<div class="empty">No archived versions yet.</div>');
|
|
6694
|
+
}
|
|
6695
|
+
versions.forEach(v => {
|
|
6696
|
+
const versionEdits = editsByArchive[v.archived_timeline_name] || [];
|
|
6697
|
+
const collapsed = v.drt_export_path
|
|
6698
|
+
? `<span class="drt-collapsed">retention-collapsed → ${escapeHtml(v.drt_export_path)}</span>`
|
|
6699
|
+
: '';
|
|
6700
|
+
const editsTable = versionEdits.length ? `
|
|
6701
|
+
<table class="history-edits-table">
|
|
6702
|
+
<thead>
|
|
6703
|
+
<tr><th>Edit</th><th>Metric</th><th>Before</th><th>After</th><th>Δ</th><th>Rationale</th></tr>
|
|
6704
|
+
</thead>
|
|
6705
|
+
<tbody>
|
|
6706
|
+
${versionEdits.map(e => `
|
|
6707
|
+
<tr>
|
|
6708
|
+
<td class="edit-type">${escapeHtml(e.edit_type || '')}</td>
|
|
6709
|
+
<td class="metric">${escapeHtml(e.target_metric || '—')}</td>
|
|
6710
|
+
<td>${e.before_value ?? '—'}</td>
|
|
6711
|
+
<td>${e.after_value ?? '—'}</td>
|
|
6712
|
+
<td>${formatDelta(e)}</td>
|
|
6713
|
+
<td>${escapeHtml(e.rationale || '')}</td>
|
|
6714
|
+
</tr>`).join('')}
|
|
6715
|
+
</tbody>
|
|
6716
|
+
</table>` : '<div class="empty">No declared brain edits recorded.</div>';
|
|
6717
|
+
const thumbUrl = v.thumbnail_path
|
|
6718
|
+
? `/api/timeline_thumbnail/${encodeURIComponent(v.thumbnail_path.split('/_soul/timeline_versions/').pop() || '')}`
|
|
6719
|
+
: null;
|
|
6720
|
+
const thumb = thumbUrl
|
|
6721
|
+
? `<div class="thumb"><img src="${thumbUrl}" alt="v${v.version} thumbnail" loading="lazy"></div>`
|
|
6722
|
+
: `<div class="thumb">no thumbnail</div>`;
|
|
6723
|
+
const diffOptions = versions
|
|
6724
|
+
.filter(other => other.version !== v.version)
|
|
6725
|
+
.map(other => `<option value="${other.version}">v${other.version}</option>`).join('');
|
|
6726
|
+
sections.push(`
|
|
6727
|
+
<div class="history-version-card">
|
|
6728
|
+
${thumb}
|
|
6729
|
+
<div class="body">
|
|
6730
|
+
<div class="version-row">
|
|
6731
|
+
<span class="version-label">v${v.version}</span>
|
|
6732
|
+
<select class="diff-against" data-diff-against-base="${v.version}" data-diff-timeline="${escapeHtml(data.timeline_name)}">
|
|
6733
|
+
<option value="">Diff against…</option>
|
|
6734
|
+
${diffOptions}
|
|
6735
|
+
</select>
|
|
6736
|
+
<button class="history-rollback-btn" data-rollback-version="${v.version}" data-rollback-timeline="${escapeHtml(data.timeline_name)}">Rollback to v${v.version}</button>
|
|
6737
|
+
</div>
|
|
6738
|
+
<div class="archived-name">${escapeHtml(v.archived_timeline_name)} ${collapsed}</div>
|
|
6739
|
+
<div class="timestamp">${escapeHtml(formatVersionDate(v.created_at))} · run=${escapeHtml(v.analysis_run_id || '—')}</div>
|
|
6740
|
+
${v.reason ? `<div class="reason">${escapeHtml(v.reason)}</div>` : ''}
|
|
6741
|
+
${editsTable}
|
|
6742
|
+
</div>
|
|
6743
|
+
</div>`);
|
|
6744
|
+
});
|
|
6745
|
+
if (unbucketed.length) {
|
|
6746
|
+
sections.push(`
|
|
6747
|
+
<div class="history-version-card">
|
|
6748
|
+
<div class="version-label">Recent edits (no matching archive)</div>
|
|
6749
|
+
<table class="history-edits-table">
|
|
6750
|
+
<thead>
|
|
6751
|
+
<tr><th>Edit</th><th>Metric</th><th>Before</th><th>After</th><th>Δ</th><th>Created</th></tr>
|
|
6752
|
+
</thead>
|
|
6753
|
+
<tbody>
|
|
6754
|
+
${unbucketed.map(e => `
|
|
6755
|
+
<tr>
|
|
6756
|
+
<td class="edit-type">${escapeHtml(e.edit_type || '')}</td>
|
|
6757
|
+
<td class="metric">${escapeHtml(e.target_metric || '—')}</td>
|
|
6758
|
+
<td>${e.before_value ?? '—'}</td>
|
|
6759
|
+
<td>${e.after_value ?? '—'}</td>
|
|
6760
|
+
<td>${formatDelta(e)}</td>
|
|
6761
|
+
<td>${escapeHtml(formatVersionDate(e.created_at))}</td>
|
|
6762
|
+
</tr>`).join('')}
|
|
6763
|
+
</tbody>
|
|
6764
|
+
</table>
|
|
6765
|
+
</div>`);
|
|
6766
|
+
}
|
|
6767
|
+
body.innerHTML = sections.join('');
|
|
6768
|
+
body.querySelectorAll('.history-rollback-btn').forEach(btn => {
|
|
6769
|
+
btn.addEventListener('click', () => {
|
|
6770
|
+
const version = Number(btn.dataset.rollbackVersion);
|
|
6771
|
+
const timelineName = btn.dataset.rollbackTimeline;
|
|
6772
|
+
if (!confirm(`Rollback "${timelineName}" to v${version}? This archives the current state first, then duplicates the archived version back into the project.`)) return;
|
|
6773
|
+
rollbackHistoryVersion(timelineName, version).catch(alertError);
|
|
6774
|
+
});
|
|
6775
|
+
});
|
|
6776
|
+
body.querySelectorAll('select.diff-against').forEach(sel => {
|
|
6777
|
+
sel.addEventListener('change', () => {
|
|
6778
|
+
const against = Number(sel.value);
|
|
6779
|
+
if (!against) return;
|
|
6780
|
+
const base = Number(sel.dataset.diffAgainstBase);
|
|
6781
|
+
const timeline = sel.dataset.diffTimeline;
|
|
6782
|
+
const from = Math.min(base, against);
|
|
6783
|
+
const to = Math.max(base, against);
|
|
6784
|
+
showDiffBetween(timeline, from, to).catch(alertError);
|
|
6785
|
+
sel.value = '';
|
|
6786
|
+
});
|
|
6787
|
+
});
|
|
6788
|
+
}
|
|
6789
|
+
|
|
6790
|
+
async function rollbackHistoryVersion(timelineName, version) {
|
|
6791
|
+
const data = await api('/api/timeline_versions/action', {
|
|
6792
|
+
method: 'POST',
|
|
6793
|
+
body: JSON.stringify({ action: 'rollback', timeline_name: timelineName, version }),
|
|
6794
|
+
}).catch(err => ({ success: false, error: String(err) }));
|
|
6795
|
+
if (!data || !data.success) {
|
|
6796
|
+
alert(`Rollback failed: ${data?.error || 'unknown'}`);
|
|
6797
|
+
return;
|
|
6798
|
+
}
|
|
6799
|
+
alert(`Restored as "${data.restored_timeline_name}". Switch to it in Resolve to inspect.`);
|
|
6800
|
+
await refreshHistoryTimelines();
|
|
6801
|
+
}
|
|
6802
|
+
|
|
6803
|
+
async function archiveCurrentTimelineFromUI(reason) {
|
|
6804
|
+
const data = await api('/api/timeline_versions/action', {
|
|
6805
|
+
method: 'POST',
|
|
6806
|
+
body: JSON.stringify({ action: 'archive_current', reason: reason || undefined }),
|
|
6807
|
+
}).catch(err => ({ success: false, error: String(err) }));
|
|
6808
|
+
if (!data || !data.success) {
|
|
6809
|
+
alert(`Archive failed: ${data?.error || 'unknown'}`);
|
|
6810
|
+
return;
|
|
6811
|
+
}
|
|
6812
|
+
const reasonInput = $('historyArchiveReason');
|
|
6813
|
+
if (reasonInput) reasonInput.value = '';
|
|
6814
|
+
await refreshHistoryTimelines();
|
|
6815
|
+
}
|
|
6816
|
+
|
|
6817
|
+
// ─── Run scoping controls ──────────────────────────────────────────
|
|
6818
|
+
state.runScope = state.runScope || { current: null, recent: [] };
|
|
6819
|
+
|
|
6820
|
+
async function refreshRunScope() {
|
|
6821
|
+
const data = await api('/api/runs').catch(() => ({ success: false }));
|
|
6822
|
+
if (!data || !data.success) return;
|
|
6823
|
+
state.runScope.current = data.current_run_id || null;
|
|
6824
|
+
state.runScope.recent = data.runs || [];
|
|
6825
|
+
const indicator = $('runScopeIndicator');
|
|
6826
|
+
if (indicator) {
|
|
6827
|
+
if (state.runScope.current) {
|
|
6828
|
+
indicator.textContent = state.runScope.current;
|
|
6829
|
+
indicator.classList.add('active');
|
|
6830
|
+
indicator.classList.remove('none');
|
|
6831
|
+
} else {
|
|
6832
|
+
indicator.textContent = 'none';
|
|
6833
|
+
indicator.classList.remove('active');
|
|
6834
|
+
indicator.classList.add('none');
|
|
6835
|
+
}
|
|
6836
|
+
}
|
|
6837
|
+
const recent = $('historyRecentRuns');
|
|
6838
|
+
if (recent) {
|
|
6839
|
+
if (!state.runScope.recent.length) {
|
|
6840
|
+
recent.innerHTML = '<div class="empty">no runs yet</div>';
|
|
6841
|
+
} else {
|
|
6842
|
+
recent.innerHTML = state.runScope.recent.slice(0, 8).map(r => `
|
|
6843
|
+
<div class="history-recent-run-row">
|
|
6844
|
+
<div><strong>${escapeHtml(r.label || '(no label)')}</strong></div>
|
|
6845
|
+
<div>${escapeHtml(r.id || '')}</div>
|
|
6846
|
+
<div>${escapeHtml(r.ended_at ? 'ended ' + formatVersionDate(r.ended_at) : 'open')}</div>
|
|
6847
|
+
</div>
|
|
6848
|
+
`).join('');
|
|
6849
|
+
}
|
|
6850
|
+
}
|
|
6851
|
+
}
|
|
6852
|
+
|
|
6853
|
+
async function beginRunFromUI() {
|
|
6854
|
+
const label = ($('runScopeLabel')?.value || '').trim();
|
|
6855
|
+
const data = await api('/api/runs/begin', {
|
|
6856
|
+
method: 'POST',
|
|
6857
|
+
body: JSON.stringify({ label: label || undefined }),
|
|
6858
|
+
}).catch(err => ({ success: false, error: String(err) }));
|
|
6859
|
+
if (!data || !data.success) {
|
|
6860
|
+
alert(`Begin run failed: ${data?.error || 'unknown'}`);
|
|
6861
|
+
return;
|
|
6862
|
+
}
|
|
6863
|
+
const labelInput = $('runScopeLabel');
|
|
6864
|
+
if (labelInput) labelInput.value = '';
|
|
6865
|
+
await refreshRunScope();
|
|
6866
|
+
}
|
|
6867
|
+
|
|
6868
|
+
async function endRunFromUI() {
|
|
6869
|
+
const data = await api('/api/runs/end', {
|
|
6870
|
+
method: 'POST',
|
|
6871
|
+
body: JSON.stringify({}),
|
|
6872
|
+
}).catch(err => ({ success: false, error: String(err) }));
|
|
6873
|
+
if (!data || !data.success) {
|
|
6874
|
+
alert(`End run failed: ${data?.error || 'unknown'}`);
|
|
6875
|
+
return;
|
|
6876
|
+
}
|
|
6877
|
+
await refreshRunScope();
|
|
6878
|
+
}
|
|
6879
|
+
|
|
6880
|
+
// ─── Structural diff between versions ─────────────────────────────
|
|
6881
|
+
async function showDiffBetween(timelineName, fromVersion, toVersion) {
|
|
6882
|
+
const view = $('historyDiffView');
|
|
6883
|
+
const body = $('historyDiffBody');
|
|
6884
|
+
if (!view || !body) return;
|
|
6885
|
+
view.hidden = false;
|
|
6886
|
+
body.innerHTML = 'loading…';
|
|
6887
|
+
const data = await api('/api/timeline_versions/action', {
|
|
6888
|
+
method: 'POST',
|
|
6889
|
+
body: JSON.stringify({
|
|
6890
|
+
action: 'diff_versions', timeline_name: timelineName,
|
|
6891
|
+
from_version: fromVersion, to_version: toVersion,
|
|
6892
|
+
}),
|
|
6893
|
+
}).catch(err => ({ success: false, error: String(err) }));
|
|
6894
|
+
if (!data || !data.success) {
|
|
6895
|
+
body.innerHTML = `<div class="empty">diff failed: ${escapeHtml(data?.error || 'unknown')}</div>`;
|
|
6896
|
+
return;
|
|
6897
|
+
}
|
|
6898
|
+
const section = (kind, rows) => `
|
|
6899
|
+
<div class="history-diff-section ${kind}">
|
|
6900
|
+
<h4>${kind} (${rows.length})</h4>
|
|
6901
|
+
${rows.length
|
|
6902
|
+
? `<ul>${rows.slice(0, 30).map(r => `<li>${escapeHtml(r.media_pool_item_id)} @ ${escapeHtml(r.track_type)}:${r.track_index} [${r.in_frame}–${r.out_frame}]</li>`).join('')}</ul>`
|
|
6903
|
+
: '<div class="empty">none</div>'}
|
|
6904
|
+
</div>`;
|
|
6905
|
+
body.innerHTML = `
|
|
6906
|
+
<div>v${fromVersion} → v${toVersion}</div>
|
|
6907
|
+
${section('added', data.added || [])}
|
|
6908
|
+
${section('removed', data.removed || [])}
|
|
6909
|
+
${section('moved', data.moved || [])}
|
|
6910
|
+
`;
|
|
6911
|
+
}
|
|
6912
|
+
|
|
6913
|
+
// ─── Caps history chart ───────────────────────────────────────────
|
|
6914
|
+
async function refreshCapsHistory() {
|
|
6915
|
+
const el = $('capsHistoryChart');
|
|
6916
|
+
if (!el) return;
|
|
6917
|
+
const data = await api('/api/caps/history?days=30').catch(() => ({ success: false }));
|
|
6918
|
+
if (!data || !data.success || !(data.history || []).length) {
|
|
6919
|
+
el.innerHTML = '<div class="empty">no usage recorded yet</div>';
|
|
6920
|
+
return;
|
|
6921
|
+
}
|
|
6922
|
+
const rows = (data.history || []).slice().reverse(); // oldest → newest
|
|
6923
|
+
const maxV = Math.max(1, ...rows.map(r => r.vision_tokens || 0));
|
|
6924
|
+
const w = 100, h = 90;
|
|
6925
|
+
const points = rows.map((r, i) => {
|
|
6926
|
+
const x = rows.length === 1 ? 0 : (i / (rows.length - 1)) * w;
|
|
6927
|
+
const y = h - ((r.vision_tokens || 0) / maxV) * (h - 12);
|
|
6928
|
+
return { x, y, value: r.vision_tokens, day: r.day_bucket };
|
|
6929
|
+
});
|
|
6930
|
+
const path = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' ');
|
|
6931
|
+
el.innerHTML = `
|
|
6932
|
+
<svg viewBox="0 0 ${w} ${h + 10}" preserveAspectRatio="none">
|
|
6933
|
+
<line class="axis" x1="0" y1="${h}" x2="${w}" y2="${h}"></line>
|
|
6934
|
+
<path class="line" d="${path}"></path>
|
|
6935
|
+
${points.map(p => `<circle class="point" cx="${p.x.toFixed(1)}" cy="${p.y.toFixed(1)}" r="1.4"><title>${p.day}: ${p.value}</title></circle>`).join('')}
|
|
6936
|
+
<text class="label" x="0" y="${h + 9}">${rows[0].day_bucket}</text>
|
|
6937
|
+
<text class="label" x="${w}" y="${h + 9}" text-anchor="end">${rows[rows.length - 1].day_bucket}</text>
|
|
6938
|
+
<text class="label" x="0" y="9">max ${maxV}</text>
|
|
6939
|
+
</svg>`;
|
|
6940
|
+
}
|
|
6941
|
+
|
|
6942
|
+
// ─── Caps inspector + refusals + reset ──────────────────────────
|
|
6943
|
+
async function inspectCapsFromUI() {
|
|
6944
|
+
const clipId = ($('capsInspectClipId')?.value || '').trim();
|
|
6945
|
+
const jobId = ($('capsInspectJobId')?.value || '').trim();
|
|
6946
|
+
const out = $('capsInspectResult');
|
|
6947
|
+
if (!out) return;
|
|
6948
|
+
if (!clipId && !jobId) {
|
|
6949
|
+
out.textContent = 'enter a clip_id or job_id';
|
|
6950
|
+
out.classList.remove('has-data');
|
|
6951
|
+
return;
|
|
6952
|
+
}
|
|
6953
|
+
const qs = new URLSearchParams();
|
|
6954
|
+
if (clipId) qs.set('clip_id', clipId);
|
|
6955
|
+
if (jobId) qs.set('job_id', jobId);
|
|
6956
|
+
const data = await api(`/api/caps?${qs}`).catch(err => ({ success: false, error: String(err) }));
|
|
6957
|
+
if (!data || !data.success) {
|
|
6958
|
+
out.textContent = `lookup failed: ${data?.error || 'unknown'}`;
|
|
6959
|
+
return;
|
|
6960
|
+
}
|
|
6961
|
+
const usage = data.usage || {};
|
|
6962
|
+
out.classList.add('has-data');
|
|
6963
|
+
out.textContent = JSON.stringify(usage, null, 2);
|
|
6964
|
+
}
|
|
6965
|
+
|
|
6966
|
+
async function resetDayUsageFromUI() {
|
|
6967
|
+
if (!confirm('Delete today\'s day-scope usage rows? This is for testing / circuit-breaker reset only.')) return;
|
|
6968
|
+
const data = await api('/api/caps/reset_day', {
|
|
6969
|
+
method: 'POST',
|
|
6970
|
+
body: JSON.stringify({}),
|
|
6971
|
+
}).catch(err => ({ success: false, error: String(err) }));
|
|
6972
|
+
if (!data || !data.success) {
|
|
6973
|
+
alert(`Reset failed: ${data?.error || 'unknown'}`);
|
|
6974
|
+
return;
|
|
6975
|
+
}
|
|
6976
|
+
alert(`Deleted ${data.deleted} row(s) for ${data.day_bucket}.`);
|
|
6977
|
+
await refreshCapsWidget();
|
|
6978
|
+
await refreshCapsHistory();
|
|
6979
|
+
}
|
|
6980
|
+
|
|
6981
|
+
async function refreshCapsRefusals() {
|
|
6982
|
+
const el = $('capsRefusalsList');
|
|
6983
|
+
if (!el) return;
|
|
6984
|
+
const data = await api('/api/caps/refusals?limit=20').catch(() => ({ success: false }));
|
|
6985
|
+
if (!data || !data.success || !(data.events || []).length) {
|
|
6986
|
+
el.innerHTML = '<div class="empty">no refusals recorded</div>';
|
|
6987
|
+
return;
|
|
6988
|
+
}
|
|
6989
|
+
el.innerHTML = (data.events || []).map(e => `
|
|
6990
|
+
<div class="caps-refusal-row">
|
|
6991
|
+
<span class="reason">${escapeHtml(e.reason || '?')}</span>
|
|
6992
|
+
<span>clip=${escapeHtml(e.clip_id || '—')} job=${escapeHtml(e.job_id || '—')} est=${e.estimated_vision_tokens}</span>
|
|
6993
|
+
<span class="when">${escapeHtml(formatVersionDate(e.occurred_at))}</span>
|
|
6994
|
+
</div>
|
|
6995
|
+
`).join('');
|
|
6996
|
+
}
|
|
6997
|
+
|
|
6998
|
+
// ─── Updates page wiring ──────────────────────────────────────────
|
|
6999
|
+
state.updates = state.updates || { restartTimer: null };
|
|
7000
|
+
|
|
7001
|
+
async function refreshUpdateStatus() {
|
|
7002
|
+
const el = $('updateStatusBadge');
|
|
7003
|
+
if (!el) return;
|
|
7004
|
+
const data = await api('/api/update/status').catch(() => ({ success: false }));
|
|
7005
|
+
if (!data || !data.success) { el.textContent = 'status unknown'; el.dataset.status = ''; return; }
|
|
7006
|
+
el.dataset.status = data.status || '';
|
|
7007
|
+
const cur = data.current_version || '?';
|
|
7008
|
+
const latest = data.latest_version || '?';
|
|
7009
|
+
el.textContent = data.status === 'update_available'
|
|
7010
|
+
? `update available: ${cur} → ${latest}`
|
|
7011
|
+
: (data.status === 'up_to_date' ? `up to date (${cur})` : `${data.status || 'unknown'} — ${cur}`);
|
|
7012
|
+
}
|
|
7013
|
+
|
|
7014
|
+
async function refreshUpdateHistory() {
|
|
7015
|
+
const el = $('updateHistoryTable');
|
|
7016
|
+
if (!el) return;
|
|
7017
|
+
const data = await api('/api/update/history?limit=20').catch(() => ({ success: false }));
|
|
7018
|
+
if (!data || !data.success || !(data.entries || []).length) {
|
|
7019
|
+
el.innerHTML = '<div class="empty">no update history yet</div>';
|
|
7020
|
+
return;
|
|
7021
|
+
}
|
|
7022
|
+
const header = `
|
|
7023
|
+
<div class="update-history-row header">
|
|
7024
|
+
<span>timestamp</span><span>kind</span><span>versions</span><span>reason / msg</span><span>integrity</span><span>by</span>
|
|
7025
|
+
</div>`;
|
|
7026
|
+
const rows = (data.entries || []).map(e => {
|
|
7027
|
+
const v = (e.from_version || '?') + ' → ' + (e.to_version || '?');
|
|
7028
|
+
const statusCls = e.success ? 'status-ok' : 'status-fail';
|
|
7029
|
+
const reason = e.reason || (e.message || '').slice(0, 120);
|
|
7030
|
+
const integ = (e.integrity || {}).verified;
|
|
7031
|
+
const integHtml = integ === true
|
|
7032
|
+
? '<span class="integrity-ok">verified</span>'
|
|
7033
|
+
: integ === false ? '<span class="integrity-bad">MISMATCH</span>'
|
|
7034
|
+
: '<span class="integrity-unknown">—</span>';
|
|
7035
|
+
return `
|
|
7036
|
+
<div class="update-history-row">
|
|
7037
|
+
<span class="${statusCls}">${escapeHtml(e.timestamp || '')}</span>
|
|
7038
|
+
<span class="kind">${escapeHtml(e.kind || '')}</span>
|
|
7039
|
+
<span>${escapeHtml(v)}</span>
|
|
7040
|
+
<span title="${escapeHtml(reason)}">${escapeHtml(reason || '')}</span>
|
|
7041
|
+
<span>${integHtml}</span>
|
|
7042
|
+
<span>${escapeHtml(e.initiator || '')}</span>
|
|
7043
|
+
</div>`;
|
|
7044
|
+
}).join('');
|
|
7045
|
+
el.innerHTML = header + rows;
|
|
7046
|
+
}
|
|
7047
|
+
|
|
7048
|
+
async function previewUpdateFromUI() {
|
|
7049
|
+
const result = $('updateActionResult');
|
|
7050
|
+
if (result) result.textContent = 'fetching preview…';
|
|
7051
|
+
const data = await api('/api/update/preview').catch(err => ({ success: false, error: String(err) }));
|
|
7052
|
+
if (!data || !data.success) {
|
|
7053
|
+
if (result) result.textContent = `preview failed: ${data?.error || 'unknown'}`;
|
|
7054
|
+
return;
|
|
7055
|
+
}
|
|
7056
|
+
const breaking = (data.breaking_changes || []).length
|
|
7057
|
+
? `⚠️ Breaking changes:\n${(data.breaking_changes || []).map(b => ' - ' + b).join('\n')}\n\n`
|
|
7058
|
+
: '';
|
|
7059
|
+
const body = (data.release_notes || '').slice(0, 4000);
|
|
7060
|
+
if (result) {
|
|
7061
|
+
result.textContent = `current: ${data.current_version} → ${data.latest_version} (${data.channel}${data.prerelease ? ', prerelease' : ''})\n\n${breaking}${body}`;
|
|
7062
|
+
}
|
|
7063
|
+
}
|
|
7064
|
+
|
|
7065
|
+
async function applyUpdateFromUI() {
|
|
7066
|
+
const stash = !!$('updateStashCheckbox')?.checked;
|
|
7067
|
+
const force = !!$('updateForceJobsCheckbox')?.checked;
|
|
7068
|
+
const result = $('updateActionResult');
|
|
7069
|
+
if (result) result.textContent = 'applying…';
|
|
7070
|
+
const data = await api('/api/update/apply', {
|
|
7071
|
+
method: 'POST',
|
|
7072
|
+
body: JSON.stringify({
|
|
7073
|
+
strategy: stash ? 'stash_if_needed' : 'refuse_on_dirty',
|
|
7074
|
+
force_active_jobs: force,
|
|
7075
|
+
}),
|
|
7076
|
+
}).catch(err => ({ success: false, error: String(err) }));
|
|
7077
|
+
if (result) result.textContent = JSON.stringify(data, null, 2);
|
|
7078
|
+
await refreshUpdateStatus();
|
|
7079
|
+
await refreshUpdateHistory();
|
|
7080
|
+
await refreshRestartBanner();
|
|
7081
|
+
}
|
|
7082
|
+
|
|
7083
|
+
async function rollbackUpdateFromUI() {
|
|
7084
|
+
if (!confirm('Rollback to the previous build? This runs git reset --hard to the prior SHA. Requires a clean working tree.')) return;
|
|
7085
|
+
const result = $('updateActionResult');
|
|
7086
|
+
if (result) result.textContent = 'rolling back…';
|
|
7087
|
+
const data = await api('/api/update/rollback', {
|
|
7088
|
+
method: 'POST', body: JSON.stringify({}),
|
|
7089
|
+
}).catch(err => ({ success: false, error: String(err) }));
|
|
7090
|
+
if (result) result.textContent = JSON.stringify(data, null, 2);
|
|
7091
|
+
await refreshUpdateHistory();
|
|
7092
|
+
await refreshUpdateStatus();
|
|
7093
|
+
}
|
|
7094
|
+
|
|
7095
|
+
async function refreshRestartBanner() {
|
|
7096
|
+
const banner = $('restartNeededBanner');
|
|
7097
|
+
if (!banner) return;
|
|
7098
|
+
const data = await api('/api/restart_needed').catch(() => ({ needed: false }));
|
|
7099
|
+
if (data && data.needed) {
|
|
7100
|
+
banner.hidden = false;
|
|
7101
|
+
const detail = $('restartBannerDetail');
|
|
7102
|
+
if (detail) {
|
|
7103
|
+
detail.textContent = `from ${data.from_version || '?'} → ${data.to_version || '?'} at ${data.applied_at || ''}`;
|
|
7104
|
+
}
|
|
5428
7105
|
} else {
|
|
5429
|
-
|
|
7106
|
+
banner.hidden = true;
|
|
5430
7107
|
}
|
|
5431
7108
|
}
|
|
5432
7109
|
|
|
5433
|
-
async function
|
|
5434
|
-
const
|
|
5435
|
-
|
|
5436
|
-
|
|
7110
|
+
async function ackRestart() {
|
|
7111
|
+
const data = await api('/api/restart_needed/clear', {
|
|
7112
|
+
method: 'POST', body: JSON.stringify({}),
|
|
7113
|
+
}).catch(() => ({ success: false }));
|
|
7114
|
+
await refreshRestartBanner();
|
|
5437
7115
|
}
|
|
5438
7116
|
|
|
5439
|
-
async function
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
7117
|
+
async function setUpdateChannel(value) {
|
|
7118
|
+
await api('/api/update/channel', {
|
|
7119
|
+
method: 'POST',
|
|
7120
|
+
body: JSON.stringify({ channel: value }),
|
|
7121
|
+
}).catch(() => null);
|
|
7122
|
+
await refreshUpdateStatus();
|
|
5443
7123
|
}
|
|
5444
7124
|
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
7125
|
+
// ─── Media Pool History table (was: Provenance) ─────────────────
|
|
7126
|
+
state.mpc = state.mpc || { allChanges: [], actions: new Set() };
|
|
7127
|
+
|
|
7128
|
+
function populateMpcActionFilter(actions) {
|
|
7129
|
+
const sel = $('mpcActionFilter');
|
|
7130
|
+
if (!sel) return;
|
|
7131
|
+
const prev = sel.value;
|
|
7132
|
+
const sorted = Array.from(actions).sort();
|
|
7133
|
+
sel.innerHTML = '<option value="">all actions</option>' +
|
|
7134
|
+
sorted.map(a => `<option value="${escapeHtml(a)}">${escapeHtml(a)}</option>`).join('');
|
|
7135
|
+
if (sorted.includes(prev)) sel.value = prev;
|
|
5448
7136
|
}
|
|
5449
7137
|
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
if (
|
|
5453
|
-
const
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
7138
|
+
function renderMpcRows(rows) {
|
|
7139
|
+
const el = $('mpcTable');
|
|
7140
|
+
if (!el) return;
|
|
7141
|
+
const meta = $('mpcMeta');
|
|
7142
|
+
if (!rows.length) {
|
|
7143
|
+
el.innerHTML = '<div class="empty">no media-pool changes recorded</div>';
|
|
7144
|
+
if (meta) meta.textContent = '';
|
|
7145
|
+
return;
|
|
7146
|
+
}
|
|
7147
|
+
const header = `
|
|
7148
|
+
<div class="mpc-row header">
|
|
7149
|
+
<span>timestamp</span><span>action</span><span>target</span><span>initiator</span><span>run id</span>
|
|
7150
|
+
</div>`;
|
|
7151
|
+
const groupBy = !!$('mpcGroupByClip')?.checked;
|
|
7152
|
+
const renderRow = (c) => {
|
|
7153
|
+
const ts = escapeHtml(formatVersionDate(c.created_at));
|
|
7154
|
+
const action = escapeHtml(c.action || '');
|
|
7155
|
+
const tname = c.target_name || '';
|
|
7156
|
+
const tid = c.target_id || '';
|
|
7157
|
+
const clipId = (c.params && (c.params.clip_id || c.params.id || c.params.target_id)) || null;
|
|
7158
|
+
const targetCell = clipId
|
|
7159
|
+
? `<div class="target-cell"><a class="clip-link" data-clip-jump="${escapeHtml(clipId)}">${escapeHtml(tname || clipId)}</a><span class="target-id">${escapeHtml(tid || clipId)}</span></div>`
|
|
7160
|
+
: `<div class="target-cell"><span class="target-name">${escapeHtml(tname || tid || '—')}</span>${tid && tname ? `<span class="target-id">${escapeHtml(tid)}</span>` : ''}</div>`;
|
|
7161
|
+
return `
|
|
7162
|
+
<div class="mpc-row">
|
|
7163
|
+
<span>${ts}</span>
|
|
7164
|
+
<span class="action-name">${action}</span>
|
|
7165
|
+
${targetCell}
|
|
7166
|
+
<span>${escapeHtml(c.initiator || '')}</span>
|
|
7167
|
+
<span class="run-id" title="${escapeHtml(c.analysis_run_id || '')}">${escapeHtml((c.analysis_run_id || '—').slice(0, 18))}</span>
|
|
7168
|
+
</div>`;
|
|
7169
|
+
};
|
|
7170
|
+
let body = '';
|
|
7171
|
+
if (groupBy) {
|
|
7172
|
+
const groups = new Map();
|
|
7173
|
+
for (const c of rows) {
|
|
7174
|
+
const key = (c.target_name || c.target_id || '—');
|
|
7175
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
7176
|
+
groups.get(key).push(c);
|
|
7177
|
+
}
|
|
7178
|
+
for (const [key, items] of groups) {
|
|
7179
|
+
body += `<div class="mpc-row group"><span>${escapeHtml(key)}</span><span class="group-count">${items.length} change${items.length === 1 ? '' : 's'}</span></div>`;
|
|
7180
|
+
body += items.map(renderRow).join('');
|
|
7181
|
+
}
|
|
7182
|
+
} else {
|
|
7183
|
+
body = rows.map(renderRow).join('');
|
|
7184
|
+
}
|
|
7185
|
+
el.innerHTML = header + body;
|
|
7186
|
+
if (meta) meta.textContent = `${rows.length} record${rows.length === 1 ? '' : 's'}`;
|
|
5457
7187
|
}
|
|
5458
7188
|
|
|
5459
|
-
function
|
|
5460
|
-
const
|
|
5461
|
-
|
|
5462
|
-
if (
|
|
5463
|
-
|
|
5464
|
-
|
|
7189
|
+
async function refreshMpcTable() {
|
|
7190
|
+
const limit = parseInt(($('mpcLimit')?.value || '50'), 10) || 50;
|
|
7191
|
+
const data = await api(`/api/media_pool_changes?limit=${limit}`).catch(() => ({ success: false }));
|
|
7192
|
+
if (!data || !data.success) {
|
|
7193
|
+
renderMpcRows([]);
|
|
7194
|
+
return;
|
|
7195
|
+
}
|
|
7196
|
+
const changes = data.changes || [];
|
|
7197
|
+
state.mpc.allChanges = changes;
|
|
7198
|
+
const actions = new Set(changes.map(c => c.action).filter(Boolean));
|
|
7199
|
+
state.mpc.actions = actions;
|
|
7200
|
+
populateMpcActionFilter(actions);
|
|
7201
|
+
applyMpcFilter();
|
|
7202
|
+
}
|
|
7203
|
+
|
|
7204
|
+
function applyMpcFilter() {
|
|
7205
|
+
const filter = ($('mpcActionFilter')?.value || '').trim();
|
|
7206
|
+
const rows = filter
|
|
7207
|
+
? state.mpc.allChanges.filter(c => c.action === filter)
|
|
7208
|
+
: state.mpc.allChanges;
|
|
7209
|
+
renderMpcRows(rows);
|
|
7210
|
+
}
|
|
7211
|
+
|
|
7212
|
+
// ─── Analysis caps widget ────────────────────────────────────────────
|
|
7213
|
+
state.caps = state.caps || { preset: 'standard', usage: null, debounce: null, presetsAvailable: null };
|
|
7214
|
+
|
|
7215
|
+
const CAPS_OVERRIDE_FIELDS = [
|
|
7216
|
+
['capsOvResponseChars', 'response_chars'],
|
|
7217
|
+
['capsOvVisionClip', 'vision_tokens_per_clip'],
|
|
7218
|
+
['capsOvFramesClip', 'frames_per_clip'],
|
|
7219
|
+
['capsOvVisionJob', 'vision_tokens_per_job'],
|
|
7220
|
+
['capsOvVisionDay', 'vision_tokens_per_day'],
|
|
7221
|
+
['capsOvWallClock', 'wall_clock_seconds_per_call'],
|
|
7222
|
+
['capsOvFrameDim', 'max_frame_dim_pixels'],
|
|
7223
|
+
];
|
|
7224
|
+
|
|
7225
|
+
const CAPS_PRESET_TAGS = {
|
|
7226
|
+
minimal: 'Preview / triage',
|
|
7227
|
+
standard: 'Realistic default',
|
|
7228
|
+
generous: 'High-fidelity, few clips',
|
|
7229
|
+
unlimited: 'Guards off — use with care',
|
|
7230
|
+
};
|
|
7231
|
+
|
|
7232
|
+
const CAPS_PRESET_STAT_ORDER = [
|
|
7233
|
+
['response_chars', 'response chars'],
|
|
7234
|
+
['vision_tokens_per_clip', 'tokens / clip'],
|
|
7235
|
+
['frames_per_clip', 'frames / clip'],
|
|
7236
|
+
['vision_tokens_per_job', 'tokens / job'],
|
|
7237
|
+
['vision_tokens_per_day', 'tokens / day'],
|
|
7238
|
+
['wall_clock_seconds_per_call', 'wall clock (s)'],
|
|
7239
|
+
['max_frame_dim_pixels', 'frame dim px'],
|
|
7240
|
+
];
|
|
7241
|
+
|
|
7242
|
+
function formatCapValue(v) {
|
|
7243
|
+
if (v === null || v === undefined) return '∞';
|
|
7244
|
+
if (typeof v === 'number') {
|
|
7245
|
+
if (v >= 1000) return (v / 1000).toFixed(v % 1000 === 0 ? 0 : 1).replace(/\.0$/, '') + 'k';
|
|
7246
|
+
return String(v);
|
|
7247
|
+
}
|
|
7248
|
+
return String(v);
|
|
7249
|
+
}
|
|
7250
|
+
|
|
7251
|
+
function renderCapsPresetCards() {
|
|
7252
|
+
const container = $('capsPresetCards');
|
|
7253
|
+
if (!container) return;
|
|
7254
|
+
const presets = state.caps.presetsAvailable || {};
|
|
7255
|
+
const order = ['minimal', 'standard', 'generous', 'unlimited'];
|
|
7256
|
+
const active = state.caps.preset || 'standard';
|
|
7257
|
+
container.innerHTML = order.filter(k => presets[k]).map(key => {
|
|
7258
|
+
const p = presets[key];
|
|
7259
|
+
const stats = CAPS_PRESET_STAT_ORDER.map(([field, label]) => `
|
|
7260
|
+
<span class="stat-label">${escapeHtml(label)}</span>
|
|
7261
|
+
<span class="stat-value">${escapeHtml(formatCapValue(p[field]))}</span>
|
|
7262
|
+
`).join('');
|
|
7263
|
+
return `
|
|
7264
|
+
<button type="button" class="caps-preset-card${key === active ? ' is-active' : ''}" data-preset-card="${escapeHtml(key)}" role="radio" aria-checked="${key === active ? 'true' : 'false'}">
|
|
7265
|
+
<div class="caps-preset-card-head">
|
|
7266
|
+
<span class="caps-preset-card-name">${escapeHtml(key)}</span>
|
|
7267
|
+
${key === active ? '<span class="caps-preset-card-badge">Active</span>' : ''}
|
|
7268
|
+
</div>
|
|
7269
|
+
<div class="caps-preset-card-tag">${escapeHtml(CAPS_PRESET_TAGS[key] || '')}</div>
|
|
7270
|
+
<div class="caps-preset-card-stats">${stats}</div>
|
|
7271
|
+
</button>
|
|
7272
|
+
`;
|
|
7273
|
+
}).join('');
|
|
7274
|
+
}
|
|
7275
|
+
|
|
7276
|
+
function applyCapsOverridePlaceholders(presetKey) {
|
|
7277
|
+
const presets = state.caps.presetsAvailable || {};
|
|
7278
|
+
const p = presets[presetKey] || presets[state.caps.preset] || {};
|
|
7279
|
+
for (const [domId, key] of CAPS_OVERRIDE_FIELDS) {
|
|
7280
|
+
const el = $(domId);
|
|
7281
|
+
if (!el) continue;
|
|
7282
|
+
const val = p[key];
|
|
7283
|
+
el.placeholder = (val === null || val === undefined) ? '∞' : `${presetKey}: ${val}`;
|
|
7284
|
+
}
|
|
7285
|
+
}
|
|
7286
|
+
|
|
7287
|
+
async function refreshCapsWidget() {
|
|
7288
|
+
const data = await api('/api/caps').catch(err => ({ success: false, error: String(err) }));
|
|
7289
|
+
if (!data || !data.success) return;
|
|
7290
|
+
state.caps.preset = data.preset;
|
|
7291
|
+
state.caps.usage = data.usage || null;
|
|
7292
|
+
state.caps.presetsAvailable = data.presets_available || state.caps.presetsAvailable;
|
|
7293
|
+
const presetEl = $('prefCapsPreset');
|
|
7294
|
+
if (presetEl) presetEl.value = data.preset;
|
|
7295
|
+
renderCapsPresetCards();
|
|
7296
|
+
applyCapsOverridePlaceholders(data.preset);
|
|
7297
|
+
// Render usage gauges (only if a project is open, otherwise the usage
|
|
7298
|
+
// block stays at its placeholder).
|
|
7299
|
+
const usage = data.usage;
|
|
7300
|
+
if (usage) {
|
|
7301
|
+
renderCapsGauge('clip', usage.usage?.clip?.vision_tokens, usage.caps?.vision_tokens_per_clip, usage.percent_consumed?.clip);
|
|
7302
|
+
renderCapsGauge('job', usage.usage?.job?.vision_tokens, usage.caps?.vision_tokens_per_job, usage.percent_consumed?.job);
|
|
7303
|
+
renderCapsGauge('day', usage.usage?.day?.vision_tokens, usage.caps?.vision_tokens_per_day, usage.percent_consumed?.day);
|
|
7304
|
+
}
|
|
7305
|
+
}
|
|
7306
|
+
|
|
7307
|
+
function renderCapsGauge(scope, used, cap, percent) {
|
|
7308
|
+
const gauge = document.querySelector(`.caps-gauge[data-scope="${scope}"]`);
|
|
7309
|
+
if (!gauge) return;
|
|
7310
|
+
const fill = gauge.querySelector('.caps-gauge-fill');
|
|
7311
|
+
const numbers = gauge.querySelector('.caps-gauge-numbers');
|
|
7312
|
+
const u = used ?? 0;
|
|
7313
|
+
if (cap === null || cap === undefined) {
|
|
7314
|
+
fill.style.width = '0%';
|
|
7315
|
+
numbers.textContent = `${formatCapValue(u)} / ∞`;
|
|
7316
|
+
gauge.dataset.state = '';
|
|
7317
|
+
return;
|
|
7318
|
+
}
|
|
7319
|
+
const p = percent != null ? percent : (cap > 0 ? Math.min(100, 100 * u / cap) : 0);
|
|
7320
|
+
fill.style.width = `${p}%`;
|
|
7321
|
+
numbers.textContent = `${formatCapValue(u)} / ${formatCapValue(cap)} · ${p.toFixed(0)}%`;
|
|
7322
|
+
gauge.dataset.state = p >= 90 ? 'over' : (p >= 70 ? 'warn' : '');
|
|
7323
|
+
}
|
|
7324
|
+
|
|
7325
|
+
function persistCapsFromUI() {
|
|
7326
|
+
const presetEl = $('prefCapsPreset');
|
|
7327
|
+
const preset = (presetEl && presetEl.value) || 'standard';
|
|
7328
|
+
const overrides = {};
|
|
7329
|
+
for (const [domId, key] of CAPS_OVERRIDE_FIELDS) {
|
|
7330
|
+
const el = $(domId);
|
|
7331
|
+
if (!el) continue;
|
|
7332
|
+
const raw = (el.value || '').trim();
|
|
7333
|
+
if (!raw) continue;
|
|
7334
|
+
overrides[key] = raw === 'unlimited' ? null : (Number.isFinite(+raw) ? +raw : raw);
|
|
7335
|
+
}
|
|
7336
|
+
clearTimeout(state.caps.debounce);
|
|
7337
|
+
state.caps.debounce = setTimeout(async () => {
|
|
7338
|
+
const result = await api('/api/caps', {
|
|
7339
|
+
method: 'POST',
|
|
7340
|
+
body: JSON.stringify({ preset, overrides }),
|
|
7341
|
+
}).catch(err => ({ success: false, error: String(err) }));
|
|
7342
|
+
if (!result || !result.success) {
|
|
7343
|
+
console.warn('caps save failed:', result?.error);
|
|
7344
|
+
}
|
|
7345
|
+
await refreshCapsWidget();
|
|
7346
|
+
}, 300);
|
|
5465
7347
|
}
|
|
5466
7348
|
|
|
5467
7349
|
function reviewSetView(view, opts = {}) {
|
|
@@ -5473,6 +7355,8 @@ HTML = r"""<!doctype html>
|
|
|
5473
7355
|
if (transcriptEl) transcriptEl.style.display = view === 'transcript' ? '' : 'none';
|
|
5474
7356
|
const combinedEl = $('reviewCombinedView');
|
|
5475
7357
|
if (combinedEl) combinedEl.style.display = view === 'combined' ? '' : 'none';
|
|
7358
|
+
const historyEl = $('reviewHistoryView');
|
|
7359
|
+
if (historyEl) historyEl.style.display = view === 'history' ? '' : 'none';
|
|
5476
7360
|
const back = $('reviewBackBtn');
|
|
5477
7361
|
if (back) back.style.display = view === 'bin' ? 'none' : '';
|
|
5478
7362
|
const meta = $('reviewMeta');
|
|
@@ -5482,6 +7366,7 @@ HTML = r"""<!doctype html>
|
|
|
5482
7366
|
else if (view === 'shot') meta.textContent = `Shot ${state.review.currentShotIndex} of ${state.review.currentClipData?.card?.clip_name || 'clip'}`;
|
|
5483
7367
|
else if (view === 'transcript') meta.textContent = `Transcript · ${state.review.currentClipData?.card?.clip_name || 'clip'}`;
|
|
5484
7368
|
else if (view === 'combined') meta.textContent = `Combined review · ${state.review.combinedData?.clip_count || '?'} clips`;
|
|
7369
|
+
else if (view === 'history') meta.textContent = 'Timeline history · versions and brain edits per timeline (C6)';
|
|
5485
7370
|
}
|
|
5486
7371
|
if (opts.writePanelState !== false) {
|
|
5487
7372
|
writePanelStateAsync({
|
|
@@ -5500,8 +7385,63 @@ HTML = r"""<!doctype html>
|
|
|
5500
7385
|
state.review.clipList = data;
|
|
5501
7386
|
populateBinFilter();
|
|
5502
7387
|
renderReviewBin();
|
|
7388
|
+
// Coverage runs in parallel — failures here must not block the clip grid.
|
|
7389
|
+
refreshReadinessCard().catch(() => {});
|
|
5503
7390
|
}
|
|
5504
7391
|
|
|
7392
|
+
async function refreshReadinessCard() {
|
|
7393
|
+
const card = $('reviewReadinessCard');
|
|
7394
|
+
if (!card) return;
|
|
7395
|
+
const data = await api('/api/coverage').catch(err => ({ success: false, error: String(err) }));
|
|
7396
|
+
if (!data || !data.success) {
|
|
7397
|
+
card.hidden = true;
|
|
7398
|
+
return;
|
|
7399
|
+
}
|
|
7400
|
+
card.hidden = false;
|
|
7401
|
+
const summary = data.summary || {};
|
|
7402
|
+
const evidence = $('readinessEvidenceBase');
|
|
7403
|
+
const row = $('readinessSummaryRow');
|
|
7404
|
+
const details = $('readinessDetails');
|
|
7405
|
+
const total = summary.clips_total_with_reports || 0;
|
|
7406
|
+
const signed = summary.clips_signed || 0;
|
|
7407
|
+
const superseded = summary.clips_superseded_by_relink || 0;
|
|
7408
|
+
const visionPending = summary.clips_vision_pending || 0;
|
|
7409
|
+
const warnings = summary.technical_warning_count || 0;
|
|
7410
|
+
const pct = total ? Math.round((signed / total) * 100) : 0;
|
|
7411
|
+
const trustDist = summary.source_trust_distribution || {};
|
|
7412
|
+
const layers = summary.layer_coverage || {};
|
|
7413
|
+
const fragments = [`${signed}/${total} clips analyzed (${pct}%)`];
|
|
7414
|
+
if (superseded) fragments.push(`${superseded} relink-superseded`);
|
|
7415
|
+
if (visionPending) fragments.push(`${visionPending} vision pending`);
|
|
7416
|
+
if (warnings) fragments.push(`${warnings} warnings`);
|
|
7417
|
+
if (evidence) evidence.textContent = 'evidence base: ' + fragments.join(', ') + '.';
|
|
7418
|
+
if (row) {
|
|
7419
|
+
row.innerHTML = [
|
|
7420
|
+
{ label: 'Analyzed', value: signed, kind: 'good' },
|
|
7421
|
+
{ label: 'Superseded', value: superseded, kind: superseded ? 'danger' : '' },
|
|
7422
|
+
{ label: 'Vision pending', value: visionPending, kind: visionPending ? 'warn' : '' },
|
|
7423
|
+
{ label: 'Warnings', value: warnings, kind: warnings ? 'warn' : '' },
|
|
7424
|
+
].map(stat => `<div class="readiness-stat ${stat.kind}"><span class="stat-value">${stat.value}</span><span class="stat-label">${escapeHtml(stat.label)}</span></div>`).join('');
|
|
7425
|
+
}
|
|
7426
|
+
if (details) {
|
|
7427
|
+
const trustChips = Object.entries(trustDist)
|
|
7428
|
+
.sort((a, b) => b[1] - a[1])
|
|
7429
|
+
.map(([k, v]) => `<span class="chip" title="source_trust">${escapeHtml(k)}: ${v}</span>`)
|
|
7430
|
+
.join('');
|
|
7431
|
+
const layerChips = ['technical', 'motion', 'transcription', 'vision', 'cut_analysis', 'readthrough']
|
|
7432
|
+
.map(layer => `<span class="chip" title="layer present count">${escapeHtml(layer)}: ${layers[layer] || 0}</span>`)
|
|
7433
|
+
.join('');
|
|
7434
|
+
details.innerHTML = `<span class="chip" style="background:none">source_trust:</span>${trustChips}<span class="chip" style="background:none">layers:</span>${layerChips}`;
|
|
7435
|
+
}
|
|
7436
|
+
}
|
|
7437
|
+
|
|
7438
|
+
document.addEventListener('click', (e) => {
|
|
7439
|
+
const target = e.target;
|
|
7440
|
+
if (target && target.id === 'readinessRefreshBtn') {
|
|
7441
|
+
refreshReadinessCard().catch(() => {});
|
|
7442
|
+
}
|
|
7443
|
+
});
|
|
7444
|
+
|
|
5505
7445
|
function populateBinFilter() {
|
|
5506
7446
|
const data = state.review.clipList;
|
|
5507
7447
|
const select = $('reviewBinFilter');
|
|
@@ -7791,6 +9731,109 @@ HTML = r"""<!doctype html>
|
|
|
7791
9731
|
if (state.review && state.review.panelStateTimer) clearInterval(state.review.panelStateTimer);
|
|
7792
9732
|
});
|
|
7793
9733
|
$('reviewRefreshBtn').onclick = () => refreshReviewBin().catch(alertError);
|
|
9734
|
+
const historyBtnEl = $('reviewHistoryBtn');
|
|
9735
|
+
if (historyBtnEl) {
|
|
9736
|
+
historyBtnEl.onclick = () => {
|
|
9737
|
+
reviewSetView('history');
|
|
9738
|
+
refreshHistoryTimelines().catch(alertError);
|
|
9739
|
+
};
|
|
9740
|
+
}
|
|
9741
|
+
const historyRefreshEl = $('historyRefreshBtn');
|
|
9742
|
+
if (historyRefreshEl) {
|
|
9743
|
+
historyRefreshEl.onclick = () => refreshHistoryTimelines().catch(alertError);
|
|
9744
|
+
}
|
|
9745
|
+
const historyArchiveBtnEl = $('historyArchiveCurrentBtn');
|
|
9746
|
+
if (historyArchiveBtnEl) {
|
|
9747
|
+
historyArchiveBtnEl.onclick = () => {
|
|
9748
|
+
const reason = ($('historyArchiveReason')?.value || '').trim();
|
|
9749
|
+
archiveCurrentTimelineFromUI(reason).catch(alertError);
|
|
9750
|
+
};
|
|
9751
|
+
}
|
|
9752
|
+
// Caps widget bootstrap + listeners
|
|
9753
|
+
const capsPresetCardsEl = $('capsPresetCards');
|
|
9754
|
+
if (capsPresetCardsEl) {
|
|
9755
|
+
capsPresetCardsEl.addEventListener('click', (ev) => {
|
|
9756
|
+
const card = ev.target.closest('[data-preset-card]');
|
|
9757
|
+
if (!card) return;
|
|
9758
|
+
const preset = card.dataset.presetCard;
|
|
9759
|
+
if (!preset) return;
|
|
9760
|
+
const hidden = $('prefCapsPreset');
|
|
9761
|
+
if (hidden) hidden.value = preset;
|
|
9762
|
+
state.caps.preset = preset;
|
|
9763
|
+
// Optimistic UI: re-render cards + override placeholders immediately.
|
|
9764
|
+
renderCapsPresetCards();
|
|
9765
|
+
applyCapsOverridePlaceholders(preset);
|
|
9766
|
+
persistCapsFromUI();
|
|
9767
|
+
});
|
|
9768
|
+
}
|
|
9769
|
+
for (const [domId] of CAPS_OVERRIDE_FIELDS) {
|
|
9770
|
+
const el = $(domId);
|
|
9771
|
+
if (el) el.addEventListener('input', persistCapsFromUI);
|
|
9772
|
+
}
|
|
9773
|
+
refreshCapsWidget().catch(() => {});
|
|
9774
|
+
refreshCapsHistory().catch(() => {});
|
|
9775
|
+
refreshCapsRefusals().catch(() => {});
|
|
9776
|
+
|
|
9777
|
+
// Caps inspector + reset
|
|
9778
|
+
$('capsInspectBtn')?.addEventListener('click', () => inspectCapsFromUI().catch(alertError));
|
|
9779
|
+
$('capsResetDayBtn')?.addEventListener('click', () => resetDayUsageFromUI().catch(alertError));
|
|
9780
|
+
|
|
9781
|
+
// Run scoping bar
|
|
9782
|
+
$('runBeginBtn')?.addEventListener('click', () => beginRunFromUI().catch(alertError));
|
|
9783
|
+
$('runEndBtn')?.addEventListener('click', () => endRunFromUI().catch(alertError));
|
|
9784
|
+
refreshRunScope().catch(() => {});
|
|
9785
|
+
|
|
9786
|
+
// History diff close
|
|
9787
|
+
$('historyDiffCloseBtn')?.addEventListener('click', () => {
|
|
9788
|
+
const view = $('historyDiffView');
|
|
9789
|
+
if (view) view.hidden = true;
|
|
9790
|
+
});
|
|
9791
|
+
|
|
9792
|
+
// Updates page wiring
|
|
9793
|
+
$('updatePreviewBtn')?.addEventListener('click', () => previewUpdateFromUI().catch(alertError));
|
|
9794
|
+
$('updateApplyBtn')?.addEventListener('click', () => applyUpdateFromUI().catch(alertError));
|
|
9795
|
+
$('updateRollbackBtn')?.addEventListener('click', () => rollbackUpdateFromUI().catch(alertError));
|
|
9796
|
+
$('prefUpdateChannel')?.addEventListener('change', e => setUpdateChannel(e.target.value).catch(() => {}));
|
|
9797
|
+
$('restartBannerAck')?.addEventListener('click', () => ackRestart().catch(() => {}));
|
|
9798
|
+
refreshUpdateStatus().catch(() => {});
|
|
9799
|
+
refreshUpdateHistory().catch(() => {});
|
|
9800
|
+
refreshRestartBanner().catch(() => {});
|
|
9801
|
+
// Poll for restart marker every 30s
|
|
9802
|
+
if (state.updates.restartTimer) clearInterval(state.updates.restartTimer);
|
|
9803
|
+
state.updates.restartTimer = setInterval(() => refreshRestartBanner().catch(() => {}), 30000);
|
|
9804
|
+
|
|
9805
|
+
// Auto-save preference (server-side preference; reads via /api/setup/defaults)
|
|
9806
|
+
$('prefAutoSaveAfterArchive')?.addEventListener('change', async e => {
|
|
9807
|
+
const data = await api('/api/setup/defaults', {
|
|
9808
|
+
method: 'POST',
|
|
9809
|
+
body: JSON.stringify({ timeline_versioning_auto_save_after_archive: !!e.target.checked }),
|
|
9810
|
+
}).catch(err => ({ success: false, error: String(err) }));
|
|
9811
|
+
if (!data || !data.success) {
|
|
9812
|
+
alert(`Save failed: ${data?.error || 'unknown'}`);
|
|
9813
|
+
}
|
|
9814
|
+
});
|
|
9815
|
+
|
|
9816
|
+
// Media Pool History (Diagnostics)
|
|
9817
|
+
$('mpcRefreshBtn')?.addEventListener('click', () => refreshMpcTable().catch(alertError));
|
|
9818
|
+
$('mpcLimit')?.addEventListener('change', () => refreshMpcTable().catch(alertError));
|
|
9819
|
+
$('mpcActionFilter')?.addEventListener('change', () => applyMpcFilter());
|
|
9820
|
+
$('mpcGroupByClip')?.addEventListener('change', () => applyMpcFilter());
|
|
9821
|
+
$('mpcTable')?.addEventListener('click', (ev) => {
|
|
9822
|
+
const link = ev.target.closest('[data-clip-jump]');
|
|
9823
|
+
if (!link) return;
|
|
9824
|
+
ev.preventDefault();
|
|
9825
|
+
const clipId = link.dataset.clipJump;
|
|
9826
|
+
if (!clipId) return;
|
|
9827
|
+
try {
|
|
9828
|
+
setPanel('analysis', { subpage: 'review' });
|
|
9829
|
+
if (typeof openClipDetail === 'function') {
|
|
9830
|
+
openClipDetail(clipId).catch(err => console.warn('clip-jump failed', err));
|
|
9831
|
+
}
|
|
9832
|
+
} catch (err) {
|
|
9833
|
+
console.warn('clip-jump failed', err);
|
|
9834
|
+
}
|
|
9835
|
+
});
|
|
9836
|
+
refreshMpcTable().catch(() => {});
|
|
7794
9837
|
$('reviewBackBtn').onclick = () => {
|
|
7795
9838
|
if (state.review.view === 'shot') {
|
|
7796
9839
|
state.review.currentShotIndex = null;
|
|
@@ -7815,6 +9858,8 @@ HTML = r"""<!doctype html>
|
|
|
7815
9858
|
state.review.currentClipId = null;
|
|
7816
9859
|
state.review.currentClipData = null;
|
|
7817
9860
|
reviewSetView('bin');
|
|
9861
|
+
} else if (state.review.view === 'history') {
|
|
9862
|
+
reviewSetView('bin');
|
|
7818
9863
|
}
|
|
7819
9864
|
};
|
|
7820
9865
|
$('reviewBinGrid').addEventListener('click', event => {
|
|
@@ -10007,6 +12052,68 @@ def _v2_open_clip_in_resolve(body: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
10007
12052
|
return {"success": False, "error": f"{type(exc).__name__}: {exc}"}
|
|
10008
12053
|
|
|
10009
12054
|
|
|
12055
|
+
# ─── C6: timeline version chain + brain-edit history helpers ─────────────────
|
|
12056
|
+
|
|
12057
|
+
|
|
12058
|
+
def list_timelines_with_versions(project_root: str) -> Dict[str, Any]:
|
|
12059
|
+
"""Every timeline that has at least one archived version, with counts."""
|
|
12060
|
+
try:
|
|
12061
|
+
conn = _timeline_brain_db.connect(project_root)
|
|
12062
|
+
except Exception as exc:
|
|
12063
|
+
return {"success": False, "error": f"{type(exc).__name__}: {exc}", "timelines": []}
|
|
12064
|
+
rows = conn.execute(
|
|
12065
|
+
"""
|
|
12066
|
+
SELECT timeline_name,
|
|
12067
|
+
COUNT(*) AS version_count,
|
|
12068
|
+
MAX(version) AS latest_version,
|
|
12069
|
+
MAX(created_at) AS most_recent
|
|
12070
|
+
FROM timeline_versions
|
|
12071
|
+
GROUP BY timeline_name
|
|
12072
|
+
ORDER BY most_recent DESC NULLS LAST
|
|
12073
|
+
"""
|
|
12074
|
+
).fetchall()
|
|
12075
|
+
return {
|
|
12076
|
+
"success": True,
|
|
12077
|
+
"timelines": [dict(r) for r in rows],
|
|
12078
|
+
}
|
|
12079
|
+
|
|
12080
|
+
|
|
12081
|
+
def get_timeline_history_payload(
|
|
12082
|
+
project_root: str, timeline_name: str, *, history_limit: int = 200,
|
|
12083
|
+
) -> Dict[str, Any]:
|
|
12084
|
+
"""Combined payload: version chain + brain edits for a single timeline."""
|
|
12085
|
+
versions = _timeline_versioning.list_timeline_versions(
|
|
12086
|
+
project_root=project_root, timeline_name=timeline_name,
|
|
12087
|
+
)
|
|
12088
|
+
edits = _brain_edits.get_brain_edit_history(
|
|
12089
|
+
project_root=project_root, timeline_name=timeline_name, limit=history_limit,
|
|
12090
|
+
)
|
|
12091
|
+
return {
|
|
12092
|
+
"success": True,
|
|
12093
|
+
"timeline_name": timeline_name,
|
|
12094
|
+
"versions": versions,
|
|
12095
|
+
"edits": edits,
|
|
12096
|
+
}
|
|
12097
|
+
|
|
12098
|
+
|
|
12099
|
+
def proxy_timeline_versioning_action(body: Dict[str, Any]) -> Dict[str, Any]:
|
|
12100
|
+
"""Bridge dashboard → MCP server timeline_versioning tool.
|
|
12101
|
+
|
|
12102
|
+
Body shape: {action, ...params}. Used for write actions (archive, rollback,
|
|
12103
|
+
prune) that need a live Resolve connection.
|
|
12104
|
+
"""
|
|
12105
|
+
action = (body.get("action") or "").strip()
|
|
12106
|
+
if not action:
|
|
12107
|
+
return {"success": False, "error": "action required"}
|
|
12108
|
+
try:
|
|
12109
|
+
from src.server import timeline_versioning as _tv_tool
|
|
12110
|
+
params = {k: v for k, v in body.items() if k != "action"}
|
|
12111
|
+
return _tv_tool(action, params=params)
|
|
12112
|
+
except Exception as exc: # noqa: BLE001
|
|
12113
|
+
return {"success": False, "error": f"{type(exc).__name__}: {exc}"}
|
|
12114
|
+
|
|
12115
|
+
|
|
12116
|
+
|
|
10010
12117
|
def _v2_enrich_search_results(project_root: str, results: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
10011
12118
|
"""Augment /api/index/query results with fps, shot_index, and a thumbnail frame.
|
|
10012
12119
|
|
|
@@ -10491,17 +12598,62 @@ def _mcp_uninstall_payload(client_id: str) -> Dict[str, Any]:
|
|
|
10491
12598
|
return {"success": True, "message": f"Removed davinci-resolve from {config_path}", "client_id": client_id}
|
|
10492
12599
|
|
|
10493
12600
|
|
|
10494
|
-
def
|
|
12601
|
+
def _list_active_batch_jobs(project_root: str) -> List[Dict[str, Any]]:
|
|
12602
|
+
"""Across the analysis base root, find batch jobs with status='running'.
|
|
12603
|
+
|
|
12604
|
+
Used by the update apply path: refuse to update mid-job because it would
|
|
12605
|
+
corrupt in-flight clip analysis state (the new build's schema may not match
|
|
12606
|
+
what the running batch was started against).
|
|
12607
|
+
"""
|
|
12608
|
+
out: List[Dict[str, Any]] = []
|
|
12609
|
+
if not project_root:
|
|
12610
|
+
return out
|
|
12611
|
+
base = os.path.dirname(os.path.normpath(project_root))
|
|
12612
|
+
if not os.path.isdir(base):
|
|
12613
|
+
return out
|
|
12614
|
+
for entry in sorted(os.listdir(base)):
|
|
12615
|
+
candidate = os.path.join(base, entry)
|
|
12616
|
+
if not os.path.isdir(candidate):
|
|
12617
|
+
continue
|
|
12618
|
+
try:
|
|
12619
|
+
payload = list_batch_jobs(candidate, limit=200)
|
|
12620
|
+
except Exception:
|
|
12621
|
+
continue
|
|
12622
|
+
for job in payload.get("jobs") or []:
|
|
12623
|
+
if (job.get("status") or "").lower() == "running":
|
|
12624
|
+
out.append({
|
|
12625
|
+
"project_root": candidate,
|
|
12626
|
+
"job_id": job.get("job_id"),
|
|
12627
|
+
"status": job.get("status"),
|
|
12628
|
+
"started_at": job.get("started_at"),
|
|
12629
|
+
})
|
|
12630
|
+
return out
|
|
12631
|
+
|
|
12632
|
+
|
|
12633
|
+
def _update_apply_payload(*, strategy: str = "refuse_on_dirty", force_active_jobs: bool = False,
|
|
12634
|
+
project_root: Optional[str] = None) -> Dict[str, Any]:
|
|
10495
12635
|
"""Apply a guarded git fast-forward update by delegating to install.py's
|
|
10496
12636
|
existing apply_safe_self_update. Returns a structured result for the UI.
|
|
10497
12637
|
"""
|
|
12638
|
+
# Active-job lock — refuse to update if a batch analysis job is mid-flight.
|
|
12639
|
+
# Pass force_active_jobs=true to override (the user explicitly accepts the
|
|
12640
|
+
# risk of in-flight state being inconsistent with the new build).
|
|
12641
|
+
if not force_active_jobs and project_root:
|
|
12642
|
+
active = _list_active_batch_jobs(project_root)
|
|
12643
|
+
if active:
|
|
12644
|
+
return {
|
|
12645
|
+
"success": False,
|
|
12646
|
+
"reason": "active_jobs",
|
|
12647
|
+
"message": f"{len(active)} batch analysis job(s) are currently running. Cancel them or pass force=true to override.",
|
|
12648
|
+
"active_jobs": active,
|
|
12649
|
+
}
|
|
10498
12650
|
try:
|
|
10499
12651
|
sys.path.insert(0, _repo_root())
|
|
10500
12652
|
from install import apply_safe_self_update # type: ignore
|
|
10501
12653
|
except Exception as exc:
|
|
10502
12654
|
return {"success": False, "error": f"update helper unavailable: {exc}"}
|
|
10503
12655
|
try:
|
|
10504
|
-
result = apply_safe_self_update(_repo_root(), dry_run=False)
|
|
12656
|
+
result = apply_safe_self_update(_repo_root(), dry_run=False, initiator="dashboard", strategy=strategy)
|
|
10505
12657
|
except Exception as exc:
|
|
10506
12658
|
return {"success": False, "error": str(exc)}
|
|
10507
12659
|
out: Dict[str, Any] = {
|
|
@@ -10510,12 +12662,148 @@ def _update_apply_payload() -> Dict[str, Any]:
|
|
|
10510
12662
|
"reason": result.get("reason"),
|
|
10511
12663
|
"message": result.get("message"),
|
|
10512
12664
|
"current_version": _mcp_version(),
|
|
12665
|
+
"from_version": result.get("from_version"),
|
|
12666
|
+
"to_version": result.get("to_version"),
|
|
12667
|
+
"from_sha": result.get("from_sha"),
|
|
12668
|
+
"to_sha": result.get("to_sha"),
|
|
10513
12669
|
}
|
|
10514
12670
|
if result.get("success") and result.get("changed"):
|
|
10515
12671
|
out["restart_required"] = True
|
|
12672
|
+
# Eagerly migrate per-project DBs so schema bumps in the new build
|
|
12673
|
+
# surface immediately instead of waiting for the next analysis call.
|
|
12674
|
+
out["db_migrations"] = _eager_migrate_after_update(project_root)
|
|
12675
|
+
# Drop a restart-needed marker the host / dashboard can poll for.
|
|
12676
|
+
_write_restart_marker(_repo_root(), result)
|
|
12677
|
+
# Surface stash status on the result.
|
|
12678
|
+
for k in ("stash_ref", "stash_pop_conflict", "remediation"):
|
|
12679
|
+
if k in result and result[k] is not None:
|
|
12680
|
+
out[k] = result[k]
|
|
12681
|
+
return out
|
|
12682
|
+
|
|
12683
|
+
|
|
12684
|
+
def _write_restart_marker(repo_root: str, update_result: Dict[str, Any]) -> None:
|
|
12685
|
+
"""Drop a `.mcp_restart_needed` marker file with update metadata.
|
|
12686
|
+
|
|
12687
|
+
The MCP server is a child process of the host (Claude Code, etc.) so we
|
|
12688
|
+
can't restart it ourselves. The marker is a hint the host can poll via
|
|
12689
|
+
`/api/restart_needed` or by reading the file directly.
|
|
12690
|
+
"""
|
|
12691
|
+
log_dir = os.path.join(repo_root, "logs")
|
|
12692
|
+
try:
|
|
12693
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
12694
|
+
marker = {
|
|
12695
|
+
"needed": True,
|
|
12696
|
+
"from_version": update_result.get("from_version"),
|
|
12697
|
+
"to_version": update_result.get("to_version"),
|
|
12698
|
+
"from_sha": update_result.get("from_sha"),
|
|
12699
|
+
"to_sha": update_result.get("to_sha"),
|
|
12700
|
+
"applied_at": _now_iso_safe(),
|
|
12701
|
+
}
|
|
12702
|
+
with open(os.path.join(log_dir, ".mcp_restart_needed"), "w", encoding="utf-8") as fh:
|
|
12703
|
+
json.dump(marker, fh, indent=2)
|
|
12704
|
+
except OSError:
|
|
12705
|
+
pass
|
|
12706
|
+
|
|
12707
|
+
|
|
12708
|
+
def _now_iso_safe() -> str:
|
|
12709
|
+
import time as _time
|
|
12710
|
+
return _time.strftime("%Y-%m-%dT%H:%M:%SZ", _time.gmtime())
|
|
12711
|
+
|
|
12712
|
+
|
|
12713
|
+
def _read_restart_marker(repo_root: str) -> Dict[str, Any]:
|
|
12714
|
+
path = os.path.join(repo_root, "logs", ".mcp_restart_needed")
|
|
12715
|
+
if not os.path.isfile(path):
|
|
12716
|
+
return {"needed": False}
|
|
12717
|
+
try:
|
|
12718
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
12719
|
+
payload = json.load(fh)
|
|
12720
|
+
if isinstance(payload, dict):
|
|
12721
|
+
payload.setdefault("needed", True)
|
|
12722
|
+
return payload
|
|
12723
|
+
except (OSError, json.JSONDecodeError):
|
|
12724
|
+
pass
|
|
12725
|
+
return {"needed": True, "marker_path": path}
|
|
12726
|
+
|
|
12727
|
+
|
|
12728
|
+
def _clear_restart_marker(repo_root: str) -> Dict[str, Any]:
|
|
12729
|
+
path = os.path.join(repo_root, "logs", ".mcp_restart_needed")
|
|
12730
|
+
try:
|
|
12731
|
+
os.remove(path)
|
|
12732
|
+
except OSError:
|
|
12733
|
+
return {"success": False, "error": "marker not present or unreadable"}
|
|
12734
|
+
return {"success": True}
|
|
12735
|
+
|
|
12736
|
+
|
|
12737
|
+
def _eager_migrate_after_update(project_root: Optional[str] = None) -> Dict[str, Any]:
|
|
12738
|
+
"""Walk every project under the analysis base root and open + migrate its
|
|
12739
|
+
timeline_brain.sqlite. Surfaces schema bumps from the new build right after
|
|
12740
|
+
`git pull` instead of on next per-project work."""
|
|
12741
|
+
if project_root:
|
|
12742
|
+
base = os.path.dirname(os.path.normpath(project_root))
|
|
12743
|
+
else:
|
|
12744
|
+
base = os.path.expanduser("~/Documents/davinci-resolve-mcp-analysis")
|
|
12745
|
+
migrated: List[Dict[str, Any]] = []
|
|
12746
|
+
if not os.path.isdir(base):
|
|
12747
|
+
return {"success": True, "migrated": migrated, "note": "no base root found"}
|
|
12748
|
+
for entry in sorted(os.listdir(base)):
|
|
12749
|
+
candidate = os.path.join(base, entry)
|
|
12750
|
+
if not os.path.isdir(os.path.join(candidate, "_soul")):
|
|
12751
|
+
continue
|
|
12752
|
+
try:
|
|
12753
|
+
_timeline_brain_db.connect(candidate)
|
|
12754
|
+
migrated.append({"project_root": candidate, "ok": True})
|
|
12755
|
+
except Exception as exc:
|
|
12756
|
+
migrated.append({"project_root": candidate, "ok": False, "error": str(exc)})
|
|
12757
|
+
return {"success": True, "migrated": migrated}
|
|
12758
|
+
|
|
12759
|
+
|
|
12760
|
+
def _update_rollback_payload() -> Dict[str, Any]:
|
|
12761
|
+
try:
|
|
12762
|
+
sys.path.insert(0, _repo_root())
|
|
12763
|
+
from install import rollback_to_previous_build # type: ignore
|
|
12764
|
+
except Exception as exc:
|
|
12765
|
+
return {"success": False, "error": f"rollback helper unavailable: {exc}"}
|
|
12766
|
+
try:
|
|
12767
|
+
result = rollback_to_previous_build(_repo_root(), initiator="dashboard")
|
|
12768
|
+
except Exception as exc:
|
|
12769
|
+
return {"success": False, "error": str(exc)}
|
|
12770
|
+
out: Dict[str, Any] = dict(result)
|
|
12771
|
+
out["current_version"] = _mcp_version()
|
|
12772
|
+
if result.get("success"):
|
|
12773
|
+
out["restart_required"] = True
|
|
10516
12774
|
return out
|
|
10517
12775
|
|
|
10518
12776
|
|
|
12777
|
+
def _update_history_payload(limit: int = 20) -> Dict[str, Any]:
|
|
12778
|
+
try:
|
|
12779
|
+
sys.path.insert(0, _repo_root())
|
|
12780
|
+
from install import read_update_history # type: ignore
|
|
12781
|
+
except Exception as exc:
|
|
12782
|
+
return {"success": False, "error": f"history helper unavailable: {exc}", "entries": []}
|
|
12783
|
+
try:
|
|
12784
|
+
return read_update_history(_repo_root(), limit=limit)
|
|
12785
|
+
except Exception as exc:
|
|
12786
|
+
return {"success": False, "error": str(exc), "entries": []}
|
|
12787
|
+
|
|
12788
|
+
|
|
12789
|
+
def _update_preview_payload() -> Dict[str, Any]:
|
|
12790
|
+
"""Render the about-to-apply update for user confirmation.
|
|
12791
|
+
|
|
12792
|
+
Returns release notes, flagged breaking changes, channel, prerelease flag,
|
|
12793
|
+
and the target SHA so the dashboard can show a meaningful modal before
|
|
12794
|
+
`git pull` actually runs.
|
|
12795
|
+
"""
|
|
12796
|
+
try:
|
|
12797
|
+
sys.path.insert(0, _repo_root())
|
|
12798
|
+
from install import preview_update # type: ignore
|
|
12799
|
+
except Exception as exc:
|
|
12800
|
+
return {"success": False, "error": f"preview helper unavailable: {exc}"}
|
|
12801
|
+
try:
|
|
12802
|
+
return preview_update(_repo_root())
|
|
12803
|
+
except Exception as exc:
|
|
12804
|
+
return {"success": False, "error": str(exc)}
|
|
12805
|
+
|
|
12806
|
+
|
|
10519
12807
|
def _update_status_payload(project_root: Optional[str], *, force: bool = False) -> Dict[str, Any]:
|
|
10520
12808
|
current = _mcp_version()
|
|
10521
12809
|
base = {
|
|
@@ -10706,6 +12994,19 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
10706
12994
|
force = (query.get("force") or ["0"])[0].lower() in {"1", "true", "yes"}
|
|
10707
12995
|
self._json(_update_status_payload(self.state.project_root, force=force))
|
|
10708
12996
|
return
|
|
12997
|
+
if path == "/api/update/history":
|
|
12998
|
+
try:
|
|
12999
|
+
limit = int((query.get("limit") or ["20"])[0])
|
|
13000
|
+
except (TypeError, ValueError):
|
|
13001
|
+
limit = 20
|
|
13002
|
+
self._json(_update_history_payload(limit=limit))
|
|
13003
|
+
return
|
|
13004
|
+
if path == "/api/restart_needed":
|
|
13005
|
+
self._json(_read_restart_marker(_repo_root()))
|
|
13006
|
+
return
|
|
13007
|
+
if path == "/api/update/preview":
|
|
13008
|
+
self._json(_update_preview_payload())
|
|
13009
|
+
return
|
|
10709
13010
|
if path == "/api/mcp/status":
|
|
10710
13011
|
self._json(_mcp_status_payload())
|
|
10711
13012
|
return
|
|
@@ -10742,6 +13043,12 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
10742
13043
|
if path == "/api/index/status":
|
|
10743
13044
|
self._json(analysis_index_status(self.state.project_root))
|
|
10744
13045
|
return
|
|
13046
|
+
if path == "/api/coverage":
|
|
13047
|
+
# Standalone readiness rollup — no live Resolve required. The
|
|
13048
|
+
# `coverage_report` action gives target-vs-records detail; this
|
|
13049
|
+
# endpoint summarizes what the analysis directory already knows.
|
|
13050
|
+
self._json(analysis_root_coverage(self.state.project_root))
|
|
13051
|
+
return
|
|
10745
13052
|
if path == "/api/index/query":
|
|
10746
13053
|
q = (query.get("q") or [""])[0]
|
|
10747
13054
|
payload = query_analysis_index(self.state.project_root, q, limit=(query.get("limit") or [20])[0])
|
|
@@ -10749,6 +13056,105 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
10749
13056
|
payload["results"] = _v2_enrich_search_results(self.state.project_root, payload["results"])
|
|
10750
13057
|
self._json(payload)
|
|
10751
13058
|
return
|
|
13059
|
+
# ─── C6 timeline-history surface ───────────────────────────────
|
|
13060
|
+
if path == "/api/timeline_versions":
|
|
13061
|
+
self._json(list_timelines_with_versions(self.state.project_root))
|
|
13062
|
+
return
|
|
13063
|
+
if path.startswith("/api/timeline_versions/"):
|
|
13064
|
+
timeline_name = unquote(path[len("/api/timeline_versions/"):])
|
|
13065
|
+
if not timeline_name:
|
|
13066
|
+
self._json({"success": False, "error": "timeline_name required"}, HTTPStatus.BAD_REQUEST)
|
|
13067
|
+
return
|
|
13068
|
+
self._json(get_timeline_history_payload(self.state.project_root, timeline_name))
|
|
13069
|
+
return
|
|
13070
|
+
if path == "/api/brain_edits/registry":
|
|
13071
|
+
self._json({"success": True, **_brain_edits.read_brain_edits_registry(self.state.project_root)})
|
|
13072
|
+
return
|
|
13073
|
+
if path == "/api/caps/history":
|
|
13074
|
+
try:
|
|
13075
|
+
from src.utils import analysis_caps as _ac
|
|
13076
|
+
days = int((query.get("days") or ["30"])[0])
|
|
13077
|
+
self._json({
|
|
13078
|
+
"success": True,
|
|
13079
|
+
"history": _ac.get_usage_history(project_root=self.state.project_root, days=days),
|
|
13080
|
+
})
|
|
13081
|
+
except Exception as exc:
|
|
13082
|
+
self._json({"success": False, "error": f"{type(exc).__name__}: {exc}"})
|
|
13083
|
+
return
|
|
13084
|
+
if path == "/api/caps/refusals":
|
|
13085
|
+
try:
|
|
13086
|
+
from src.utils import analysis_caps as _ac
|
|
13087
|
+
limit = int((query.get("limit") or ["20"])[0])
|
|
13088
|
+
self._json({
|
|
13089
|
+
"success": True,
|
|
13090
|
+
"events": _ac.get_caps_events(
|
|
13091
|
+
project_root=self.state.project_root,
|
|
13092
|
+
event_type=(query.get("event_type") or ["refusal"])[0] or "refusal",
|
|
13093
|
+
limit=limit,
|
|
13094
|
+
),
|
|
13095
|
+
})
|
|
13096
|
+
except Exception as exc:
|
|
13097
|
+
self._json({"success": False, "error": f"{type(exc).__name__}: {exc}"})
|
|
13098
|
+
return
|
|
13099
|
+
if path == "/api/media_pool_changes":
|
|
13100
|
+
try:
|
|
13101
|
+
from src.utils import media_pool_changes as _mpc
|
|
13102
|
+
limit = int((query.get("limit") or ["50"])[0])
|
|
13103
|
+
self._json({
|
|
13104
|
+
"success": True,
|
|
13105
|
+
"changes": _mpc.get_media_pool_change_history(
|
|
13106
|
+
project_root=self.state.project_root, limit=limit,
|
|
13107
|
+
),
|
|
13108
|
+
})
|
|
13109
|
+
except Exception as exc:
|
|
13110
|
+
self._json({"success": False, "error": f"{type(exc).__name__}: {exc}"})
|
|
13111
|
+
return
|
|
13112
|
+
if path == "/api/runs":
|
|
13113
|
+
try:
|
|
13114
|
+
from src.utils import analysis_runs as _ar
|
|
13115
|
+
limit = int((query.get("limit") or ["50"])[0])
|
|
13116
|
+
self._json({
|
|
13117
|
+
"success": True,
|
|
13118
|
+
"runs": _ar.list_runs(project_root=self.state.project_root, limit=limit),
|
|
13119
|
+
"current_run_id": _ar.current_run_id(),
|
|
13120
|
+
})
|
|
13121
|
+
except Exception as exc:
|
|
13122
|
+
self._json({"success": False, "error": f"{type(exc).__name__}: {exc}"})
|
|
13123
|
+
return
|
|
13124
|
+
if path == "/api/caps":
|
|
13125
|
+
# Effective caps + per-project usage rollup. Proxies into the
|
|
13126
|
+
# media_analysis tool's get_caps action which already does the
|
|
13127
|
+
# preference lookup + DB rollup.
|
|
13128
|
+
try:
|
|
13129
|
+
from src.server import media_analysis as _ma_tool
|
|
13130
|
+
import asyncio
|
|
13131
|
+
result = asyncio.run(_ma_tool("get_caps", params={}))
|
|
13132
|
+
self._json(result)
|
|
13133
|
+
except Exception as exc:
|
|
13134
|
+
self._json({"success": False, "error": f"{type(exc).__name__}: {exc}"})
|
|
13135
|
+
return
|
|
13136
|
+
if path.startswith("/api/timeline_thumbnail/"):
|
|
13137
|
+
rel = unquote(path[len("/api/timeline_thumbnail/"):])
|
|
13138
|
+
# Path is <slug>/<vNN.png>; constrain it to live under _soul/timeline_versions
|
|
13139
|
+
base = os.path.join(self.state.project_root, "_soul", "timeline_versions")
|
|
13140
|
+
full = os.path.realpath(os.path.join(base, rel))
|
|
13141
|
+
if not full.startswith(os.path.realpath(base) + os.sep) and full != os.path.realpath(base):
|
|
13142
|
+
self._json({"success": False, "error": "path escape"}, HTTPStatus.FORBIDDEN)
|
|
13143
|
+
return
|
|
13144
|
+
if not os.path.isfile(full):
|
|
13145
|
+
self._json({"success": False, "error": "not found"}, HTTPStatus.NOT_FOUND)
|
|
13146
|
+
return
|
|
13147
|
+
try:
|
|
13148
|
+
with open(full, "rb") as fh:
|
|
13149
|
+
data = fh.read()
|
|
13150
|
+
self.send_response(200)
|
|
13151
|
+
self.send_header("Content-Type", "image/png")
|
|
13152
|
+
self.send_header("Content-Length", str(len(data)))
|
|
13153
|
+
self.end_headers()
|
|
13154
|
+
self.wfile.write(data)
|
|
13155
|
+
except OSError as exc:
|
|
13156
|
+
self._json({"success": False, "error": str(exc)}, HTTPStatus.INTERNAL_SERVER_ERROR)
|
|
13157
|
+
return
|
|
10752
13158
|
# ─── V2 Review API ──────────────────────────────────────────────
|
|
10753
13159
|
if path == "/api/clips":
|
|
10754
13160
|
self._json(list_analyzed_clips(self.state.project_root))
|
|
@@ -10818,7 +13224,27 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
10818
13224
|
if not _request_is_loopback(self):
|
|
10819
13225
|
self._json({"success": False, "error": "Self-update is only available to loopback clients."}, HTTPStatus.FORBIDDEN)
|
|
10820
13226
|
return
|
|
10821
|
-
|
|
13227
|
+
strategy = (body.get("strategy") or "refuse_on_dirty").strip().lower()
|
|
13228
|
+
if strategy not in {"refuse_on_dirty", "stash_if_needed"}:
|
|
13229
|
+
strategy = "refuse_on_dirty"
|
|
13230
|
+
force_active_jobs = bool(body.get("force_active_jobs") or body.get("force"))
|
|
13231
|
+
self._json(_update_apply_payload(
|
|
13232
|
+
strategy=strategy,
|
|
13233
|
+
force_active_jobs=force_active_jobs,
|
|
13234
|
+
project_root=self.state.project_root,
|
|
13235
|
+
))
|
|
13236
|
+
return
|
|
13237
|
+
if path == "/api/restart_needed/clear":
|
|
13238
|
+
if not _request_is_loopback(self):
|
|
13239
|
+
self._json({"success": False, "error": "Loopback only."}, HTTPStatus.FORBIDDEN)
|
|
13240
|
+
return
|
|
13241
|
+
self._json(_clear_restart_marker(_repo_root()))
|
|
13242
|
+
return
|
|
13243
|
+
if path == "/api/update/rollback":
|
|
13244
|
+
if not _request_is_loopback(self):
|
|
13245
|
+
self._json({"success": False, "error": "Rollback is only available to loopback clients."}, HTTPStatus.FORBIDDEN)
|
|
13246
|
+
return
|
|
13247
|
+
self._json(_update_rollback_payload())
|
|
10822
13248
|
return
|
|
10823
13249
|
if path.startswith("/api/clips/") and path.endswith("/transcript/regenerate"):
|
|
10824
13250
|
if not _request_is_loopback(self):
|
|
@@ -10921,6 +13347,80 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
10921
13347
|
payload = self.state.set_context(body)
|
|
10922
13348
|
self._json(payload, 200 if payload.get("success") else 400)
|
|
10923
13349
|
return
|
|
13350
|
+
# ─── C6 timeline-history write actions (loopback only) ─────────
|
|
13351
|
+
if path == "/api/timeline_versions/action":
|
|
13352
|
+
if not _request_is_loopback(self):
|
|
13353
|
+
self._json({"success": False, "error": "Timeline versioning writes are loopback-only."}, HTTPStatus.FORBIDDEN)
|
|
13354
|
+
return
|
|
13355
|
+
self._json(proxy_timeline_versioning_action(body))
|
|
13356
|
+
return
|
|
13357
|
+
if path == "/api/caps":
|
|
13358
|
+
if not _request_is_loopback(self):
|
|
13359
|
+
self._json({"success": False, "error": "Caps writes are loopback-only."}, HTTPStatus.FORBIDDEN)
|
|
13360
|
+
return
|
|
13361
|
+
try:
|
|
13362
|
+
from src.server import media_analysis as _ma_tool
|
|
13363
|
+
import asyncio
|
|
13364
|
+
result = asyncio.run(_ma_tool("set_caps_preset", params=body))
|
|
13365
|
+
self._json(result, 200 if result.get("success") else 400)
|
|
13366
|
+
except Exception as exc:
|
|
13367
|
+
self._json({"success": False, "error": f"{type(exc).__name__}: {exc}"})
|
|
13368
|
+
return
|
|
13369
|
+
if path == "/api/caps/reset_day":
|
|
13370
|
+
if not _request_is_loopback(self):
|
|
13371
|
+
self._json({"success": False, "error": "Loopback only."}, HTTPStatus.FORBIDDEN)
|
|
13372
|
+
return
|
|
13373
|
+
try:
|
|
13374
|
+
from src.utils import analysis_caps as _ac
|
|
13375
|
+
result = _ac.reset_day_usage(
|
|
13376
|
+
project_root=self.state.project_root,
|
|
13377
|
+
day_bucket=body.get("day_bucket"),
|
|
13378
|
+
)
|
|
13379
|
+
self._json(result)
|
|
13380
|
+
except Exception as exc:
|
|
13381
|
+
self._json({"success": False, "error": f"{type(exc).__name__}: {exc}"})
|
|
13382
|
+
return
|
|
13383
|
+
if path == "/api/runs/begin":
|
|
13384
|
+
if not _request_is_loopback(self):
|
|
13385
|
+
self._json({"success": False, "error": "Loopback only."}, HTTPStatus.FORBIDDEN)
|
|
13386
|
+
return
|
|
13387
|
+
try:
|
|
13388
|
+
from src.utils import analysis_runs as _ar
|
|
13389
|
+
result = _ar.begin_run(
|
|
13390
|
+
project_root=self.state.project_root,
|
|
13391
|
+
label=body.get("label"),
|
|
13392
|
+
initiator=body.get("initiator") or "dashboard",
|
|
13393
|
+
)
|
|
13394
|
+
self._json(result)
|
|
13395
|
+
except Exception as exc:
|
|
13396
|
+
self._json({"success": False, "error": f"{type(exc).__name__}: {exc}"})
|
|
13397
|
+
return
|
|
13398
|
+
if path == "/api/runs/end":
|
|
13399
|
+
if not _request_is_loopback(self):
|
|
13400
|
+
self._json({"success": False, "error": "Loopback only."}, HTTPStatus.FORBIDDEN)
|
|
13401
|
+
return
|
|
13402
|
+
try:
|
|
13403
|
+
from src.utils import analysis_runs as _ar
|
|
13404
|
+
result = _ar.end_run(
|
|
13405
|
+
project_root=self.state.project_root,
|
|
13406
|
+
analysis_run_id=body.get("analysis_run_id"),
|
|
13407
|
+
)
|
|
13408
|
+
self._json(result)
|
|
13409
|
+
except Exception as exc:
|
|
13410
|
+
self._json({"success": False, "error": f"{type(exc).__name__}: {exc}"})
|
|
13411
|
+
return
|
|
13412
|
+
if path == "/api/update/channel":
|
|
13413
|
+
if not _request_is_loopback(self):
|
|
13414
|
+
self._json({"success": False, "error": "Loopback only."}, HTTPStatus.FORBIDDEN)
|
|
13415
|
+
return
|
|
13416
|
+
channel = (body.get("channel") or "stable").strip().lower()
|
|
13417
|
+
if channel not in {"stable", "beta", "dev"}:
|
|
13418
|
+
self._json({"success": False, "error": "channel must be stable | beta | dev"}, HTTPStatus.BAD_REQUEST)
|
|
13419
|
+
return
|
|
13420
|
+
os.environ["DAVINCI_RESOLVE_MCP_UPDATE_CHANNEL"] = channel
|
|
13421
|
+
self._json({"success": True, "channel": channel,
|
|
13422
|
+
"note": "Set for this process; persist via env var to survive restart."})
|
|
13423
|
+
return
|
|
10924
13424
|
# ─── V2 Review API (writes) ─────────────────────────────────────
|
|
10925
13425
|
if path == "/api/panel_state":
|
|
10926
13426
|
merge = body.pop("__merge__", True)
|