cctally 1.67.1 → 1.69.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 +68 -0
- package/bin/_cctally_alerts.py +54 -2
- package/bin/_cctally_cache.py +754 -109
- package/bin/_cctally_cache_report.py +41 -17
- package/bin/_cctally_codex.py +109 -4
- package/bin/_cctally_config.py +368 -2
- package/bin/_cctally_core.py +187 -15
- package/bin/_cctally_dashboard.py +679 -5
- package/bin/_cctally_dashboard_conversation.py +9 -4
- package/bin/_cctally_dashboard_envelope.py +110 -1
- package/bin/_cctally_dashboard_share.py +557 -79
- package/bin/_cctally_db.py +366 -17
- package/bin/_cctally_diff.py +49 -16
- package/bin/_cctally_doctor.py +131 -0
- package/bin/_cctally_forecast.py +12 -4
- package/bin/_cctally_parser.py +321 -92
- package/bin/_cctally_project.py +24 -8
- package/bin/_cctally_quota.py +1381 -0
- package/bin/_cctally_record.py +319 -14
- package/bin/_cctally_refresh.py +105 -3
- package/bin/_cctally_reporting.py +7 -1
- package/bin/_cctally_setup.py +343 -113
- package/bin/_cctally_statusline.py +228 -0
- package/bin/_cctally_tui.py +787 -7
- package/bin/_lib_alert_axes.py +11 -0
- package/bin/_lib_codex_hooks.py +367 -0
- package/bin/_lib_conversation_query.py +156 -67
- package/bin/_lib_doctor.py +238 -0
- package/bin/_lib_jsonl.py +529 -162
- package/bin/_lib_quota.py +566 -0
- package/bin/_lib_share.py +152 -34
- package/bin/_lib_snapshot_cache.py +26 -0
- package/bin/_lib_source_identity.py +74 -0
- package/bin/cctally +325 -10
- package/dashboard/static/assets/index-CBbErI-P.js +80 -0
- package/dashboard/static/assets/index-kDDVOLa_.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +5 -1
- package/dashboard/static/assets/index-BybNp_Di.js +0 -80
- package/dashboard/static/assets/index-DgAmLK65.css +0 -1
|
@@ -280,10 +280,15 @@ def _handle_get_conversations_impl(handler) -> None:
|
|
|
280
280
|
|
|
281
281
|
def _handle_get_conversations_facets_impl(handler) -> None:
|
|
282
282
|
"""``GET /api/conversations/facets`` — distinct project labels + their
|
|
283
|
-
conversation counts, for the browse
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
283
|
+
conversation counts AND per-model-family session counts, for the browse
|
|
284
|
+
filter's project + model multi-selects (spec §2 + #278 Theme C). Behind the
|
|
285
|
+
SAME loopback/Host privacy gate as the list route. The projects half is a
|
|
286
|
+
cheap GROUP BY over the ``conversation_sessions`` rollup; the model-family
|
|
287
|
+
half is an index-only scan of ``conversation_messages(model, session_id)``
|
|
288
|
+
via the partial covering index ``idx_conversation_messages_model_session``
|
|
289
|
+
(#301 — a full heap-walk of the whole table before that index). The popover
|
|
290
|
+
loads its options once from here (deriving from a paginated page would be
|
|
291
|
+
incomplete).
|
|
287
292
|
"""
|
|
288
293
|
if not handler._require_transcripts_allowed():
|
|
289
294
|
return
|
|
@@ -42,6 +42,7 @@ import datetime as dt
|
|
|
42
42
|
import importlib.util as _ilu
|
|
43
43
|
import os
|
|
44
44
|
import sys
|
|
45
|
+
from collections.abc import Mapping
|
|
45
46
|
from zoneinfo import ZoneInfo
|
|
46
47
|
|
|
47
48
|
from _cctally_core import (
|
|
@@ -628,6 +629,103 @@ _ENVELOPE_AXIS_MAPPERS = {
|
|
|
628
629
|
}
|
|
629
630
|
|
|
630
631
|
|
|
632
|
+
def _source_wire_value(value: object) -> object:
|
|
633
|
+
"""Copy one published source value into JSON-compatible wire material.
|
|
634
|
+
|
|
635
|
+
Source state is frozen with mapping proxies and tuples so request threads
|
|
636
|
+
cannot mutate it. The envelope must return fresh ordinary containers
|
|
637
|
+
without consulting the cache or stats databases.
|
|
638
|
+
"""
|
|
639
|
+
if isinstance(value, Mapping):
|
|
640
|
+
return {str(key): _source_wire_value(item) for key, item in value.items()}
|
|
641
|
+
if isinstance(value, (tuple, list)):
|
|
642
|
+
return [_source_wire_value(item) for item in value]
|
|
643
|
+
if isinstance(value, dt.datetime):
|
|
644
|
+
return _iso_z(value)
|
|
645
|
+
return value
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _unavailable_source_wire() -> dict:
|
|
649
|
+
return {
|
|
650
|
+
"availability": "unavailable",
|
|
651
|
+
"freshness": "stale",
|
|
652
|
+
"warnings": [{
|
|
653
|
+
"code": "source_build_failed",
|
|
654
|
+
"message": "Source data could not be built.",
|
|
655
|
+
"domain": "read_model",
|
|
656
|
+
}],
|
|
657
|
+
"data_version": "",
|
|
658
|
+
"last_success_at": None,
|
|
659
|
+
"capabilities": {},
|
|
660
|
+
"data": None,
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def _source_state_to_wire(state: object) -> dict:
|
|
665
|
+
"""Serialize one immutable source state without exposing its storage ID."""
|
|
666
|
+
warnings = getattr(state, "warnings", ())
|
|
667
|
+
capabilities = getattr(state, "capabilities", {})
|
|
668
|
+
last_success_at = getattr(state, "last_success_at", None)
|
|
669
|
+
return {
|
|
670
|
+
"availability": getattr(state, "availability"),
|
|
671
|
+
"freshness": getattr(state, "freshness"),
|
|
672
|
+
"warnings": [
|
|
673
|
+
{
|
|
674
|
+
"code": warning.code,
|
|
675
|
+
"message": warning.message,
|
|
676
|
+
**({"domain": warning.domain} if warning.domain is not None else {}),
|
|
677
|
+
}
|
|
678
|
+
for warning in warnings
|
|
679
|
+
],
|
|
680
|
+
"data_version": getattr(state, "data_version"),
|
|
681
|
+
"last_success_at": (
|
|
682
|
+
_iso_z(last_success_at) if isinstance(last_success_at, dt.datetime) else None
|
|
683
|
+
),
|
|
684
|
+
"capabilities": {
|
|
685
|
+
str(name): {
|
|
686
|
+
"status": capability.status,
|
|
687
|
+
**({"semantics": capability.semantics}
|
|
688
|
+
if capability.semantics is not None else {}),
|
|
689
|
+
}
|
|
690
|
+
for name, capability in capabilities.items()
|
|
691
|
+
},
|
|
692
|
+
"data": _source_wire_value(getattr(state, "data", None)),
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def _source_bundle_to_envelope(bundle: object | None) -> dict:
|
|
697
|
+
"""Return S4's additive source contract from a published bundle only."""
|
|
698
|
+
unavailable = {source: _unavailable_source_wire() for source in ("claude", "codex", "all")}
|
|
699
|
+
if bundle is None:
|
|
700
|
+
return {
|
|
701
|
+
"source_schema_version": 1,
|
|
702
|
+
"default_source": "claude",
|
|
703
|
+
"source_order": ["claude", "codex", "all"],
|
|
704
|
+
"sources": unavailable,
|
|
705
|
+
}
|
|
706
|
+
try:
|
|
707
|
+
order = tuple(bundle.source_order)
|
|
708
|
+
sources = bundle.sources
|
|
709
|
+
if order != ("claude", "codex", "all") or set(sources) != set(order):
|
|
710
|
+
raise ValueError("invalid published source bundle")
|
|
711
|
+
return {
|
|
712
|
+
"source_schema_version": bundle.source_schema_version,
|
|
713
|
+
"default_source": bundle.default_source,
|
|
714
|
+
"source_order": list(order),
|
|
715
|
+
"sources": {
|
|
716
|
+
source: _source_state_to_wire(sources[source])
|
|
717
|
+
for source in order
|
|
718
|
+
},
|
|
719
|
+
}
|
|
720
|
+
except Exception: # Never turn an unexpected snapshot shape into request-thread I/O.
|
|
721
|
+
return {
|
|
722
|
+
"source_schema_version": 1,
|
|
723
|
+
"default_source": "claude",
|
|
724
|
+
"source_order": ["claude", "codex", "all"],
|
|
725
|
+
"sources": unavailable,
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
|
|
631
729
|
def _build_alerts_envelope_array(
|
|
632
730
|
conn: sqlite3.Connection,
|
|
633
731
|
limit: int = 100,
|
|
@@ -1242,7 +1340,7 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
1242
1340
|
(r for r in reversed(snap.trend) if r.is_current), None
|
|
1243
1341
|
) if snap.trend else None
|
|
1244
1342
|
|
|
1245
|
-
|
|
1343
|
+
envelope = {
|
|
1246
1344
|
"envelope_version": 2,
|
|
1247
1345
|
# #278 Theme A: single additive first-paint hydration latch. True only
|
|
1248
1346
|
# on the cheap seed + A2's partial republishes (data still being
|
|
@@ -1250,6 +1348,15 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
1250
1348
|
# default keeps positionally-constructed fixture snapshots serializing.
|
|
1251
1349
|
"hydrating": bool(getattr(snap, "hydrating", False)),
|
|
1252
1350
|
"generated_at": _iso_z(snap.generated_at),
|
|
1351
|
+
# #300: the all-inputs data-version string at build time (changes iff any
|
|
1352
|
+
# DB leg the detail endpoints read changed; flat on an idle tick). The
|
|
1353
|
+
# browser's lazy detail fetchers revalidate on THIS (an actual
|
|
1354
|
+
# data-change signal) instead of the 5s ``generated_at`` heartbeat, so a
|
|
1355
|
+
# finished/static session/project/conversation is not re-GET every tick.
|
|
1356
|
+
# ``getattr`` default keeps positionally-constructed fixture snapshots
|
|
1357
|
+
# serializing; "" is the client's "no signal" sentinel (falls back to
|
|
1358
|
+
# ``generated_at``).
|
|
1359
|
+
"data_version": str(getattr(snap, "data_version", "") or ""),
|
|
1253
1360
|
# last_sync_at in DataSnapshot is a monotonic float, not a wall
|
|
1254
1361
|
# clock — the envelope's wall-clock moment is unknowable from
|
|
1255
1362
|
# here, so expose it as None and rely on sync_age_s for the UI.
|
|
@@ -1478,6 +1585,8 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
1478
1585
|
# PREVIEW badge when set.
|
|
1479
1586
|
**sys.modules["_cctally_dashboard"]._channel_env_fragment(),
|
|
1480
1587
|
}
|
|
1588
|
+
envelope.update(_source_bundle_to_envelope(getattr(snap, "source_bundle", None)))
|
|
1589
|
+
return envelope
|
|
1481
1590
|
|
|
1482
1591
|
|
|
1483
1592
|
def _session_detail_to_envelope(detail: "TuiSessionDetail") -> dict:
|