cctally 1.99.1 → 1.101.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/bin/_cctally_alerts.py +13 -2
  3. package/bin/_cctally_cache.py +3 -1
  4. package/bin/_cctally_cache_report.py +103 -6
  5. package/bin/_cctally_dashboard.py +2083 -356
  6. package/bin/_cctally_dashboard_envelope.py +115 -31
  7. package/bin/_cctally_dashboard_perf.py +433 -0
  8. package/bin/_cctally_dashboard_share.py +101 -29
  9. package/bin/_cctally_dashboard_sources.py +1072 -214
  10. package/bin/_cctally_db.py +30 -14
  11. package/bin/_cctally_diff.py +20 -0
  12. package/bin/_cctally_doctor.py +1331 -1148
  13. package/bin/_cctally_forecast.py +304 -96
  14. package/bin/_cctally_milestone_history.py +10 -2
  15. package/bin/_cctally_parser.py +70 -1
  16. package/bin/_cctally_project.py +155 -47
  17. package/bin/_cctally_quota.py +40 -21
  18. package/bin/_cctally_record.py +44 -3
  19. package/bin/_cctally_refresh.py +60 -10
  20. package/bin/_cctally_share.py +9 -2
  21. package/bin/_cctally_source_analytics.py +40 -4
  22. package/bin/_cctally_statusline.py +53 -7
  23. package/bin/_cctally_tui.py +723 -232
  24. package/bin/_cctally_update.py +28 -22
  25. package/bin/_lib_alert_scope.py +685 -0
  26. package/bin/_lib_alerts_payload.py +112 -7
  27. package/bin/_lib_cache_report.py +110 -1
  28. package/bin/_lib_codex_pools.py +20 -8
  29. package/bin/_lib_dashboard_sources.py +237 -30
  30. package/bin/_lib_doctor.py +37 -0
  31. package/bin/_lib_forecast.py +12 -4
  32. package/bin/_lib_jsonl.py +4 -2
  33. package/bin/_lib_perf.py +132 -3
  34. package/bin/_lib_pricing.py +8 -7
  35. package/bin/_lib_render.py +31 -3
  36. package/bin/_lib_share_templates.py +150 -55
  37. package/bin/_lib_snapshot_cache.py +71 -13
  38. package/bin/_lib_source_analytics.py +2 -2
  39. package/bin/_lib_source_identity.py +50 -2
  40. package/bin/_lib_subscription_weeks.py +65 -0
  41. package/bin/_lib_tick_stats.py +538 -0
  42. package/bin/cctally +29 -7
  43. package/dashboard/static/assets/dashboardStream.shared-worker-1XTMV3nr.js +1 -0
  44. package/dashboard/static/assets/index-D6Eb9KDn.js +97 -0
  45. package/dashboard/static/assets/index-i3g7g8zo.css +1 -0
  46. package/dashboard/static/dashboard.html +2 -2
  47. package/package.json +4 -1
  48. package/dashboard/static/assets/index-C5NBB2w9.js +0 -97
  49. package/dashboard/static/assets/index-hJP4wlIO.css +0 -1
@@ -58,6 +58,25 @@ def _share_load_lib(*args, **kwargs):
58
58
  return sys.modules["cctally"]._share_load_lib(*args, **kwargs)
59
59
 
60
60
 
61
+ def _optional_float(value):
62
+ """``float(value)`` when there is a value, ``None`` when there is not.
63
+
64
+ #620 S1 D5. The idiom this replaces — ``float(getattr(x, "f", 0.0) or
65
+ 0.0)`` — collapses three distinct states (a real zero, an explicit
66
+ ``None``, and a missing attribute) onto one number, so a withheld
67
+ measurement leaves the builder indistinguishable from a measured zero.
68
+ Written as a helper rather than repeated inline because the reason is
69
+ the same at every site and the inline form reads as a default.
70
+ """
71
+ if value is None:
72
+ return None
73
+ # No `except (TypeError, ValueError): return None`. On a surface whose
74
+ # entire purpose is telling "absent" apart from "present", reporting a
75
+ # malformed value as withheld is the same conflation the helper removes.
76
+ # A malformed value is a defect and must surface as one (#620 S1).
77
+ return float(value)
78
+
79
+
61
80
  def _share_now_utc(*args, **kwargs):
