cctally 1.64.0 → 1.65.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 +17 -0
- package/bin/_cctally_cache.py +60 -19
- package/bin/_cctally_dashboard.py +364 -38
- package/bin/_cctally_tui.py +445 -236
- package/bin/_cctally_update.py +5 -0
- package/bin/_lib_conversation_query.py +349 -36
- package/bin/_lib_perf.py +180 -0
- package/bin/_lib_transcript_access.py +23 -0
- package/dashboard/static/assets/index-CJTUCpKt.js +80 -0
- package/dashboard/static/assets/index-DQWNrIqu.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +2 -1
- package/dashboard/static/assets/index-0jzYm75p.css +0 -1
- package/dashboard/static/assets/index-D_Ylyqsf.js +0 -80
package/bin/_cctally_tui.py
CHANGED
|
@@ -222,6 +222,11 @@ from _lib_display_tz import (
|
|
|
222
222
|
)
|
|
223
223
|
from _lib_aggregators import _aggregate_monthly
|
|
224
224
|
from _lib_fmt import stable_sum
|
|
225
|
+
# Opt-in backend phase-instrumentation collector (issue #276, Session A). Pure
|
|
226
|
+
# stdlib leaf; near-noop when CCTALLY_PERF_TRACE is unset (phase() returns a
|
|
227
|
+
# shared no-op singleton), so the _tui_build_snapshot seam wraps below cost
|
|
228
|
+
# nothing on the default path.
|
|
229
|
+
import _lib_perf as _perf
|
|
225
230
|
|
|
226
231
|
|
|
227
232
|
# === Module-level back-ref shims for helpers that STAY in bin/cctally ======
|
|
@@ -1104,6 +1109,22 @@ class DataSnapshot:
|
|
|
1104
1109
|
# keep working, and both are carried forward on a sync crash (Codex F6).
|
|
1105
1110
|
doctor_payload: dict | None = None
|
|
1106
1111
|
envelope_precompute: dict | None = None
|
|
1112
|
+
# ---- #278 Theme A: first-paint hydration latch ----
|
|
1113
|
+
# ``True`` ONLY on data that is genuinely still being assembled — the
|
|
1114
|
+
# cheap first-paint seed (``_dashboard_initial_snapshot`` on a normal
|
|
1115
|
+
# launch) and A2's throttled partial republishes (set on a
|
|
1116
|
+
# ``dataclasses.replace`` copy for the PUBLISH only, never dirtying the
|
|
1117
|
+
# object the dispatch memo retains). Every path that yields
|
|
1118
|
+
# complete/stable data leaves/forces it ``False``: a fresh full build is
|
|
1119
|
+
# ``False`` for free (this default), and each ``dataclasses.replace``
|
|
1120
|
+
# clone site that copies a prior snapshot's value (idle short-circuit,
|
|
1121
|
+
# update-check republish, run-sync/settings republish) forces it back to
|
|
1122
|
+
# ``False``. Serialized into the dashboard envelope by
|
|
1123
|
+
# ``snapshot_to_envelope`` (additive key ``"hydrating"``); consumers
|
|
1124
|
+
# tolerate unknown keys, and it appears in NO ``--json``/CLI surface.
|
|
1125
|
+
# Placed LAST with a default so positional fixture constructors keep
|
|
1126
|
+
# working.
|
|
1127
|
+
hydrating: bool = False
|
|
1107
1128
|
|
|
1108
1129
|
@classmethod
|
|
1109
1130
|
def synthesize_for_marketing(cls, *, as_of_iso: str) -> "DataSnapshot":
|
|
@@ -2092,6 +2113,13 @@ def _tui_build_snapshot(
|
|
|
2092
2113
|
"""
|
|
2093
2114
|
import time
|
|
2094
2115
|
now_utc = now_utc or dt.datetime.now(dt.timezone.utc)
|
|
2116
|
+
# #276 perf: reset any prior tree on this thread, then bracket the whole
|
|
2117
|
+
# snapshot build as the "snapshot" root phase. Opened via the context-
|
|
2118
|
+
# manager protocol so the long body below is not reindented; closed +
|
|
2119
|
+
# stashed at each return. Near-noop when CCTALLY_PERF_TRACE is unset.
|
|
2120
|
+
_perf.reset_thread()
|
|
2121
|
+
_p_snapshot = _perf.phase("snapshot")
|
|
2122
|
+
_p_snapshot.__enter__()
|
|
2095
2123
|
# Read config ONCE per rebuild and reuse it for the display-tz resolution
|
|
2096
2124
|
# here AND the envelope precompute below (#268 M4) — the envelope used to
|
|
2097
2125
|
# call ``load_config()`` twice per SSE client per tick.
|
|
@@ -2134,15 +2162,17 @@ def _tui_build_snapshot(
|
|
|
2134
2162
|
# builders always read pure regardless (skip_sync is reassigned to
|
|
2135
2163
|
# True below), so --no-sync means "no ingest, still read".
|
|
2136
2164
|
do_ingest = not skip_sync
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2165
|
+
with _perf.phase("sync") as _p_sync:
|
|
2166
|
+
_p_sync.set_meta(ingest=do_ingest)
|
|
2167
|
+
if do_ingest:
|
|
2140
2168
|
try:
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2169
|
+
cache_conn = _cctally().open_cache_db()
|
|
2170
|
+
try:
|
|
2171
|
+
sync_cache(cache_conn)
|
|
2172
|
+
finally:
|
|
2173
|
+
cache_conn.close()
|
|
2174
|
+
except Exception as exc:
|
|
2175
|
+
errors.append(f"sync-cache: {exc}")
|
|
2146
2176
|
# Force pure reads for every view builder below, independent of the
|
|
2147
2177
|
# caller's flag: the single ingest above is the only glob per tick.
|
|
2148
2178
|
skip_sync = True
|
|
@@ -2193,11 +2223,12 @@ def _tui_build_snapshot(
|
|
|
2193
2223
|
dispatch_key = None
|
|
2194
2224
|
if precompute_envelope:
|
|
2195
2225
|
dispatch_sig = None
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2226
|
+
with _perf.phase("signature"):
|
|
2227
|
+
try:
|
|
2228
|
+
dispatch_sig = _tui_compute_dispatch_signature(conn)
|
|
2229
|
+
except Exception as exc:
|
|
2230
|
+
errors.append(f"dispatch-signature: {exc}")
|
|
2231
|
+
dispatch_sig = None
|
|
2201
2232
|
if dispatch_sig is not None:
|
|
2202
2233
|
# The idle decision keys on the DB signature AND a render key
|
|
2203
2234
|
# that captures the config-derived inputs the composite
|
|
@@ -2220,13 +2251,20 @@ def _tui_build_snapshot(
|
|
|
2220
2251
|
and dispatch_key == prior_key
|
|
2221
2252
|
and not _snapshot_period_rolled_over(
|
|
2222
2253
|
prior_snap, now_utc, _build_display_tz)):
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2254
|
+
with _perf.phase("idle-decision"):
|
|
2255
|
+
idle_snap = _tui_build_idle_snapshot(
|
|
2256
|
+
prior_snap, now_utc=now_utc,
|
|
2257
|
+
precompute_envelope=precompute_envelope,
|
|
2258
|
+
runtime_bind=runtime_bind, raw_config=raw_config,
|
|
2259
|
+
errors=errors,
|
|
2260
|
+
)
|
|
2261
|
+
_sc.store_dispatch_state(dispatch_key, idle_snap)
|
|
2262
|
+
_p_snapshot.__exit__(None, None, None)
|
|
2263
|
+
if _perf.enabled():
|
|
2264
|
+
_perf.stash_last(
|
|
2265
|
+
_perf.current_root(), generation=None,
|
|
2266
|
+
generated_at=now_utc.isoformat(),
|
|
2267
|
+
)
|
|
2230
2268
|
return idle_snap
|
|
2231
2269
|
# ── #269 M3.1: non-idle rebuild — reconcile the shared
|
|
2232
2270
|
# per-weekref immutable-cost cache ONCE here (spec §4/§6),
|
|
@@ -2241,39 +2279,48 @@ def _tui_build_snapshot(
|
|
|
2241
2279
|
try:
|
|
2242
2280
|
_rc_cache_conn = _cctally().open_cache_db()
|
|
2243
2281
|
try:
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2282
|
+
with _perf.phase("reconcile.weekref") as _pr:
|
|
2283
|
+
_pr.set_meta(hit=False)
|
|
2284
|
+
_sc.reconcile_weekref_cache(
|
|
2285
|
+
_rc_cache_conn,
|
|
2286
|
+
max_entry_id=dispatch_sig.max_entry_id,
|
|
2287
|
+
max_mutation_seq=dispatch_sig.entry_mutation_seq,
|
|
2288
|
+
reset_sig=dispatch_sig.reset_sig,
|
|
2289
|
+
)
|
|
2290
|
+
use_weekref_cost_cache = True
|
|
2291
|
+
_pr.set_meta(hit=True)
|
|
2251
2292
|
# #269 M4.5: reconcile the projects-envelope cache with
|
|
2252
2293
|
# the SAME short-lived cache conn (session_files_sig +
|
|
2253
2294
|
# the new-entry watermark both live in cache.db). Only
|
|
2254
2295
|
# after this succeeds does `_build_projects_envelope`
|
|
2255
2296
|
# opt into the cache below.
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2297
|
+
with _perf.phase("reconcile.projects_env") as _pr:
|
|
2298
|
+
_pr.set_meta(hit=False)
|
|
2299
|
+
_sc.reconcile_projects_env_cache(
|
|
2300
|
+
_rc_cache_conn,
|
|
2301
|
+
max_entry_id=dispatch_sig.max_entry_id,
|
|
2302
|
+
max_mutation_seq=dispatch_sig.entry_mutation_seq,
|
|
2303
|
+
max_wus_id=dispatch_sig.max_wus_id,
|
|
2304
|
+
sf_sig=_sc.session_files_sig(_rc_cache_conn),
|
|
2305
|
+
)
|
|
2306
|
+
use_projects_env_cache = True
|
|
2307
|
+
_pr.set_meta(hit=True)
|
|
2264
2308
|
# #271 M3: reconcile the Bug-K pre-credit segment cache
|
|
2265
2309
|
# with the SAME short-lived cache conn (its watermark
|
|
2266
2310
|
# query lives in cache.db), using the dispatch-signature
|
|
2267
2311
|
# legs already computed. Only after this succeeds does
|
|
2268
2312
|
# `_dashboard_build_weekly_periods` opt into the cache
|
|
2269
2313
|
# below (`use_bugk_segment_cache=True`).
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2314
|
+
with _perf.phase("reconcile.bugk") as _pr:
|
|
2315
|
+
_pr.set_meta(hit=False)
|
|
2316
|
+
_sc.reconcile_bugk_cache(
|
|
2317
|
+
_rc_cache_conn,
|
|
2318
|
+
max_entry_id=dispatch_sig.max_entry_id,
|
|
2319
|
+
max_mutation_seq=dispatch_sig.entry_mutation_seq,
|
|
2320
|
+
reset_sig=dispatch_sig.reset_sig,
|
|
2321
|
+
)
|
|
2322
|
+
use_bugk_segment_cache = True
|
|
2323
|
+
_pr.set_meta(hit=True)
|
|
2277
2324
|
# #272: reconcile the cache-report per-day cache with the
|
|
2278
2325
|
# SAME short-lived cache conn (its watermark query +
|
|
2279
2326
|
# session_files_sig both live in cache.db), using the
|
|
@@ -2282,95 +2329,104 @@ def _tui_build_snapshot(
|
|
|
2282
2329
|
# `tz_key` full-clears on a display-tz change (every
|
|
2283
2330
|
# calendar-day key shifts). Only after this succeeds does
|
|
2284
2331
|
# `build_cache_report_snapshot` opt into the cache below.
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2332
|
+
with _perf.phase("reconcile.cache_report") as _pr:
|
|
2333
|
+
_pr.set_meta(hit=False)
|
|
2334
|
+
_sc.reconcile_cache_report_cache(
|
|
2335
|
+
_rc_cache_conn,
|
|
2336
|
+
max_entry_id=dispatch_sig.max_entry_id,
|
|
2337
|
+
max_mutation_seq=dispatch_sig.entry_mutation_seq,
|
|
2338
|
+
reset_sig=dispatch_sig.reset_sig,
|
|
2339
|
+
sf_sig=_sc.session_files_sig(_rc_cache_conn),
|
|
2340
|
+
# `_load_sibling` is the canonical idempotent
|
|
2341
|
+
# sibling-access idiom (it returns the already-loaded
|
|
2342
|
+
# cctally-bound kernel instance). `_lib_cache_report`
|
|
2343
|
+
# is loaded eagerly at import (bin/cctally), so this
|
|
2344
|
+
# is a plain re-fetch of a populated `sys.modules`
|
|
2345
|
+
# entry — NOT a first-tick KeyError guard.
|
|
2346
|
+
bucket_tz=_cctally()._load_sibling(
|
|
2347
|
+
"_lib_cache_report"
|
|
2348
|
+
)._resolve_bucket_tz(_build_display_tz),
|
|
2349
|
+
tz_key=str(_build_display_tz),
|
|
2350
|
+
)
|
|
2351
|
+
use_cache_report_cache = True
|
|
2352
|
+
_pr.set_meta(hit=True)
|
|
2303
2353
|
finally:
|
|
2304
2354
|
_rc_cache_conn.close()
|
|
2305
2355
|
except Exception as exc:
|
|
2306
2356
|
errors.append(f"snapshot-cache-reconcile: {exc}")
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2357
|
+
with _perf.phase("build.current_week"):
|
|
2358
|
+
try:
|
|
2359
|
+
cw = _tui_build_current_week(conn, now_utc, skip_sync=skip_sync)
|
|
2360
|
+
except Exception as exc:
|
|
2361
|
+
errors.append(f"current-week: {exc}")
|
|
2311
2362
|
fc_view = None
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2363
|
+
with _perf.phase("build.forecast"):
|
|
2364
|
+
try:
|
|
2365
|
+
# Issue #57: build the ForecastView once so we capture both
|
|
2366
|
+
# the legacy ``ForecastOutput`` (for ``snap.forecast``, which
|
|
2367
|
+
# the many TUI panel consumers still read) and the surface
|
|
2368
|
+
# fields the envelope adapter used to re-derive inline.
|
|
2369
|
+
fc_view = _tui_build_forecast_view(
|
|
2370
|
+
conn, now_utc, skip_sync=skip_sync,
|
|
2371
|
+
use_weekref_cost_cache=use_weekref_cost_cache,
|
|
2372
|
+
)
|
|
2373
|
+
fc = fc_view.output if fc_view is not None else None
|
|
2374
|
+
except Exception as exc:
|
|
2375
|
+
errors.append(f"forecast: {exc}")
|
|
2324
2376
|
# Trend: source from build_trend_view so we capture the 3-sample
|
|
2325
2377
|
# avg_dollars_per_pct alongside the rows. The TUI build path
|
|
2326
2378
|
# historically called _tui_build_trend (which now wraps the
|
|
2327
2379
|
# builder); calling the builder directly here saves one
|
|
2328
2380
|
# `_aggregate_*` round-trip.
|
|
2329
2381
|
trend_avg_dpp = None
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2382
|
+
with _perf.phase("build.trend"):
|
|
2383
|
+
try:
|
|
2384
|
+
c = _cctally()
|
|
2385
|
+
_trend_view = c.build_trend_view(
|
|
2386
|
+
conn, now_utc=now_utc, n=8, display_tz=_build_display_tz,
|
|
2387
|
+
skip_sync=skip_sync,
|
|
2388
|
+
use_weekref_cost_cache=use_weekref_cost_cache,
|
|
2389
|
+
)
|
|
2390
|
+
trend = list(_trend_view.rows)
|
|
2391
|
+
trend_avg_dpp = _trend_view.avg_dollars_per_pct
|
|
2392
|
+
except Exception as exc:
|
|
2393
|
+
errors.append(f"trend: {exc}")
|
|
2394
|
+
with _perf.phase("build.sessions"):
|
|
2395
|
+
try:
|
|
2396
|
+
# The sessions aggregator goes through
|
|
2397
|
+
# `get_claude_session_entries`, which runs `sync_cache` unless
|
|
2398
|
+
# `skip_sync=True` is threaded through. Honor the caller's
|
|
2399
|
+
# intent so `--no-sync` and the initial cache-only paint
|
|
2400
|
+
# both avoid ingest latency/lock contention.
|
|
2401
|
+
sessions = _tui_build_sessions(
|
|
2402
|
+
now_utc, skip_sync=skip_sync, use_session_cache=True,
|
|
2403
|
+
)
|
|
2404
|
+
except Exception as exc:
|
|
2405
|
+
errors.append(f"sessions: {exc}")
|
|
2352
2406
|
# ---- v2 additions ----
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2407
|
+
with _perf.phase("build.milestones"):
|
|
2408
|
+
try:
|
|
2409
|
+
if cw is not None:
|
|
2410
|
+
milestones = _tui_build_percent_milestones(conn)
|
|
2411
|
+
except Exception as exc:
|
|
2412
|
+
errors.append(f"milestones: {exc}")
|
|
2358
2413
|
history: list = []
|
|
2359
2414
|
history_median_dpp: "float | None" = None
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2415
|
+
with _perf.phase("build.weekly_history"):
|
|
2416
|
+
try:
|
|
2417
|
+
# Issue #59: build the full TrendView so we capture the
|
|
2418
|
+
# pre-computed 4-week-median-non-current scalar alongside
|
|
2419
|
+
# the row list; the dashboard envelope adapter surfaces
|
|
2420
|
+
# the scalar as ``trend.history_median_dpp`` so
|
|
2421
|
+
# TrendModal.tsx stops re-deriving it client-side.
|
|
2422
|
+
history_view = _tui_build_weekly_history_view(
|
|
2423
|
+
conn, now_utc, skip_sync=skip_sync, display_tz=_build_display_tz,
|
|
2424
|
+
use_weekref_cost_cache=use_weekref_cost_cache,
|
|
2425
|
+
)
|
|
2426
|
+
history = list(history_view.rows)
|
|
2427
|
+
history_median_dpp = history_view.median_dpp_non_current_4w
|
|
2428
|
+
except Exception as exc:
|
|
2429
|
+
errors.append(f"weekly-history: {exc}")
|
|
2374
2430
|
# ---- v2.1 additions: dashboard Weekly / Monthly panels ----
|
|
2375
2431
|
# Sync-thread view-model totals (spec §6.6): sum directly over
|
|
2376
2432
|
# the panel rows the dashboard ACTUALLY renders. The previous
|
|
@@ -2386,23 +2442,24 @@ def _tui_build_snapshot(
|
|
|
2386
2442
|
# ``test_weekly_envelope_total_matches_sum_of_visible_rows``.
|
|
2387
2443
|
weekly_total_cost_usd = 0.0
|
|
2388
2444
|
weekly_total_tokens = 0
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2445
|
+
with _perf.phase("build.weekly_periods"):
|
|
2446
|
+
try:
|
|
2447
|
+
weekly_periods = _dashboard_build_weekly_periods(
|
|
2448
|
+
conn, now_utc, n=12, skip_sync=skip_sync,
|
|
2449
|
+
use_group_a_cache=True,
|
|
2450
|
+
use_bugk_segment_cache=use_bugk_segment_cache,
|
|
2451
|
+
)
|
|
2452
|
+
# ``stable_sum`` (math.fsum) returns float ``0.0`` on empty rows,
|
|
2453
|
+
# so the envelope stays byte-stable with the pre-fix ``0.0`` shape
|
|
2454
|
+
# (the dashboard fixture goldens assert exact JSON match).
|
|
2455
|
+
weekly_total_cost_usd = stable_sum(
|
|
2456
|
+
r.cost_usd for r in weekly_periods
|
|
2457
|
+
)
|
|
2458
|
+
weekly_total_tokens = sum(
|
|
2459
|
+
(r.total_tokens for r in weekly_periods), 0,
|
|
2460
|
+
)
|
|
2461
|
+
except Exception as exc:
|
|
2462
|
+
errors.append(f"weekly-periods: {exc}")
|
|
2406
2463
|
# Sync-thread view-model totals (spec §6.6): sum-over-visible-rows
|
|
2407
2464
|
# (same invariant as weekly above). Monthly has no Bug-K analogue,
|
|
2408
2465
|
# but coupling the footer total to the panel-row source of truth
|
|
@@ -2410,20 +2467,21 @@ def _tui_build_snapshot(
|
|
|
2410
2467
|
# same arithmetic with no behavioral upside.
|
|
2411
2468
|
monthly_total_cost_usd = 0.0
|
|
2412
2469
|
monthly_total_tokens = 0
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2470
|
+
with _perf.phase("build.monthly_periods"):
|
|
2471
|
+
try:
|
|
2472
|
+
monthly_periods = _dashboard_build_monthly_periods(
|
|
2473
|
+
conn, now_utc, n=12, skip_sync=skip_sync,
|
|
2474
|
+
use_group_a_cache=True,
|
|
2475
|
+
display_tz=_build_display_tz,
|
|
2476
|
+
)
|
|
2477
|
+
monthly_total_cost_usd = stable_sum(
|
|
2478
|
+
r.cost_usd for r in monthly_periods
|
|
2479
|
+
)
|
|
2480
|
+
monthly_total_tokens = sum(
|
|
2481
|
+
(r.total_tokens for r in monthly_periods), 0,
|
|
2482
|
+
)
|
|
2483
|
+
except Exception as exc:
|
|
2484
|
+
errors.append(f"monthly-periods: {exc}")
|
|
2427
2485
|
# ---- v2.2 additions: dashboard Blocks / Daily panels ----
|
|
2428
2486
|
# Issue #56: build the BlocksView once and read both rows
|
|
2429
2487
|
# (presentation) and totals (envelope scalars) from the same
|
|
@@ -2432,20 +2490,21 @@ def _tui_build_snapshot(
|
|
|
2432
2490
|
# kept as a thin shim for monkeypatch surfaces).
|
|
2433
2491
|
blocks_total_cost_usd = 0.0
|
|
2434
2492
|
blocks_total_tokens = 0
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2493
|
+
with _perf.phase("build.blocks"):
|
|
2494
|
+
try:
|
|
2495
|
+
if cw is not None:
|
|
2496
|
+
_blocks_view = _dashboard_build_blocks_view(
|
|
2497
|
+
conn, now_utc,
|
|
2498
|
+
week_start_at=cw.week_start_at,
|
|
2499
|
+
week_end_at=cw.week_end_at,
|
|
2500
|
+
skip_sync=skip_sync,
|
|
2501
|
+
display_tz=_build_display_tz,
|
|
2502
|
+
)
|
|
2503
|
+
blocks_panel = list(_blocks_view.rows)
|
|
2504
|
+
blocks_total_cost_usd = _blocks_view.total_cost_usd
|
|
2505
|
+
blocks_total_tokens = _blocks_view.total_tokens
|
|
2506
|
+
except Exception as exc:
|
|
2507
|
+
errors.append(f"blocks-panel: {exc}")
|
|
2449
2508
|
# Sync-thread view-model totals (Bundle 1 / spec §6.6):
|
|
2450
2509
|
# sum-over-visible-rows (same invariant as weekly/monthly above).
|
|
2451
2510
|
# Gap days in the materialized panel carry ``cost_usd=0.0`` /
|
|
@@ -2454,42 +2513,45 @@ def _tui_build_snapshot(
|
|
|
2454
2513
|
# ``build_daily_view`` pass that did the same arithmetic.
|
|
2455
2514
|
daily_total_cost_usd = 0.0
|
|
2456
2515
|
daily_total_tokens = 0
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2516
|
+
with _perf.phase("build.daily"):
|
|
2517
|
+
try:
|
|
2518
|
+
daily_panel = _dashboard_build_daily_panel(
|
|
2519
|
+
conn, now_utc, n=30, skip_sync=skip_sync,
|
|
2520
|
+
use_group_a_cache=True,
|
|
2521
|
+
display_tz=_build_display_tz,
|
|
2522
|
+
)
|
|
2523
|
+
daily_total_cost_usd = stable_sum(
|
|
2524
|
+
r.cost_usd for r in daily_panel
|
|
2525
|
+
)
|
|
2526
|
+
daily_total_tokens = sum(
|
|
2527
|
+
(r.total_tokens for r in daily_panel), 0,
|
|
2528
|
+
)
|
|
2529
|
+
except Exception as exc:
|
|
2530
|
+
errors.append(f"daily-panel: {exc}")
|
|
2471
2531
|
# ---- threshold-actions T5: alerts envelope array ----
|
|
2472
2532
|
# Precomputed at sync time so `snapshot_to_envelope` stays a pure
|
|
2473
2533
|
# renderer (no DB I/O on the dashboard hot path; mirrors how
|
|
2474
2534
|
# `current_week.five_hour_block` is precomputed via
|
|
2475
2535
|
# `_select_current_block_for_envelope`).
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2536
|
+
with _perf.phase("build.alerts"):
|
|
2537
|
+
try:
|
|
2538
|
+
alerts = _build_alerts_envelope_array(conn)
|
|
2539
|
+
except Exception as exc:
|
|
2540
|
+
errors.append(f"alerts: {exc}")
|
|
2480
2541
|
# ---- 5h in-place credit (v1.7.x) ----
|
|
2481
2542
|
# Load 5h milestones (pre + post credit) for the current
|
|
2482
2543
|
# block's window so CurrentWeekModal can render a merged
|
|
2483
2544
|
# chronological timeline alongside its weekly milestones.
|
|
2484
2545
|
# Spec §5.3 (Codex r1 finding 3).
|
|
2485
2546
|
fh_milestones: list[dict] = []
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2547
|
+
with _perf.phase("build.five_hour_milestones"):
|
|
2548
|
+
try:
|
|
2549
|
+
win_key = None
|
|
2550
|
+
if cw is not None and isinstance(cw.five_hour_block, dict):
|
|
2551
|
+
win_key = cw.five_hour_block.get("five_hour_window_key")
|
|
2552
|
+
fh_milestones = _tui_build_five_hour_milestones(conn, win_key)
|
|
2553
|
+
except Exception as exc:
|
|
2554
|
+
errors.append(f"five-hour-milestones: {exc}")
|
|
2493
2555
|
# ---- Projects panel + modal envelope (spec §5.2, plan Task 1) -----
|
|
2494
2556
|
# Per-tick aggregation lives on the sync thread; the dashboard's
|
|
2495
2557
|
# pure ``snapshot_to_envelope`` reads ``snap.projects_envelope``
|
|
@@ -2504,6 +2566,11 @@ def _tui_build_snapshot(
|
|
|
2504
2566
|
# all three tables. ATTACH/DETACH is cheap and scoped to this
|
|
2505
2567
|
# sub-build; no schema migration / lock acquisition is needed.
|
|
2506
2568
|
projects_envelope_block: dict | None = None
|
|
2569
|
+
# #276 perf: time the projects-envelope build (ATTACH + walk + DETACH).
|
|
2570
|
+
# CM protocol, not a ``with`` block, to avoid reindenting the ~40-line
|
|
2571
|
+
# try/except/finally; the broad except + finally guarantee no escape.
|
|
2572
|
+
_p_pe = _perf.phase("build.projects_envelope")
|
|
2573
|
+
_p_pe.__enter__()
|
|
2507
2574
|
try:
|
|
2508
2575
|
c = _cctally()
|
|
2509
2576
|
cache_db_path = _cctally_core.CACHE_DB_PATH
|
|
@@ -2545,6 +2612,7 @@ def _tui_build_snapshot(
|
|
|
2545
2612
|
conn.execute("DETACH DATABASE cache_db")
|
|
2546
2613
|
except Exception:
|
|
2547
2614
|
pass
|
|
2615
|
+
_p_pe.__exit__(None, None, None)
|
|
2548
2616
|
# Late-bind disambiguated `project_key` onto each SessionsPanel
|
|
2549
2617
|
# row so the SessionsPanel → ProjectsModal cross-nav (spec §4.1)
|
|
2550
2618
|
# routes by the same identity the Projects envelope emits.
|
|
@@ -2606,28 +2674,29 @@ def _tui_build_snapshot(
|
|
|
2606
2674
|
# on the DataSnapshot field and the client renders the empty
|
|
2607
2675
|
# state.
|
|
2608
2676
|
cache_report_block = None
|
|
2609
|
-
|
|
2610
|
-
cfg_cr = load_config().get("cache_report") or {}
|
|
2611
|
-
threshold_raw = cfg_cr.get("anomaly_threshold_pp", 15)
|
|
2677
|
+
with _perf.phase("build.cache_report"):
|
|
2612
2678
|
try:
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2679
|
+
cfg_cr = load_config().get("cache_report") or {}
|
|
2680
|
+
threshold_raw = cfg_cr.get("anomaly_threshold_pp", 15)
|
|
2681
|
+
try:
|
|
2682
|
+
threshold_pp = int(threshold_raw)
|
|
2683
|
+
except (TypeError, ValueError):
|
|
2684
|
+
threshold_pp = 15
|
|
2685
|
+
if threshold_pp < 1 or threshold_pp > 100:
|
|
2686
|
+
threshold_pp = 15
|
|
2687
|
+
_dash_mod = sys.modules["_cctally_dashboard"]
|
|
2688
|
+
_bcr = _dash_mod.build_cache_report_snapshot
|
|
2689
|
+
cache_report_block = _bcr(
|
|
2690
|
+
now_utc=now_utc,
|
|
2691
|
+
anomaly_threshold_pp=threshold_pp,
|
|
2692
|
+
# Hardcoded for v1; F10 tracks lifting via cache_report.anomaly_window_days config.
|
|
2693
|
+
anomaly_window_days=_dash_mod.CACHE_REPORT_ANOMALY_WINDOW_DAYS,
|
|
2694
|
+
display_tz=_build_display_tz,
|
|
2695
|
+
skip_sync=skip_sync,
|
|
2696
|
+
use_cache_report_cache=use_cache_report_cache,
|
|
2697
|
+
)
|
|
2698
|
+
except Exception as exc:
|
|
2699
|
+
errors.append(f"cache-report: {exc}")
|
|
2631
2700
|
|
|
2632
2701
|
# ---- #268 M4: doctor / config / update-state precompute (spec §6) ----
|
|
2633
2702
|
# Precompute the envelope's doctor / config / update-state reads ONCE
|
|
@@ -2639,18 +2708,20 @@ def _tui_build_snapshot(
|
|
|
2639
2708
|
doctor_payload_block: "dict | None" = None
|
|
2640
2709
|
envelope_precompute_block: "dict | None" = None
|
|
2641
2710
|
if precompute_envelope:
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2711
|
+
with _perf.phase("doctor"):
|
|
2712
|
+
try:
|
|
2713
|
+
doctor_payload_block = _tui_precompute_doctor_payload(
|
|
2714
|
+
now_utc, runtime_bind,
|
|
2715
|
+
)
|
|
2716
|
+
except Exception as exc:
|
|
2717
|
+
errors.append(f"doctor-precompute: {exc}")
|
|
2718
|
+
with _perf.phase("envelope.precompute"):
|
|
2719
|
+
try:
|
|
2720
|
+
envelope_precompute_block = _tui_precompute_envelope_config(
|
|
2721
|
+
raw_config,
|
|
2722
|
+
)
|
|
2723
|
+
except Exception as exc:
|
|
2724
|
+
errors.append(f"envelope-precompute: {exc}")
|
|
2654
2725
|
|
|
2655
2726
|
snap = DataSnapshot(
|
|
2656
2727
|
current_week=cw,
|
|
@@ -2692,6 +2763,16 @@ def _tui_build_snapshot(
|
|
|
2692
2763
|
_cctally()._load_sibling("_lib_snapshot_cache").store_dispatch_state(
|
|
2693
2764
|
dispatch_key, snap,
|
|
2694
2765
|
)
|
|
2766
|
+
# #276 perf: close the "snapshot" root and freeze the completed tree
|
|
2767
|
+
# into the process-global slot for the loopback /api/debug/backend
|
|
2768
|
+
# endpoint. Whole-dict atomic assignment; never mutated after. No-op
|
|
2769
|
+
# when tracing is off.
|
|
2770
|
+
_p_snapshot.__exit__(None, None, None)
|
|
2771
|
+
if _perf.enabled():
|
|
2772
|
+
_perf.stash_last(
|
|
2773
|
+
_perf.current_root(), generation=None,
|
|
2774
|
+
generated_at=now_utc.isoformat(),
|
|
2775
|
+
)
|
|
2695
2776
|
return snap
|
|
2696
2777
|
finally:
|
|
2697
2778
|
conn.close()
|
|
@@ -2894,6 +2975,10 @@ def _tui_build_idle_snapshot(prior, *, now_utc, precompute_envelope,
|
|
|
2894
2975
|
last_sync_error=("; ".join(errors) if errors else None),
|
|
2895
2976
|
doctor_payload=doctor_payload,
|
|
2896
2977
|
envelope_precompute=envelope_precompute,
|
|
2978
|
+
# #278 §1.4.1: an idle snapshot means the data-version signature is
|
|
2979
|
+
# unchanged (data stable) → force the hydration latch clear even if
|
|
2980
|
+
# ``prior`` was a hydrating seed/partial.
|
|
2981
|
+
hydrating=False,
|
|
2897
2982
|
)
|
|
2898
2983
|
|
|
2899
2984
|
|
|
@@ -5090,6 +5175,69 @@ def _tui_refresh_interval_type(s: str) -> float:
|
|
|
5090
5175
|
return v
|
|
5091
5176
|
|
|
5092
5177
|
|
|
5178
|
+
# ── #278 Theme A (A2): progressive first-run ingest fill ─────────────────────
|
|
5179
|
+
# A first-run / long-gap dashboard sync walks many files and takes seconds to
|
|
5180
|
+
# tens-of-seconds; before this it published exactly once at the end (empty →
|
|
5181
|
+
# single jump). A2 wires the existing ``sync_cache(progress=…)`` seam to a
|
|
5182
|
+
# THROTTLED partial republish so the heavy panels fill progressively. It is
|
|
5183
|
+
# self-limiting to first-run: a warm returning user's sync finishes under T, so
|
|
5184
|
+
# the throttle never fires and A2 is a pure no-op (exactly one publish). Not
|
|
5185
|
+
# user-configurable (YAGNI).
|
|
5186
|
+
_A2_PARTIAL_THROTTLE_S = 2.0 # T
|
|
5187
|
+
|
|
5188
|
+
|
|
5189
|
+
class _A2ThrottleClock:
|
|
5190
|
+
"""Completion-measured throttle for A2 partial republishes.
|
|
5191
|
+
|
|
5192
|
+
``should_fire(now)`` is True at most once per ``interval_s``, measured from
|
|
5193
|
+
the LAST fire's COMPLETION — call ``mark_done(now)`` after the partial build
|
|
5194
|
+
finishes so a slow partial self-spaces rather than stacking (spec §2.2).
|
|
5195
|
+
Seeded at the sync's START, so the first partial waits a full interval and a
|
|
5196
|
+
sub-T warm sync fires zero partials.
|
|
5197
|
+
"""
|
|
5198
|
+
__slots__ = ("_interval", "_last")
|
|
5199
|
+
|
|
5200
|
+
def __init__(self, interval_s: float, *, start: float):
|
|
5201
|
+
self._interval = interval_s
|
|
5202
|
+
self._last = start
|
|
5203
|
+
|
|
5204
|
+
def should_fire(self, now: float) -> bool:
|
|
5205
|
+
return (now - self._last) >= self._interval
|
|
5206
|
+
|
|
5207
|
+
def mark_done(self, now: float) -> None:
|
|
5208
|
+
self._last = now
|
|
5209
|
+
|
|
5210
|
+
|
|
5211
|
+
def _make_a2_progress_cb(*, ref, hub, build_partial, throttle, monotonic,
|
|
5212
|
+
perf=_perf):
|
|
5213
|
+
"""Build the A2 throttled ``sync_cache`` progress callback (extracted so the
|
|
5214
|
+
throttle + suppression logic is unit-testable with injected deps).
|
|
5215
|
+
|
|
5216
|
+
On proceed it builds a partial via ``build_partial()`` (a fresh, complete
|
|
5217
|
+
``skip_sync=True`` snapshot over the current committed cache — NOT nested in
|
|
5218
|
+
another build) and stores the clean non-hydrating object on ``ref``, then
|
|
5219
|
+
publishes a ``hydrating=True`` COPY over the hub (§1.4.1 shared-object-leak:
|
|
5220
|
+
only the publish carries the latch, so the memo's retained object stays
|
|
5221
|
+
clean). The dispatch-memo write itself happens inside ``build_partial()`` /
|
|
5222
|
+
``_tui_build_snapshot`` (its snapshot-cache reconcile step), NOT in this cb.
|
|
5223
|
+
Suppressed entirely while perf tracing is
|
|
5224
|
+
active (spec §2.2): progressive fill is a UX nicety, and a partial build's
|
|
5225
|
+
``_perf.reset_thread`` would clobber the standalone ``sync_cache``'s
|
|
5226
|
+
in-flight trace phases. Trace-off (all normal use) is unaffected.
|
|
5227
|
+
"""
|
|
5228
|
+
def cb(_stats) -> None:
|
|
5229
|
+
if perf.enabled():
|
|
5230
|
+
return
|
|
5231
|
+
now = monotonic()
|
|
5232
|
+
if not throttle.should_fire(now):
|
|
5233
|
+
return
|
|
5234
|
+
snap = build_partial()
|
|
5235
|
+
ref.set(snap)
|
|
5236
|
+
hub.publish(dataclasses.replace(snap, hydrating=True))
|
|
5237
|
+
throttle.mark_done(monotonic())
|
|
5238
|
+
return cb
|
|
5239
|
+
|
|
5240
|
+
|
|
5093
5241
|
def _make_run_sync_now_locked(*, ref, hub, pinned_now, display_tz_pref_override,
|
|
5094
5242
|
runtime_bind=None):
|
|
5095
5243
|
"""Return a closure that does the snapshot-rebuild + SSE-publish work.
|
|
@@ -5107,25 +5255,78 @@ def _make_run_sync_now_locked(*, ref, hub, pinned_now, display_tz_pref_override,
|
|
|
5107
5255
|
DASHBOARD-only factory, so every rebuild here precomputes the envelope's
|
|
5108
5256
|
doctor / config / update-state onto the snapshot (``precompute_envelope=True``)
|
|
5109
5257
|
— keeping ``snapshot_to_envelope`` a pure renderer across all SSE clients.
|
|
5258
|
+
|
|
5259
|
+
#278 A2 (§2.1): the ``skip_sync=False`` path DECOUPLES the ingest from the
|
|
5260
|
+
build — it runs ``sync_cache`` STANDALONE (with a throttled progress cb),
|
|
5261
|
+
then builds the final snapshot with ``skip_sync=True``. Routing the ingest
|
|
5262
|
+
through ``_tui_build_snapshot`` instead would be unsafe: that function
|
|
5263
|
+
unconditionally ``_perf.reset_thread()``s (corrupting the trace §0 fixes),
|
|
5264
|
+
writes dispatch state, and mutates single-writer accelerator caches — so a
|
|
5265
|
+
re-entrant partial build from inside its ``sync`` phase would corrupt all
|
|
5266
|
+
three. The decoupled final build reads the same post-sync cache and computes
|
|
5267
|
+
the same post-sync dispatch signature, so it is byte-identical to today's
|
|
5268
|
+
``_tui_build_snapshot(skip_sync=False)``. The ``skip_sync=True`` path (POST
|
|
5269
|
+
/api/settings) stays the single-build path.
|
|
5110
5270
|
"""
|
|
5271
|
+
def _build(skip_sync: bool):
|
|
5272
|
+
# Resolve _tui_build_snapshot via cctally's namespace so the eager
|
|
5273
|
+
# re-export AND ``monkeypatch.setitem(ns, "_tui_build_snapshot", spy)``
|
|
5274
|
+
# in tests propagate into this closure body (a bare-name lookup would
|
|
5275
|
+
# resolve in this sibling's __dict__ and miss the cctally-side patch).
|
|
5276
|
+
return sys.modules["cctally"]._tui_build_snapshot(
|
|
5277
|
+
now_utc=pinned_now, skip_sync=skip_sync,
|
|
5278
|
+
display_tz_pref_override=display_tz_pref_override,
|
|
5279
|
+
precompute_envelope=True, runtime_bind=runtime_bind,
|
|
5280
|
+
)
|
|
5281
|
+
|
|
5111
5282
|
def _locked(skip_sync: bool) -> None:
|
|
5112
5283
|
try:
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5284
|
+
if not skip_sync:
|
|
5285
|
+
# ── Decoupled ingest + build (§2.1) ─────────────────────────
|
|
5286
|
+
import time as _time
|
|
5287
|
+
sync_error = None
|
|
5288
|
+
start = _time.monotonic()
|
|
5289
|
+
throttle = _A2ThrottleClock(_A2_PARTIAL_THROTTLE_S, start=start)
|
|
5290
|
+
cb = _make_a2_progress_cb(
|
|
5291
|
+
ref=ref, hub=hub,
|
|
5292
|
+
build_partial=lambda: _build(skip_sync=True),
|
|
5293
|
+
throttle=throttle, monotonic=_time.monotonic,
|
|
5294
|
+
)
|
|
5295
|
+
cache_conn = _cctally().open_cache_db()
|
|
5296
|
+
try:
|
|
5297
|
+
# Under CCTALLY_PERF_TRACE the phase tree this standalone
|
|
5298
|
+
# sync_cache builds is intentionally NOT surfaced in the live
|
|
5299
|
+
# dashboard trace: the final _build(skip_sync=True) below runs
|
|
5300
|
+
# _tui_build_snapshot, which resets the thread perf stack, so
|
|
5301
|
+
# these phases never reach an emitter. §0's trace-attribution
|
|
5302
|
+
# target is `cctally-bench --trace`, which builds directly
|
|
5303
|
+
# (no decoupled standalone ingest) and keeps its phase tree.
|
|
5304
|
+
sync_cache(cache_conn, progress=cb)
|
|
5305
|
+
except Exception as exc: # noqa: BLE001 — surfaced on the snap
|
|
5306
|
+
sync_error = f"sync-cache: {exc}"
|
|
5307
|
+
finally:
|
|
5308
|
+
cache_conn.close()
|
|
5309
|
+
snap = _build(skip_sync=True) # final: hydrating=False (default)
|
|
5310
|
+
if sync_error is not None:
|
|
5311
|
+
# Thread the standalone sync error into last_sync_error, sync
|
|
5312
|
+
# error FIRST (mirrors the internal path where the `sync`
|
|
5313
|
+
# phase's error is errors[0]) so the surface is unchanged.
|
|
5314
|
+
existing = snap.last_sync_error
|
|
5315
|
+
merged = (sync_error if not existing
|
|
5316
|
+
else f"{sync_error}; {existing}")
|
|
5317
|
+
snap = dataclasses.replace(snap, last_sync_error=merged)
|
|
5318
|
+
ref.set(snap)
|
|
5319
|
+
hub.publish(snap)
|
|
5320
|
+
return
|
|
5321
|
+
# ── skip_sync=True: single-build path (POST /api/settings) ──────
|
|
5322
|
+
snap = _build(skip_sync=True)
|
|
5323
|
+
# Mirror the startup override: suppress the monotonic sync stamp so
|
|
5324
|
+
# the envelope keeps emitting sync_age_s=None and the client keeps
|
|
5325
|
+
# rendering "sync paused" after the user hits r / clicks the sync
|
|
5326
|
+
# chip. #278 §1.4.1: force the hydration latch clear (default False
|
|
5327
|
+
# from _tui_build_snapshot, restated here since this is a replace()
|
|
5328
|
+
# clone site).
|
|
5329
|
+
snap = dataclasses.replace(snap, last_sync_at=None, hydrating=False)
|
|
5129
5330
|
ref.set(snap)
|
|
5130
5331
|
hub.publish(snap)
|
|
5131
5332
|
except Exception as exc:
|
|
@@ -5134,6 +5335,10 @@ def _make_run_sync_now_locked(*, ref, hub, pinned_now, display_tz_pref_override,
|
|
|
5134
5335
|
prev,
|
|
5135
5336
|
last_sync_error=f"sync crashed: {exc}",
|
|
5136
5337
|
generated_at=dt.datetime.now(dt.timezone.utc),
|
|
5338
|
+
# #278 §1.4.1: a crash-carry snapshot is stable (not mid-
|
|
5339
|
+
# assembly); clear the latch even if ``prev`` was a hydrating
|
|
5340
|
+
# seed/partial, so the client doesn't stay stuck in skeletons.
|
|
5341
|
+
hydrating=False,
|
|
5137
5342
|
)
|
|
5138
5343
|
ref.set(crashed)
|
|
5139
5344
|
hub.publish(crashed)
|
|
@@ -5500,6 +5705,10 @@ def _tui_render_once(
|
|
|
5500
5705
|
)
|
|
5501
5706
|
except Exception:
|
|
5502
5707
|
snap = _tui_empty_snapshot(now_utc)
|
|
5708
|
+
# #276 perf: flush the snapshot phase tree to stderr when tracing is on
|
|
5709
|
+
# (the rendered frame still goes to stdout only). No-op when off.
|
|
5710
|
+
if _perf.enabled():
|
|
5711
|
+
_perf.flush_stderr(_perf.current_root())
|
|
5503
5712
|
|
|
5504
5713
|
# --- Render -----------------------------------------------------------
|
|
5505
5714
|
# Drift guards run at module import + inside _tui_build_theme itself,
|