cctally 1.64.0 → 1.66.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 +50 -0
- package/bin/_cctally_alerts.py +1 -1
- package/bin/_cctally_cache.py +262 -40
- package/bin/_cctally_cache_report.py +7 -5
- package/bin/_cctally_core.py +49 -14
- package/bin/_cctally_dashboard.py +827 -4911
- package/bin/_cctally_dashboard_cache_report.py +714 -0
- package/bin/_cctally_dashboard_conversation.py +771 -0
- package/bin/_cctally_dashboard_envelope.py +1520 -0
- package/bin/_cctally_dashboard_share.py +2012 -0
- package/bin/_cctally_db.py +127 -5
- package/bin/_cctally_doctor.py +104 -3
- package/bin/_cctally_five_hour.py +2 -1
- package/bin/_cctally_forecast.py +78 -135
- package/bin/_cctally_parser.py +919 -784
- package/bin/_cctally_percent_breakdown.py +1 -1
- package/bin/_cctally_pricing_check.py +39 -0
- package/bin/_cctally_project.py +8 -5
- package/bin/_cctally_record.py +279 -274
- package/bin/_cctally_reporting.py +1 -1
- package/bin/_cctally_sync_week.py +1 -1
- package/bin/_cctally_telemetry.py +3 -5
- package/bin/_cctally_tui.py +487 -254
- package/bin/_cctally_update.py +5 -0
- package/bin/_lib_alert_dispatch.py +1 -1
- package/bin/_lib_blocks.py +6 -1
- package/bin/_lib_conversation_query.py +349 -36
- package/bin/_lib_credit.py +133 -0
- package/bin/_lib_doctor.py +150 -1
- package/bin/_lib_five_hour.py +34 -0
- package/bin/_lib_forecast.py +144 -0
- package/bin/_lib_json_envelope.py +38 -0
- package/bin/_lib_jsonl.py +115 -73
- package/bin/_lib_log.py +96 -0
- package/bin/_lib_perf.py +180 -0
- package/bin/_lib_pricing.py +47 -1
- package/bin/_lib_pricing_check.py +25 -3
- package/bin/_lib_record.py +179 -0
- package/bin/_lib_render.py +18 -10
- package/bin/_lib_snapshot_cache.py +61 -0
- package/bin/_lib_transcript_access.py +23 -0
- package/bin/cctally +51 -7
- package/bin/cctally-dashboard +2 -2
- package/bin/cctally-statusline +1 -0
- package/bin/cctally-tui +2 -2
- 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 +11 -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,42 @@ 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
|
|
230
|
+
|
|
231
|
+
import importlib.util as _ilu
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _ensure_sibling_loaded(name: str) -> None:
|
|
235
|
+
"""Register a NON-eager-loaded ``_lib_*`` sibling in ``sys.modules``.
|
|
236
|
+
|
|
237
|
+
``_lib_forecast`` (#279 S4 F2) is a NEW consumer-only sibling — kept out
|
|
238
|
+
of ``bin/cctally``'s eager-load block so ``bin/cctally`` stays
|
|
239
|
+
byte-untouched (spec §2). Under the ``SourceFileLoader`` harness path
|
|
240
|
+
(``bin/`` absent from ``sys.path``) a bare ``from _lib_forecast import``
|
|
241
|
+
would miss, so this pre-registers the sibling ``__file__``-relative when
|
|
242
|
+
it is not already importable (mirrors ``_cctally_cache._load_lib``). The
|
|
243
|
+
honest import that follows is a ``sys.modules`` hit in every load context.
|
|
244
|
+
"""
|
|
245
|
+
if name in sys.modules:
|
|
246
|
+
return
|
|
247
|
+
try:
|
|
248
|
+
__import__(name) # bin/ on sys.path: prod script / conftest / pytest
|
|
249
|
+
return
|
|
250
|
+
except ModuleNotFoundError:
|
|
251
|
+
pass
|
|
252
|
+
_p = os.path.join(os.path.dirname(__file__), f"{name}.py")
|
|
253
|
+
_spec = _ilu.spec_from_file_location(name, _p)
|
|
254
|
+
_mod = _ilu.module_from_spec(_spec)
|
|
255
|
+
sys.modules[name] = _mod
|
|
256
|
+
_spec.loader.exec_module(_mod)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
_ensure_sibling_loaded("_lib_forecast")
|
|
260
|
+
from _lib_forecast import _compute_forecast, ForecastInputs, ForecastOutput, BudgetRow
|
|
225
261
|
|
|
226
262
|
|
|
227
263
|
# === Module-level back-ref shims for helpers that STAY in bin/cctally ======
|
|
@@ -252,10 +288,6 @@ def _apply_midweek_reset_override(*args, **kwargs):
|
|
|
252
288
|
return sys.modules["cctally"]._apply_midweek_reset_override(*args, **kwargs)
|
|
253
289
|
|
|
254
290
|
|
|
255
|
-
def _compute_forecast(*args, **kwargs):
|
|
256
|
-
return sys.modules["cctally"]._compute_forecast(*args, **kwargs)
|
|
257
|
-
|
|
258
|
-
|
|
259
291
|
def _resolve_forecast_now(*args, **kwargs):
|
|
260
292
|
return sys.modules["cctally"]._resolve_forecast_now(*args, **kwargs)
|
|
261
293
|
|
|
@@ -308,20 +340,11 @@ def sync_cache(*args, **kwargs):
|
|
|
308
340
|
return sys.modules["cctally"].sync_cache(*args, **kwargs)
|
|
309
341
|
|
|
310
342
|
|
|
311
|
-
#
|
|
312
|
-
# ``
|
|
313
|
-
#
|
|
314
|
-
#
|
|
315
|
-
|
|
316
|
-
return sys.modules["cctally"].ForecastInputs(*args, **kwargs)
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
def ForecastOutput(*args, **kwargs):
|
|
320
|
-
return sys.modules["cctally"].ForecastOutput(*args, **kwargs)
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
def BudgetRow(*args, **kwargs):
|
|
324
|
-
return sys.modules["cctally"].BudgetRow(*args, **kwargs)
|
|
343
|
+
# ``_compute_forecast`` + the forecast dataclasses (``ForecastInputs`` /
|
|
344
|
+
# ``ForecastOutput`` / ``BudgetRow``) now live in ``bin/_lib_forecast.py``
|
|
345
|
+
# (#279 S4 F2) and are honest-imported at module top — used as bare-name
|
|
346
|
+
# constructors inside ``_tui_build_forecast``. Single class def per name in
|
|
347
|
+
# ``_lib_forecast``; everything imports it, so class identity stays unique.
|
|
325
348
|
|
|
326
349
|
|
|
327
350
|
# Dashboard back-refs consumed by the TUI's snapshot builders.
|
|
@@ -1104,6 +1127,22 @@ class DataSnapshot:
|
|
|
1104
1127
|
# keep working, and both are carried forward on a sync crash (Codex F6).
|
|
1105
1128
|
doctor_payload: dict | None = None
|
|
1106
1129
|
envelope_precompute: dict | None = None
|
|
1130
|
+
# ---- #278 Theme A: first-paint hydration latch ----
|
|
1131
|
+
# ``True`` ONLY on data that is genuinely still being assembled — the
|
|
1132
|
+
# cheap first-paint seed (``_dashboard_initial_snapshot`` on a normal
|
|
1133
|
+
# launch) and A2's throttled partial republishes (set on a
|
|
1134
|
+
# ``dataclasses.replace`` copy for the PUBLISH only, never dirtying the
|
|
1135
|
+
# object the dispatch memo retains). Every path that yields
|
|
1136
|
+
# complete/stable data leaves/forces it ``False``: a fresh full build is
|
|
1137
|
+
# ``False`` for free (this default), and each ``dataclasses.replace``
|
|
1138
|
+
# clone site that copies a prior snapshot's value (idle short-circuit,
|
|
1139
|
+
# update-check republish, run-sync/settings republish) forces it back to
|
|
1140
|
+
# ``False``. Serialized into the dashboard envelope by
|
|
1141
|
+
# ``snapshot_to_envelope`` (additive key ``"hydrating"``); consumers
|
|
1142
|
+
# tolerate unknown keys, and it appears in NO ``--json``/CLI surface.
|
|
1143
|
+
# Placed LAST with a default so positional fixture constructors keep
|
|
1144
|
+
# working.
|
|
1145
|
+
hydrating: bool = False
|
|
1107
1146
|
|
|
1108
1147
|
@classmethod
|
|
1109
1148
|
def synthesize_for_marketing(cls, *, as_of_iso: str) -> "DataSnapshot":
|
|
@@ -2092,6 +2131,13 @@ def _tui_build_snapshot(
|
|
|
2092
2131
|
"""
|
|
2093
2132
|
import time
|
|
2094
2133
|
now_utc = now_utc or dt.datetime.now(dt.timezone.utc)
|
|
2134
|
+
# #276 perf: reset any prior tree on this thread, then bracket the whole
|
|
2135
|
+
# snapshot build as the "snapshot" root phase. Opened via the context-
|
|
2136
|
+
# manager protocol so the long body below is not reindented; closed +
|
|
2137
|
+
# stashed at each return. Near-noop when CCTALLY_PERF_TRACE is unset.
|
|
2138
|
+
_perf.reset_thread()
|
|
2139
|
+
_p_snapshot = _perf.phase("snapshot")
|
|
2140
|
+
_p_snapshot.__enter__()
|
|
2095
2141
|
# Read config ONCE per rebuild and reuse it for the display-tz resolution
|
|
2096
2142
|
# here AND the envelope precompute below (#268 M4) — the envelope used to
|
|
2097
2143
|
# call ``load_config()`` twice per SSE client per tick.
|
|
@@ -2134,15 +2180,17 @@ def _tui_build_snapshot(
|
|
|
2134
2180
|
# builders always read pure regardless (skip_sync is reassigned to
|
|
2135
2181
|
# True below), so --no-sync means "no ingest, still read".
|
|
2136
2182
|
do_ingest = not skip_sync
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2183
|
+
with _perf.phase("sync") as _p_sync:
|
|
2184
|
+
_p_sync.set_meta(ingest=do_ingest)
|
|
2185
|
+
if do_ingest:
|
|
2140
2186
|
try:
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2187
|
+
cache_conn = _cctally().open_cache_db()
|
|
2188
|
+
try:
|
|
2189
|
+
sync_cache(cache_conn)
|
|
2190
|
+
finally:
|
|
2191
|
+
cache_conn.close()
|
|
2192
|
+
except Exception as exc:
|
|
2193
|
+
errors.append(f"sync-cache: {exc}")
|
|
2146
2194
|
# Force pure reads for every view builder below, independent of the
|
|
2147
2195
|
# caller's flag: the single ingest above is the only glob per tick.
|
|
2148
2196
|
skip_sync = True
|
|
@@ -2193,11 +2241,12 @@ def _tui_build_snapshot(
|
|
|
2193
2241
|
dispatch_key = None
|
|
2194
2242
|
if precompute_envelope:
|
|
2195
2243
|
dispatch_sig = None
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2244
|
+
with _perf.phase("signature"):
|
|
2245
|
+
try:
|
|
2246
|
+
dispatch_sig = _tui_compute_dispatch_signature(conn)
|
|
2247
|
+
except Exception as exc:
|
|
2248
|
+
errors.append(f"dispatch-signature: {exc}")
|
|
2249
|
+
dispatch_sig = None
|
|
2201
2250
|
if dispatch_sig is not None:
|
|
2202
2251
|
# The idle decision keys on the DB signature AND a render key
|
|
2203
2252
|
# that captures the config-derived inputs the composite
|
|
@@ -2220,13 +2269,20 @@ def _tui_build_snapshot(
|
|
|
2220
2269
|
and dispatch_key == prior_key
|
|
2221
2270
|
and not _snapshot_period_rolled_over(
|
|
2222
2271
|
prior_snap, now_utc, _build_display_tz)):
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2272
|
+
with _perf.phase("idle-decision"):
|
|
2273
|
+
idle_snap = _tui_build_idle_snapshot(
|
|
2274
|
+
prior_snap, now_utc=now_utc,
|
|
2275
|
+
precompute_envelope=precompute_envelope,
|
|
2276
|
+
runtime_bind=runtime_bind, raw_config=raw_config,
|
|
2277
|
+
errors=errors,
|
|
2278
|
+
)
|
|
2279
|
+
_sc.store_dispatch_state(dispatch_key, idle_snap)
|
|
2280
|
+
_p_snapshot.__exit__(None, None, None)
|
|
2281
|
+
if _perf.enabled():
|
|
2282
|
+
_perf.stash_last(
|
|
2283
|
+
_perf.current_root(), generation=None,
|
|
2284
|
+
generated_at=now_utc.isoformat(),
|
|
2285
|
+
)
|
|
2230
2286
|
return idle_snap
|
|
2231
2287
|
# ── #269 M3.1: non-idle rebuild — reconcile the shared
|
|
2232
2288
|
# per-weekref immutable-cost cache ONCE here (spec §4/§6),
|
|
@@ -2241,39 +2297,48 @@ def _tui_build_snapshot(
|
|
|
2241
2297
|
try:
|
|
2242
2298
|
_rc_cache_conn = _cctally().open_cache_db()
|
|
2243
2299
|
try:
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2300
|
+
with _perf.phase("reconcile.weekref") as _pr:
|
|
2301
|
+
_pr.set_meta(hit=False)
|
|
2302
|
+
_sc.reconcile_weekref_cache(
|
|
2303
|
+
_rc_cache_conn,
|
|
2304
|
+
max_entry_id=dispatch_sig.max_entry_id,
|
|
2305
|
+
max_mutation_seq=dispatch_sig.entry_mutation_seq,
|
|
2306
|
+
reset_sig=dispatch_sig.reset_sig,
|
|
2307
|
+
)
|
|
2308
|
+
use_weekref_cost_cache = True
|
|
2309
|
+
_pr.set_meta(hit=True)
|
|
2251
2310
|
# #269 M4.5: reconcile the projects-envelope cache with
|
|
2252
2311
|
# the SAME short-lived cache conn (session_files_sig +
|
|
2253
2312
|
# the new-entry watermark both live in cache.db). Only
|
|
2254
2313
|
# after this succeeds does `_build_projects_envelope`
|
|
2255
2314
|
# opt into the cache below.
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2315
|
+
with _perf.phase("reconcile.projects_env") as _pr:
|
|
2316
|
+
_pr.set_meta(hit=False)
|
|
2317
|
+
_sc.reconcile_projects_env_cache(
|
|
2318
|
+
_rc_cache_conn,
|
|
2319
|
+
max_entry_id=dispatch_sig.max_entry_id,
|
|
2320
|
+
max_mutation_seq=dispatch_sig.entry_mutation_seq,
|
|
2321
|
+
max_wus_id=dispatch_sig.max_wus_id,
|
|
2322
|
+
sf_sig=_sc.session_files_sig(_rc_cache_conn),
|
|
2323
|
+
)
|
|
2324
|
+
use_projects_env_cache = True
|
|
2325
|
+
_pr.set_meta(hit=True)
|
|
2264
2326
|
# #271 M3: reconcile the Bug-K pre-credit segment cache
|
|
2265
2327
|
# with the SAME short-lived cache conn (its watermark
|
|
2266
2328
|
# query lives in cache.db), using the dispatch-signature
|
|
2267
2329
|
# legs already computed. Only after this succeeds does
|
|
2268
2330
|
# `_dashboard_build_weekly_periods` opt into the cache
|
|
2269
2331
|
# below (`use_bugk_segment_cache=True`).
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2332
|
+
with _perf.phase("reconcile.bugk") as _pr:
|
|
2333
|
+
_pr.set_meta(hit=False)
|
|
2334
|
+
_sc.reconcile_bugk_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
|
+
)
|
|
2340
|
+
use_bugk_segment_cache = True
|
|
2341
|
+
_pr.set_meta(hit=True)
|
|
2277
2342
|
# #272: reconcile the cache-report per-day cache with the
|
|
2278
2343
|
# SAME short-lived cache conn (its watermark query +
|
|
2279
2344
|
# session_files_sig both live in cache.db), using the
|
|
@@ -2282,95 +2347,104 @@ def _tui_build_snapshot(
|
|
|
2282
2347
|
# `tz_key` full-clears on a display-tz change (every
|
|
2283
2348
|
# calendar-day key shifts). Only after this succeeds does
|
|
2284
2349
|
# `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
|
-
|
|
2350
|
+
with _perf.phase("reconcile.cache_report") as _pr:
|
|
2351
|
+
_pr.set_meta(hit=False)
|
|
2352
|
+
_sc.reconcile_cache_report_cache(
|
|
2353
|
+
_rc_cache_conn,
|
|
2354
|
+
max_entry_id=dispatch_sig.max_entry_id,
|
|
2355
|
+
max_mutation_seq=dispatch_sig.entry_mutation_seq,
|
|
2356
|
+
reset_sig=dispatch_sig.reset_sig,
|
|
2357
|
+
sf_sig=_sc.session_files_sig(_rc_cache_conn),
|
|
2358
|
+
# `_load_sibling` is the canonical idempotent
|
|
2359
|
+
# sibling-access idiom (it returns the already-loaded
|
|
2360
|
+
# cctally-bound kernel instance). `_lib_cache_report`
|
|
2361
|
+
# is loaded eagerly at import (bin/cctally), so this
|
|
2362
|
+
# is a plain re-fetch of a populated `sys.modules`
|
|
2363
|
+
# entry — NOT a first-tick KeyError guard.
|
|
2364
|
+
bucket_tz=_cctally()._load_sibling(
|
|
2365
|
+
"_lib_cache_report"
|
|
2366
|
+
)._resolve_bucket_tz(_build_display_tz),
|
|
2367
|
+
tz_key=str(_build_display_tz),
|
|
2368
|
+
)
|
|
2369
|
+
use_cache_report_cache = True
|
|
2370
|
+
_pr.set_meta(hit=True)
|
|
2303
2371
|
finally:
|
|
2304
2372
|
_rc_cache_conn.close()
|
|
2305
2373
|
except Exception as exc:
|
|
2306
2374
|
errors.append(f"snapshot-cache-reconcile: {exc}")
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2375
|
+
with _perf.phase("build.current_week"):
|
|
2376
|
+
try:
|
|
2377
|
+
cw = _tui_build_current_week(conn, now_utc, skip_sync=skip_sync)
|
|
2378
|
+
except Exception as exc:
|
|
2379
|
+
errors.append(f"current-week: {exc}")
|
|
2311
2380
|
fc_view = None
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2381
|
+
with _perf.phase("build.forecast"):
|
|
2382
|
+
try:
|
|
2383
|
+
# Issue #57: build the ForecastView once so we capture both
|
|
2384
|
+
# the legacy ``ForecastOutput`` (for ``snap.forecast``, which
|
|
2385
|
+
# the many TUI panel consumers still read) and the surface
|
|
2386
|
+
# fields the envelope adapter used to re-derive inline.
|
|
2387
|
+
fc_view = _tui_build_forecast_view(
|
|
2388
|
+
conn, now_utc, skip_sync=skip_sync,
|
|
2389
|
+
use_weekref_cost_cache=use_weekref_cost_cache,
|
|
2390
|
+
)
|
|
2391
|
+
fc = fc_view.output if fc_view is not None else None
|
|
2392
|
+
except Exception as exc:
|
|
2393
|
+
errors.append(f"forecast: {exc}")
|
|
2324
2394
|
# Trend: source from build_trend_view so we capture the 3-sample
|
|
2325
2395
|
# avg_dollars_per_pct alongside the rows. The TUI build path
|
|
2326
2396
|
# historically called _tui_build_trend (which now wraps the
|
|
2327
2397
|
# builder); calling the builder directly here saves one
|
|
2328
2398
|
# `_aggregate_*` round-trip.
|
|
2329
2399
|
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
|
-
|
|
2400
|
+
with _perf.phase("build.trend"):
|
|
2401
|
+
try:
|
|
2402
|
+
c = _cctally()
|
|
2403
|
+
_trend_view = c.build_trend_view(
|
|
2404
|
+
conn, now_utc=now_utc, n=8, display_tz=_build_display_tz,
|
|
2405
|
+
skip_sync=skip_sync,
|
|
2406
|
+
use_weekref_cost_cache=use_weekref_cost_cache,
|
|
2407
|
+
)
|
|
2408
|
+
trend = list(_trend_view.rows)
|
|
2409
|
+
trend_avg_dpp = _trend_view.avg_dollars_per_pct
|
|
2410
|
+
except Exception as exc:
|
|
2411
|
+
errors.append(f"trend: {exc}")
|
|
2412
|
+
with _perf.phase("build.sessions"):
|
|
2413
|
+
try:
|
|
2414
|
+
# The sessions aggregator goes through
|
|
2415
|
+
# `get_claude_session_entries`, which runs `sync_cache` unless
|
|
2416
|
+
# `skip_sync=True` is threaded through. Honor the caller's
|
|
2417
|
+
# intent so `--no-sync` and the initial cache-only paint
|
|
2418
|
+
# both avoid ingest latency/lock contention.
|
|
2419
|
+
sessions = _tui_build_sessions(
|
|
2420
|
+
now_utc, skip_sync=skip_sync, use_session_cache=True,
|
|
2421
|
+
)
|
|
2422
|
+
except Exception as exc:
|
|
2423
|
+
errors.append(f"sessions: {exc}")
|
|
2352
2424
|
# ---- v2 additions ----
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2425
|
+
with _perf.phase("build.milestones"):
|
|
2426
|
+
try:
|
|
2427
|
+
if cw is not None:
|
|
2428
|
+
milestones = _tui_build_percent_milestones(conn)
|
|
2429
|
+
except Exception as exc:
|
|
2430
|
+
errors.append(f"milestones: {exc}")
|
|
2358
2431
|
history: list = []
|
|
2359
2432
|
history_median_dpp: "float | None" = None
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2433
|
+
with _perf.phase("build.weekly_history"):
|
|
2434
|
+
try:
|
|
2435
|
+
# Issue #59: build the full TrendView so we capture the
|
|
2436
|
+
# pre-computed 4-week-median-non-current scalar alongside
|
|
2437
|
+
# the row list; the dashboard envelope adapter surfaces
|
|
2438
|
+
# the scalar as ``trend.history_median_dpp`` so
|
|
2439
|
+
# TrendModal.tsx stops re-deriving it client-side.
|
|
2440
|
+
history_view = _tui_build_weekly_history_view(
|
|
2441
|
+
conn, now_utc, skip_sync=skip_sync, display_tz=_build_display_tz,
|
|
2442
|
+
use_weekref_cost_cache=use_weekref_cost_cache,
|
|
2443
|
+
)
|
|
2444
|
+
history = list(history_view.rows)
|
|
2445
|
+
history_median_dpp = history_view.median_dpp_non_current_4w
|
|
2446
|
+
except Exception as exc:
|
|
2447
|
+
errors.append(f"weekly-history: {exc}")
|
|
2374
2448
|
# ---- v2.1 additions: dashboard Weekly / Monthly panels ----
|
|
2375
2449
|
# Sync-thread view-model totals (spec §6.6): sum directly over
|
|
2376
2450
|
# the panel rows the dashboard ACTUALLY renders. The previous
|
|
@@ -2386,23 +2460,24 @@ def _tui_build_snapshot(
|
|
|
2386
2460
|
# ``test_weekly_envelope_total_matches_sum_of_visible_rows``.
|
|
2387
2461
|
weekly_total_cost_usd = 0.0
|
|
2388
2462
|
weekly_total_tokens = 0
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2463
|
+
with _perf.phase("build.weekly_periods"):
|
|
2464
|
+
try:
|
|
2465
|
+
weekly_periods = _dashboard_build_weekly_periods(
|
|
2466
|
+
conn, now_utc, n=12, skip_sync=skip_sync,
|
|
2467
|
+
use_group_a_cache=True,
|
|
2468
|
+
use_bugk_segment_cache=use_bugk_segment_cache,
|
|
2469
|
+
)
|
|
2470
|
+
# ``stable_sum`` (math.fsum) returns float ``0.0`` on empty rows,
|
|
2471
|
+
# so the envelope stays byte-stable with the pre-fix ``0.0`` shape
|
|
2472
|
+
# (the dashboard fixture goldens assert exact JSON match).
|
|
2473
|
+
weekly_total_cost_usd = stable_sum(
|
|
2474
|
+
r.cost_usd for r in weekly_periods
|
|
2475
|
+
)
|
|
2476
|
+
weekly_total_tokens = sum(
|
|
2477
|
+
(r.total_tokens for r in weekly_periods), 0,
|
|
2478
|
+
)
|
|
2479
|
+
except Exception as exc:
|
|
2480
|
+
errors.append(f"weekly-periods: {exc}")
|
|
2406
2481
|
# Sync-thread view-model totals (spec §6.6): sum-over-visible-rows
|
|
2407
2482
|
# (same invariant as weekly above). Monthly has no Bug-K analogue,
|
|
2408
2483
|
# but coupling the footer total to the panel-row source of truth
|
|
@@ -2410,20 +2485,21 @@ def _tui_build_snapshot(
|
|
|
2410
2485
|
# same arithmetic with no behavioral upside.
|
|
2411
2486
|
monthly_total_cost_usd = 0.0
|
|
2412
2487
|
monthly_total_tokens = 0
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2488
|
+
with _perf.phase("build.monthly_periods"):
|
|
2489
|
+
try:
|
|
2490
|
+
monthly_periods = _dashboard_build_monthly_periods(
|
|
2491
|
+
conn, now_utc, n=12, skip_sync=skip_sync,
|
|
2492
|
+
use_group_a_cache=True,
|
|
2493
|
+
display_tz=_build_display_tz,
|
|
2494
|
+
)
|
|
2495
|
+
monthly_total_cost_usd = stable_sum(
|
|
2496
|
+
r.cost_usd for r in monthly_periods
|
|
2497
|
+
)
|
|
2498
|
+
monthly_total_tokens = sum(
|
|
2499
|
+
(r.total_tokens for r in monthly_periods), 0,
|
|
2500
|
+
)
|
|
2501
|
+
except Exception as exc:
|
|
2502
|
+
errors.append(f"monthly-periods: {exc}")
|
|
2427
2503
|
# ---- v2.2 additions: dashboard Blocks / Daily panels ----
|
|
2428
2504
|
# Issue #56: build the BlocksView once and read both rows
|
|
2429
2505
|
# (presentation) and totals (envelope scalars) from the same
|
|
@@ -2432,20 +2508,21 @@ def _tui_build_snapshot(
|
|
|
2432
2508
|
# kept as a thin shim for monkeypatch surfaces).
|
|
2433
2509
|
blocks_total_cost_usd = 0.0
|
|
2434
2510
|
blocks_total_tokens = 0
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2511
|
+
with _perf.phase("build.blocks"):
|
|
2512
|
+
try:
|
|
2513
|
+
if cw is not None:
|
|
2514
|
+
_blocks_view = _dashboard_build_blocks_view(
|
|
2515
|
+
conn, now_utc,
|
|
2516
|
+
week_start_at=cw.week_start_at,
|
|
2517
|
+
week_end_at=cw.week_end_at,
|
|
2518
|
+
skip_sync=skip_sync,
|
|
2519
|
+
display_tz=_build_display_tz,
|
|
2520
|
+
)
|
|
2521
|
+
blocks_panel = list(_blocks_view.rows)
|
|
2522
|
+
blocks_total_cost_usd = _blocks_view.total_cost_usd
|
|
2523
|
+
blocks_total_tokens = _blocks_view.total_tokens
|
|
2524
|
+
except Exception as exc:
|
|
2525
|
+
errors.append(f"blocks-panel: {exc}")
|
|
2449
2526
|
# Sync-thread view-model totals (Bundle 1 / spec §6.6):
|
|
2450
2527
|
# sum-over-visible-rows (same invariant as weekly/monthly above).
|
|
2451
2528
|
# Gap days in the materialized panel carry ``cost_usd=0.0`` /
|
|
@@ -2454,42 +2531,45 @@ def _tui_build_snapshot(
|
|
|
2454
2531
|
# ``build_daily_view`` pass that did the same arithmetic.
|
|
2455
2532
|
daily_total_cost_usd = 0.0
|
|
2456
2533
|
daily_total_tokens = 0
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2534
|
+
with _perf.phase("build.daily"):
|
|
2535
|
+
try:
|
|
2536
|
+
daily_panel = _dashboard_build_daily_panel(
|
|
2537
|
+
conn, now_utc, n=30, skip_sync=skip_sync,
|
|
2538
|
+
use_group_a_cache=True,
|
|
2539
|
+
display_tz=_build_display_tz,
|
|
2540
|
+
)
|
|
2541
|
+
daily_total_cost_usd = stable_sum(
|
|
2542
|
+
r.cost_usd for r in daily_panel
|
|
2543
|
+
)
|
|
2544
|
+
daily_total_tokens = sum(
|
|
2545
|
+
(r.total_tokens for r in daily_panel), 0,
|
|
2546
|
+
)
|
|
2547
|
+
except Exception as exc:
|
|
2548
|
+
errors.append(f"daily-panel: {exc}")
|
|
2471
2549
|
# ---- threshold-actions T5: alerts envelope array ----
|
|
2472
2550
|
# Precomputed at sync time so `snapshot_to_envelope` stays a pure
|
|
2473
2551
|
# renderer (no DB I/O on the dashboard hot path; mirrors how
|
|
2474
2552
|
# `current_week.five_hour_block` is precomputed via
|
|
2475
2553
|
# `_select_current_block_for_envelope`).
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2554
|
+
with _perf.phase("build.alerts"):
|
|
2555
|
+
try:
|
|
2556
|
+
alerts = _build_alerts_envelope_array(conn)
|
|
2557
|
+
except Exception as exc:
|
|
2558
|
+
errors.append(f"alerts: {exc}")
|
|
2480
2559
|
# ---- 5h in-place credit (v1.7.x) ----
|
|
2481
2560
|
# Load 5h milestones (pre + post credit) for the current
|
|
2482
2561
|
# block's window so CurrentWeekModal can render a merged
|
|
2483
2562
|
# chronological timeline alongside its weekly milestones.
|
|
2484
2563
|
# Spec §5.3 (Codex r1 finding 3).
|
|
2485
2564
|
fh_milestones: list[dict] = []
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2565
|
+
with _perf.phase("build.five_hour_milestones"):
|
|
2566
|
+
try:
|
|
2567
|
+
win_key = None
|
|
2568
|
+
if cw is not None and isinstance(cw.five_hour_block, dict):
|
|
2569
|
+
win_key = cw.five_hour_block.get("five_hour_window_key")
|
|
2570
|
+
fh_milestones = _tui_build_five_hour_milestones(conn, win_key)
|
|
2571
|
+
except Exception as exc:
|
|
2572
|
+
errors.append(f"five-hour-milestones: {exc}")
|
|
2493
2573
|
# ---- Projects panel + modal envelope (spec §5.2, plan Task 1) -----
|
|
2494
2574
|
# Per-tick aggregation lives on the sync thread; the dashboard's
|
|
2495
2575
|
# pure ``snapshot_to_envelope`` reads ``snap.projects_envelope``
|
|
@@ -2504,6 +2584,11 @@ def _tui_build_snapshot(
|
|
|
2504
2584
|
# all three tables. ATTACH/DETACH is cheap and scoped to this
|
|
2505
2585
|
# sub-build; no schema migration / lock acquisition is needed.
|
|
2506
2586
|
projects_envelope_block: dict | None = None
|
|
2587
|
+
# #276 perf: time the projects-envelope build (ATTACH + walk + DETACH).
|
|
2588
|
+
# CM protocol, not a ``with`` block, to avoid reindenting the ~40-line
|
|
2589
|
+
# try/except/finally; the broad except + finally guarantee no escape.
|
|
2590
|
+
_p_pe = _perf.phase("build.projects_envelope")
|
|
2591
|
+
_p_pe.__enter__()
|
|
2507
2592
|
try:
|
|
2508
2593
|
c = _cctally()
|
|
2509
2594
|
cache_db_path = _cctally_core.CACHE_DB_PATH
|
|
@@ -2545,6 +2630,7 @@ def _tui_build_snapshot(
|
|
|
2545
2630
|
conn.execute("DETACH DATABASE cache_db")
|
|
2546
2631
|
except Exception:
|
|
2547
2632
|
pass
|
|
2633
|
+
_p_pe.__exit__(None, None, None)
|
|
2548
2634
|
# Late-bind disambiguated `project_key` onto each SessionsPanel
|
|
2549
2635
|
# row so the SessionsPanel → ProjectsModal cross-nav (spec §4.1)
|
|
2550
2636
|
# routes by the same identity the Projects envelope emits.
|
|
@@ -2606,28 +2692,29 @@ def _tui_build_snapshot(
|
|
|
2606
2692
|
# on the DataSnapshot field and the client renders the empty
|
|
2607
2693
|
# state.
|
|
2608
2694
|
cache_report_block = None
|
|
2609
|
-
|
|
2610
|
-
cfg_cr = load_config().get("cache_report") or {}
|
|
2611
|
-
threshold_raw = cfg_cr.get("anomaly_threshold_pp", 15)
|
|
2695
|
+
with _perf.phase("build.cache_report"):
|
|
2612
2696
|
try:
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2697
|
+
cfg_cr = load_config().get("cache_report") or {}
|
|
2698
|
+
threshold_raw = cfg_cr.get("anomaly_threshold_pp", 15)
|
|
2699
|
+
try:
|
|
2700
|
+
threshold_pp = int(threshold_raw)
|
|
2701
|
+
except (TypeError, ValueError):
|
|
2702
|
+
threshold_pp = 15
|
|
2703
|
+
if threshold_pp < 1 or threshold_pp > 100:
|
|
2704
|
+
threshold_pp = 15
|
|
2705
|
+
_dash_mod = sys.modules["_cctally_dashboard"]
|
|
2706
|
+
_bcr = _dash_mod.build_cache_report_snapshot
|
|
2707
|
+
cache_report_block = _bcr(
|
|
2708
|
+
now_utc=now_utc,
|
|
2709
|
+
anomaly_threshold_pp=threshold_pp,
|
|
2710
|
+
# Hardcoded for v1; F10 tracks lifting via cache_report.anomaly_window_days config.
|
|
2711
|
+
anomaly_window_days=_dash_mod.CACHE_REPORT_ANOMALY_WINDOW_DAYS,
|
|
2712
|
+
display_tz=_build_display_tz,
|
|
2713
|
+
skip_sync=skip_sync,
|
|
2714
|
+
use_cache_report_cache=use_cache_report_cache,
|
|
2715
|
+
)
|
|
2716
|
+
except Exception as exc:
|
|
2717
|
+
errors.append(f"cache-report: {exc}")
|
|
2631
2718
|
|
|
2632
2719
|
# ---- #268 M4: doctor / config / update-state precompute (spec §6) ----
|
|
2633
2720
|
# Precompute the envelope's doctor / config / update-state reads ONCE
|
|
@@ -2639,18 +2726,20 @@ def _tui_build_snapshot(
|
|
|
2639
2726
|
doctor_payload_block: "dict | None" = None
|
|
2640
2727
|
envelope_precompute_block: "dict | None" = None
|
|
2641
2728
|
if precompute_envelope:
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2729
|
+
with _perf.phase("doctor"):
|
|
2730
|
+
try:
|
|
2731
|
+
doctor_payload_block = _tui_precompute_doctor_payload(
|
|
2732
|
+
now_utc, runtime_bind,
|
|
2733
|
+
)
|
|
2734
|
+
except Exception as exc:
|
|
2735
|
+
errors.append(f"doctor-precompute: {exc}")
|
|
2736
|
+
with _perf.phase("envelope.precompute"):
|
|
2737
|
+
try:
|
|
2738
|
+
envelope_precompute_block = _tui_precompute_envelope_config(
|
|
2739
|
+
raw_config,
|
|
2740
|
+
)
|
|
2741
|
+
except Exception as exc:
|
|
2742
|
+
errors.append(f"envelope-precompute: {exc}")
|
|
2654
2743
|
|
|
2655
2744
|
snap = DataSnapshot(
|
|
2656
2745
|
current_week=cw,
|
|
@@ -2692,6 +2781,16 @@ def _tui_build_snapshot(
|
|
|
2692
2781
|
_cctally()._load_sibling("_lib_snapshot_cache").store_dispatch_state(
|
|
2693
2782
|
dispatch_key, snap,
|
|
2694
2783
|
)
|
|
2784
|
+
# #276 perf: close the "snapshot" root and freeze the completed tree
|
|
2785
|
+
# into the process-global slot for the loopback /api/debug/backend
|
|
2786
|
+
# endpoint. Whole-dict atomic assignment; never mutated after. No-op
|
|
2787
|
+
# when tracing is off.
|
|
2788
|
+
_p_snapshot.__exit__(None, None, None)
|
|
2789
|
+
if _perf.enabled():
|
|
2790
|
+
_perf.stash_last(
|
|
2791
|
+
_perf.current_root(), generation=None,
|
|
2792
|
+
generated_at=now_utc.isoformat(),
|
|
2793
|
+
)
|
|
2695
2794
|
return snap
|
|
2696
2795
|
finally:
|
|
2697
2796
|
conn.close()
|
|
@@ -2894,6 +2993,10 @@ def _tui_build_idle_snapshot(prior, *, now_utc, precompute_envelope,
|
|
|
2894
2993
|
last_sync_error=("; ".join(errors) if errors else None),
|
|
2895
2994
|
doctor_payload=doctor_payload,
|
|
2896
2995
|
envelope_precompute=envelope_precompute,
|
|
2996
|
+
# #278 §1.4.1: an idle snapshot means the data-version signature is
|
|
2997
|
+
# unchanged (data stable) → force the hydration latch clear even if
|
|
2998
|
+
# ``prior`` was a hydrating seed/partial.
|
|
2999
|
+
hydrating=False,
|
|
2897
3000
|
)
|
|
2898
3001
|
|
|
2899
3002
|
|
|
@@ -5090,6 +5193,69 @@ def _tui_refresh_interval_type(s: str) -> float:
|
|
|
5090
5193
|
return v
|
|
5091
5194
|
|
|
5092
5195
|
|
|
5196
|
+
# ── #278 Theme A (A2): progressive first-run ingest fill ─────────────────────
|
|
5197
|
+
# A first-run / long-gap dashboard sync walks many files and takes seconds to
|
|
5198
|
+
# tens-of-seconds; before this it published exactly once at the end (empty →
|
|
5199
|
+
# single jump). A2 wires the existing ``sync_cache(progress=…)`` seam to a
|
|
5200
|
+
# THROTTLED partial republish so the heavy panels fill progressively. It is
|
|
5201
|
+
# self-limiting to first-run: a warm returning user's sync finishes under T, so
|
|
5202
|
+
# the throttle never fires and A2 is a pure no-op (exactly one publish). Not
|
|
5203
|
+
# user-configurable (YAGNI).
|
|
5204
|
+
_A2_PARTIAL_THROTTLE_S = 2.0 # T
|
|
5205
|
+
|
|
5206
|
+
|
|
5207
|
+
class _A2ThrottleClock:
|
|
5208
|
+
"""Completion-measured throttle for A2 partial republishes.
|
|
5209
|
+
|
|
5210
|
+
``should_fire(now)`` is True at most once per ``interval_s``, measured from
|
|
5211
|
+
the LAST fire's COMPLETION — call ``mark_done(now)`` after the partial build
|
|
5212
|
+
finishes so a slow partial self-spaces rather than stacking (spec §2.2).
|
|
5213
|
+
Seeded at the sync's START, so the first partial waits a full interval and a
|
|
5214
|
+
sub-T warm sync fires zero partials.
|
|
5215
|
+
"""
|
|
5216
|
+
__slots__ = ("_interval", "_last")
|
|
5217
|
+
|
|
5218
|
+
def __init__(self, interval_s: float, *, start: float):
|
|
5219
|
+
self._interval = interval_s
|
|
5220
|
+
self._last = start
|
|
5221
|
+
|
|
5222
|
+
def should_fire(self, now: float) -> bool:
|
|
5223
|
+
return (now - self._last) >= self._interval
|
|
5224
|
+
|
|
5225
|
+
def mark_done(self, now: float) -> None:
|
|
5226
|
+
self._last = now
|
|
5227
|
+
|
|
5228
|
+
|
|
5229
|
+
def _make_a2_progress_cb(*, ref, hub, build_partial, throttle, monotonic,
|
|
5230
|
+
perf=_perf):
|
|
5231
|
+
"""Build the A2 throttled ``sync_cache`` progress callback (extracted so the
|
|
5232
|
+
throttle + suppression logic is unit-testable with injected deps).
|
|
5233
|
+
|
|
5234
|
+
On proceed it builds a partial via ``build_partial()`` (a fresh, complete
|
|
5235
|
+
``skip_sync=True`` snapshot over the current committed cache — NOT nested in
|
|
5236
|
+
another build) and stores the clean non-hydrating object on ``ref``, then
|
|
5237
|
+
publishes a ``hydrating=True`` COPY over the hub (§1.4.1 shared-object-leak:
|
|
5238
|
+
only the publish carries the latch, so the memo's retained object stays
|
|
5239
|
+
clean). The dispatch-memo write itself happens inside ``build_partial()`` /
|
|
5240
|
+
``_tui_build_snapshot`` (its snapshot-cache reconcile step), NOT in this cb.
|
|
5241
|
+
Suppressed entirely while perf tracing is
|
|
5242
|
+
active (spec §2.2): progressive fill is a UX nicety, and a partial build's
|
|
5243
|
+
``_perf.reset_thread`` would clobber the standalone ``sync_cache``'s
|
|
5244
|
+
in-flight trace phases. Trace-off (all normal use) is unaffected.
|
|
5245
|
+
"""
|
|
5246
|
+
def cb(_stats) -> None:
|
|
5247
|
+
if perf.enabled():
|
|
5248
|
+
return
|
|
5249
|
+
now = monotonic()
|
|
5250
|
+
if not throttle.should_fire(now):
|
|
5251
|
+
return
|
|
5252
|
+
snap = build_partial()
|
|
5253
|
+
ref.set(snap)
|
|
5254
|
+
hub.publish(dataclasses.replace(snap, hydrating=True))
|
|
5255
|
+
throttle.mark_done(monotonic())
|
|
5256
|
+
return cb
|
|
5257
|
+
|
|
5258
|
+
|
|
5093
5259
|
def _make_run_sync_now_locked(*, ref, hub, pinned_now, display_tz_pref_override,
|
|
5094
5260
|
runtime_bind=None):
|
|
5095
5261
|
"""Return a closure that does the snapshot-rebuild + SSE-publish work.
|
|
@@ -5107,25 +5273,84 @@ def _make_run_sync_now_locked(*, ref, hub, pinned_now, display_tz_pref_override,
|
|
|
5107
5273
|
DASHBOARD-only factory, so every rebuild here precomputes the envelope's
|
|
5108
5274
|
doctor / config / update-state onto the snapshot (``precompute_envelope=True``)
|
|
5109
5275
|
— keeping ``snapshot_to_envelope`` a pure renderer across all SSE clients.
|
|
5276
|
+
|
|
5277
|
+
#278 A2 (§2.1): the ``skip_sync=False`` path DECOUPLES the ingest from the
|
|
5278
|
+
build — it runs ``sync_cache`` STANDALONE (with a throttled progress cb),
|
|
5279
|
+
then builds the final snapshot with ``skip_sync=True``. Routing the ingest
|
|
5280
|
+
through ``_tui_build_snapshot`` instead would be unsafe: that function
|
|
5281
|
+
unconditionally ``_perf.reset_thread()``s (corrupting the trace §0 fixes),
|
|
5282
|
+
writes dispatch state, and mutates single-writer accelerator caches — so a
|
|
5283
|
+
re-entrant partial build from inside its ``sync`` phase would corrupt all
|
|
5284
|
+
three. The decoupled final build reads the same post-sync cache and computes
|
|
5285
|
+
the same post-sync dispatch signature, so it is byte-identical to today's
|
|
5286
|
+
``_tui_build_snapshot(skip_sync=False)``. The ``skip_sync=True`` path (POST
|
|
5287
|
+
/api/settings) stays the single-build path.
|
|
5110
5288
|
"""
|
|
5289
|
+
def _build(skip_sync: bool):
|
|
5290
|
+
# Resolve _tui_build_snapshot via cctally's namespace so the eager
|
|
5291
|
+
# re-export AND ``monkeypatch.setitem(ns, "_tui_build_snapshot", spy)``
|
|
5292
|
+
# in tests propagate into this closure body (a bare-name lookup would
|
|
5293
|
+
# resolve in this sibling's __dict__ and miss the cctally-side patch).
|
|
5294
|
+
return sys.modules["cctally"]._tui_build_snapshot(
|
|
5295
|
+
now_utc=pinned_now, skip_sync=skip_sync,
|
|
5296
|
+
display_tz_pref_override=display_tz_pref_override,
|
|
5297
|
+
precompute_envelope=True, runtime_bind=runtime_bind,
|
|
5298
|
+
)
|
|
5299
|
+
|
|
5111
5300
|
def _locked(skip_sync: bool) -> None:
|
|
5301
|
+
# #279 S5 F6.3 (gate P1-1): arm the snapshot-cache owner-thread tripwire
|
|
5302
|
+
# for whichever thread holds sync_lock for THIS rebuild — the periodic
|
|
5303
|
+
# sync thread, or a /api/sync / /api/settings request thread. Overwrite-
|
|
5304
|
+
# on-call, so ownership transfers to the current rebuilder; the guards in
|
|
5305
|
+
# _lib_snapshot_cache then catch a lock-bypassing foreign-thread mutation.
|
|
5306
|
+
_cctally()._load_sibling("_lib_snapshot_cache").mark_owner_thread()
|
|
5112
5307
|
try:
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5308
|
+
if not skip_sync:
|
|
5309
|
+
# ── Decoupled ingest + build (§2.1) ─────────────────────────
|
|
5310
|
+
import time as _time
|
|
5311
|
+
sync_error = None
|
|
5312
|
+
start = _time.monotonic()
|
|
5313
|
+
throttle = _A2ThrottleClock(_A2_PARTIAL_THROTTLE_S, start=start)
|
|
5314
|
+
cb = _make_a2_progress_cb(
|
|
5315
|
+
ref=ref, hub=hub,
|
|
5316
|
+
build_partial=lambda: _build(skip_sync=True),
|
|
5317
|
+
throttle=throttle, monotonic=_time.monotonic,
|
|
5318
|
+
)
|
|
5319
|
+
cache_conn = _cctally().open_cache_db()
|
|
5320
|
+
try:
|
|
5321
|
+
# Under CCTALLY_PERF_TRACE the phase tree this standalone
|
|
5322
|
+
# sync_cache builds is intentionally NOT surfaced in the live
|
|
5323
|
+
# dashboard trace: the final _build(skip_sync=True) below runs
|
|
5324
|
+
# _tui_build_snapshot, which resets the thread perf stack, so
|
|
5325
|
+
# these phases never reach an emitter. §0's trace-attribution
|
|
5326
|
+
# target is `cctally-bench --trace`, which builds directly
|
|
5327
|
+
# (no decoupled standalone ingest) and keeps its phase tree.
|
|
5328
|
+
sync_cache(cache_conn, progress=cb)
|
|
5329
|
+
except Exception as exc: # noqa: BLE001 — surfaced on the snap
|
|
5330
|
+
sync_error = f"sync-cache: {exc}"
|
|
5331
|
+
finally:
|
|
5332
|
+
cache_conn.close()
|
|
5333
|
+
snap = _build(skip_sync=True) # final: hydrating=False (default)
|
|
5334
|
+
if sync_error is not None:
|
|
5335
|
+
# Thread the standalone sync error into last_sync_error, sync
|
|
5336
|
+
# error FIRST (mirrors the internal path where the `sync`
|
|
5337
|
+
# phase's error is errors[0]) so the surface is unchanged.
|
|
5338
|
+
existing = snap.last_sync_error
|
|
5339
|
+
merged = (sync_error if not existing
|
|
5340
|
+
else f"{sync_error}; {existing}")
|
|
5341
|
+
snap = dataclasses.replace(snap, last_sync_error=merged)
|
|
5342
|
+
ref.set(snap)
|
|
5343
|
+
hub.publish(snap)
|
|
5344
|
+
return
|
|
5345
|
+
# ── skip_sync=True: single-build path (POST /api/settings) ──────
|
|
5346
|
+
snap = _build(skip_sync=True)
|
|
5347
|
+
# Mirror the startup override: suppress the monotonic sync stamp so
|
|
5348
|
+
# the envelope keeps emitting sync_age_s=None and the client keeps
|
|
5349
|
+
# rendering "sync paused" after the user hits r / clicks the sync
|
|
5350
|
+
# chip. #278 §1.4.1: force the hydration latch clear (default False
|
|
5351
|
+
# from _tui_build_snapshot, restated here since this is a replace()
|
|
5352
|
+
# clone site).
|
|
5353
|
+
snap = dataclasses.replace(snap, last_sync_at=None, hydrating=False)
|
|
5129
5354
|
ref.set(snap)
|
|
5130
5355
|
hub.publish(snap)
|
|
5131
5356
|
except Exception as exc:
|
|
@@ -5134,6 +5359,10 @@ def _make_run_sync_now_locked(*, ref, hub, pinned_now, display_tz_pref_override,
|
|
|
5134
5359
|
prev,
|
|
5135
5360
|
last_sync_error=f"sync crashed: {exc}",
|
|
5136
5361
|
generated_at=dt.datetime.now(dt.timezone.utc),
|
|
5362
|
+
# #278 §1.4.1: a crash-carry snapshot is stable (not mid-
|
|
5363
|
+
# assembly); clear the latch even if ``prev`` was a hydrating
|
|
5364
|
+
# seed/partial, so the client doesn't stay stuck in skeletons.
|
|
5365
|
+
hydrating=False,
|
|
5137
5366
|
)
|
|
5138
5367
|
ref.set(crashed)
|
|
5139
5368
|
hub.publish(crashed)
|
|
@@ -5500,6 +5729,10 @@ def _tui_render_once(
|
|
|
5500
5729
|
)
|
|
5501
5730
|
except Exception:
|
|
5502
5731
|
snap = _tui_empty_snapshot(now_utc)
|
|
5732
|
+
# #276 perf: flush the snapshot phase tree to stderr when tracing is on
|
|
5733
|
+
# (the rendered frame still goes to stdout only). No-op when off.
|
|
5734
|
+
if _perf.enabled():
|
|
5735
|
+
_perf.flush_stderr(_perf.current_root())
|
|
5503
5736
|
|
|
5504
5737
|
# --- Render -----------------------------------------------------------
|
|
5505
5738
|
# Drift guards run at module import + inside _tui_build_theme itself,
|