62
81
  return sys.modules["cctally"]._share_now_utc(*args, **kwargs)
63
82
 
@@ -631,9 +650,19 @@ def _build_weekly_share_panel_data(options: dict,
631
650
  wsa = getattr(r, "week_start_at", "") or ""
632
651
  start_date = wsa[:10] if isinstance(wsa, str) and len(wsa) >= 10 else wsa
633
652
  cost = float(getattr(r, "cost_usd", 0.0) or 0.0)
634
- used_pct_raw = getattr(r, "used_pct", None)
635
- used_pct = (float(used_pct_raw) / 100.0) if used_pct_raw is not None else 0.0
636
- dpp = float(getattr(r, "dollar_per_pct", 0.0) or 0.0)
653
+ # `WeeklyPeriodRow.used_pct` is `float | None` — `None` when the week
654
+ # has a cost snapshot but no usage snapshot. Coercing it to zero
655
+ # published `0.0%` for a week nothing measured, which reads as a real
656
+ # zero-usage week. Same defect as the rate below, same treatment.
657
+ used_pct_raw = _optional_float(getattr(r, "used_pct", None))
658
+ used_pct = None if used_pct_raw is None else used_pct_raw / 100.0
659
+ # `WeeklyPeriodRow.dollar_per_pct` is `float | None` — it is `None`
660
+ # whenever the week has no usage percentage to divide by. Coercing it
661
+ # to zero here published `$0.000` for a rate nothing measured, which
662
+ # is the same fabrication #620 S1 D5 removed on the current-week and
663
+ # trend panels. Pre-existing rather than introduced by D5, and swept
664
+ # with them because it is the same defect on the same surface.
665
+ dpp = _optional_float(getattr(r, "dollar_per_pct", None))
637
666
  # Per-week top_projects: WeeklyPeriodRow doesn't carry a
638
667
  # per-project rollup, but `week_start_at` / `week_end_at` give us
639
668
  # an exact range — aggregate session_entries once per week so the
@@ -682,7 +711,9 @@ def _build_current_week_share_panel_data(options: dict,
682
711
  return {
683
712
  "kpi_cost_usd": 0.0,
684
713
  "kpi_pct_used": 0.0,
685
- "kpi_dollar_per_pct": 0.0,
714
+ # With no current week at all there is no rate to report, so
715
+ # this is withheld for the same reason as the populated path.
716
+ "kpi_dollar_per_pct": None,
686
717
  "kpi_days_remaining": 0.0,
687
718
  "daily_progression": [],
688
719
  "top_projects": [],
@@ -728,7 +759,13 @@ def _build_current_week_share_panel_data(options: dict,
728
759
  return {
729
760
  "kpi_cost_usd": float(getattr(cw, "spent_usd", 0.0) or 0.0),
730
761
  "kpi_pct_used": used_pct,
731
- "kpi_dollar_per_pct": float(getattr(cw, "dollars_per_percent", 0.0) or 0.0),
762
+ # #620 S1 D5: `TuiCurrentWeek.dollars_per_percent` is `float | None`,
763
+ # and `float(... or 0.0)` turned both a missing attribute and an
764
+ # explicit None into $0.00 — the fabrication D5 removes, restored one
765
+ # layer below the fix. A withheld rate stays withheld; the template
766
+ # renders it as `n/a`.
767
+ "kpi_dollar_per_pct": _optional_float(
768
+ getattr(cw, "dollars_per_percent", None)),
732
769
  "kpi_days_remaining": days_remaining,
733
770
  "daily_progression": progression,
734
771
  "top_projects": top_projects,
@@ -751,25 +788,41 @@ def _build_trend_share_panel_data(options: dict,
751
788
  wsa.strftime("%Y-%m-%d") if isinstance(wsa, dt.datetime)
752
789
  else (str(wsa)[:10] if wsa else "")
753
790
  )
754
- used_pct_raw = getattr(r, "used_pct", None)
755
- used_pct = (float(used_pct_raw) / 100.0) if used_pct_raw is not None else 0.0
756
- dpp = float(getattr(r, "dollars_per_percent", 0.0) or 0.0)
791
+ # `TuiTrendRow.used_pct` is `float | None` (phantom weeks — a cost
792
+ # snapshot with no usage snapshot). See the weekly builder above.
793
+ used_pct_raw = _optional_float(getattr(r, "used_pct", None))
794
+ used_pct = None if used_pct_raw is None else used_pct_raw / 100.0
795
+ # #620 S1 D5, as above. Here the fabricated zero propagated further
796
+ # than the KPI: `cost_usd` is derived from the rate, so a zeroed rate
797
+ # also published a zero weekly cost for a week whose cost is unknown.
798
+ dpp = _optional_float(getattr(r, "dollars_per_percent", None))
757
799
  weeks.append({
758
800
  "start_date": start_date,
759
- "cost_usd": dpp * (used_pct * 100.0), # ≈ row total
801
+ # A rate times a percentage is a cost only when both are known.
802
+ # With `used_pct` coerced to zero, a week with a real rate and no
803
+ # measured percentage published a $0.00 cost.
804
+ "cost_usd": (
805
+ None if (dpp is None or used_pct is None)
806
+ else dpp * (used_pct * 100.0)
807
+ ), # ≈ row total
760
808
  "pct_used": used_pct,
761
809
  "dollar_per_pct": dpp,
762
810
  })
763
811
  # Compute 3-week delta: compare last row vs row-4-from-end.
764
- delta = {"dpp_change_pct": 0.0, "cost_change_usd": 0.0}
812
+ # A delta between two weeks is a fact only when both weeks are known.
813
+ # The previous default published `0.0`, which states "no change" — the
814
+ # same fabrication as a zeroed rate, one aggregate up (#620 S1 D5).
815
+ delta: dict = {"dpp_change_pct": None, "cost_change_usd": None}
765
816
  if len(weeks) >= 4:
766
817
  cur = weeks[-1]
767
818
  ref = weeks[-4]
768
- if ref["dollar_per_pct"]:
819
+ if ref["dollar_per_pct"] and cur["dollar_per_pct"] is not None:
769
820
  delta["dpp_change_pct"] = (
770
821
  (cur["dollar_per_pct"] - ref["dollar_per_pct"]) / ref["dollar_per_pct"]
771
822
  )
772
- delta["cost_change_usd"] = cur["cost_usd"] - ref["cost_usd"]
823
+ # A delta between two costs is only a fact when both are known.
824
+ if cur["cost_usd"] is not None and ref["cost_usd"] is not None:
825
+ delta["cost_change_usd"] = cur["cost_usd"] - ref["cost_usd"]
773
826
  return {"weeks": weeks, "delta_3_weeks": delta}
774
827
 
775
828
 
@@ -923,8 +976,10 @@ def _build_forecast_share_panel_data(options: dict,
923
976
  if fc is None:
924
977
  return {
925
978
  "projected_end_pct": 0.0,
926
- "days_to_100pct": 0.0,
927
- "days_to_90pct": 0.0,
979
+ # With no forecast at all there is no rate, so neither ceiling has
980
+ # a distance — the same withholding the populated path performs.
981
+ "days_to_100pct": None,
982
+ "days_to_90pct": None,
928
983
  "daily_budgets": {
929
984
  "avg": 0.0, "recent_24h": 0.0,
930
985
  "until_90pct": 0.0, "until_100pct": 0.0,
@@ -943,10 +998,18 @@ def _build_forecast_share_panel_data(options: dict,
943
998
  r_recent = float(r_recent_raw) if r_recent_raw is not None else r_avg
944
999
  # End-of-week projected %
945
1000
  projected_end_pct = (p_now + r_avg * remaining_hours) / 100.0
946
- # Days to ceilings (simple inverse: hours-to-target / 24)
947
- def _days_to_ceiling(target_pct: float) -> float:
948
- if r_avg <= 0 or p_now >= target_pct:
1001
+ # Days to ceilings (simple inverse: hours-to-target / 24).
1002
+ # The two exit conditions are opposite facts and must not share a value.
1003
+ # `p_now >= target_pct` means the target is already reached, and zero days
1004
+ # to it is true. `r_avg <= 0` means no rate was observed, so the target is
1005
+ # not reachable on any timeline this data describes — and in the no-usage
1006
+ # state BOTH hold (`p_now == 0`, `r_avg == 0`), so the shared `0.0`
1007
+ # rendered `Days->90% 0.0`, stating the opposite of the truth.
1008
+ def _days_to_ceiling(target_pct: float) -> "float | None":
1009
+ if p_now >= target_pct:
949
1010
  return 0.0
1011
+ if r_avg <= 0:
1012
+ return None
950
1013
  hours = (target_pct - p_now) / r_avg
951
1014
  return max(0.0, hours / 24.0)
952
1015
  days_to_100 = _days_to_ceiling(100.0)
@@ -954,27 +1017,36 @@ def _build_forecast_share_panel_data(options: dict,
954
1017
  # Daily budgets — prefer ForecastView's pre-routed pair (issue #57)
955
1018
  # when available; otherwise replay the legacy ``fc.budgets`` scan
956
1019
  # inline so positionally-constructed fixture snapshots still work.
957
- budgets: dict = {"avg": 0.0, "recent_24h": 0.0,
958
- "until_90pct": 0.0, "until_100pct": 0.0}
1020
+ # Unknown until a branch below supplies one. A ceiling budget that no
1021
+ # `BudgetRow` describes is not a $0.00/day budget.
1022
+ budgets: dict = {"avg": None, "recent_24h": None,
1023
+ "until_90pct": None, "until_100pct": None}
1024
+ # #620 S1 D5: no `or 0.0` on either branch. `ForecastView.budget_*_per_day_usd`
1025
+ # and `BudgetRow.dollars_per_day` are both `None` whenever the rate is
1026
+ # withheld, and coercing them republished $0.00 on exactly the surface
1027
+ # whose message is that nothing was measured.
959
1028
  if fc_view is not None:
960
- budgets["until_100pct"] = float(
961
- fc_view.budget_100_per_day_usd or 0.0,
962
- )
963
- budgets["until_90pct"] = float(
964
- fc_view.budget_90_per_day_usd or 0.0,
965
- )
1029
+ budgets["until_100pct"] = _optional_float(
1030
+ getattr(fc_view, "budget_100_per_day_usd", None))
1031
+ budgets["until_90pct"] = _optional_float(
1032
+ getattr(fc_view, "budget_90_per_day_usd", None))
966
1033
  else:
967
1034
  for b in getattr(fc, "budgets", None) or []:
968
1035
  tp = getattr(b, "target_percent", None)
969
- dpd = float(getattr(b, "dollars_per_day", 0.0) or 0.0)
1036
+ dpd = _optional_float(getattr(b, "dollars_per_day", None))
970
1037
  if tp == 100:
971
1038
  budgets["until_100pct"] = dpd
972
1039
  elif tp == 90:
973
1040
  budgets["until_90pct"] = dpd
974
1041
  # avg / recent_24h: derive from dollars-per-percent × r_avg/r_recent.
975
- dpp = float(getattr(inputs, "dollars_per_percent", 0.0) or 0.0) if inputs else 0.0
976
- budgets["avg"] = dpp * r_avg * 24.0
977
- budgets["recent_24h"] = dpp * r_recent * 24.0
1042
+ # #620 S1 D5: the `or 0.0` fabrication is REMOVED rather than moved. With
1043
+ # no observed usage there is no rate, so a dollars-per-day figure derived
1044
+ # from it is unavailable; substituting zero here would restore the same
1045
+ # defect one layer below the fix.
1046
+ _raw_dpp = getattr(inputs, "dollars_per_percent", None) if inputs else None
1047
+ dpp = None if _raw_dpp is None else float(_raw_dpp)
1048
+ budgets["avg"] = None if dpp is None else dpp * r_avg * 24.0
1049
+ budgets["recent_24h"] = None if dpp is None else dpp * r_recent * 24.0
978
1050
  # Projection curve — 7-day forward, using r_avg
979
1051
  today = _share_now_utc().date()
980
1052
  projection_curve: list[dict] = []