cctally 1.27.0 → 1.27.1

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 CHANGED
@@ -5,6 +5,14 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.27.1] - 2026-06-04
9
+
10
+ ### Fixed
11
+ - **Three Linux-only bugs, surfaced by running the full test suite on Linux for the first time** (cctally is developed on macOS, whose case-insensitive filesystem and BSD tooling had masked them): (1) `cctally statusline` with `display.tz = utc` no longer prints a spurious `invalid timezone 'utc'; using 'UTC'` warning on Linux — the lowercase `utc` preference is now normalized to the portable IANA key `UTC` before resolution (Linux's case-sensitive zoneinfo rejects `"utc"` where macOS silently accepts it); (2) the local web dashboard's background update-check thread no longer crashes with `TypeError: 'Event' object is not callable` when the dashboard shuts down — its internal stop-event field was shadowing `threading.Thread._stop`, which `Thread.join()` invokes during teardown; (3) `cctally setup --uninstall` now reliably terminates a running legacy usage-poller on Linux even when it was launched from a long filesystem path — the process-identity check passes `ps -ww` (unlimited-width output) so Linux's default ~80-column truncation can't drop the identifying token and mis-read the live daemon as a dead PID. No action needed on upgrade.
12
+
13
+ ### Changed
14
+ - **Internal (no user-facing change): the full test suite (`bin/cctally-test-all`) is now Linux-portable, and a GitHub-hosted Linux matrix (Ubuntu × Python 3.11/3.12/3.13) runs it on every tag, weekly, and on demand** — closing the cross-version verification gap left when the floor was lowered to 3.11 in 1.27.0. The portability work fixed interpreter- and OS-divergences that only ever ran on macOS before: a `pytest-xdist` timezone leak (one test pinned a Pacific `tzset()` whose libc state outlived `monkeypatch`, flipping later tests' date boundaries — now reset suite-wide via an autouse `conftest` fixture), terminal-width-dependent block-gap rendering under `COLUMNS=80`, Python 3.13's `~~~^^^` traceback caret-anchors in a migration golden, the macOS-`osascript`-vs-Linux-`notify-send` notifier-dispatch assumptions, a BSD-vs-util-linux `script` PTY invocation in the update-banner harness (now a portable `pty.spawn`), a host-config leak in the dashboard-envelope golden, and a detached background update-check that raced fixture teardown. (#132)
15
+
8
16
  ## [1.27.0] - 2026-06-04
9
17
 
10
18
  ### Changed
@@ -998,11 +998,15 @@ def _legacy_stop_active_poller() -> str:
998
998
  # Ownership probe: the PID file is at a predictable /tmp path that
999
999
  # outlives the daemon on uncleanly exit, and macOS PIDs cycle in a
1000
1000
  # narrow space — verify the live process is actually our legacy
1001
- # poller before signaling. ps's `-o command=` emits the full cmdline
1002
- # with no header on both macOS BSD ps and Linux util-linux ps.
1001
+ # poller before signaling. `-o command=` emits the cmdline with no
1002
+ # header on both macOS BSD ps and Linux util-linux ps; `-ww` forces
1003
+ # UNLIMITED width so the cmdline is never truncated. Without it,
1004
+ # Linux util-linux ps clamps the column to ~80 chars (macOS BSD ps
1005
+ # does not), so a poller launched from a long path drops the
1006
+ # "usage-poller.py" token off the end → a false "stale-pid".
1003
1007
  try:
1004
1008
  probe = subprocess.run(
1005
- ["ps", "-p", str(pid), "-o", "command="],
1009
+ ["ps", "-ww", "-p", str(pid), "-o", "command="],
1006
1010
  capture_output=True, text=True, timeout=2.0,
1007
1011
  )
1008
1012
  except (OSError, subprocess.TimeoutExpired):
@@ -91,6 +91,14 @@ def _resolve_statusline_tz(cli_tz, cfg, warn_once):
91
91
  tz_name = c._local_tz_name() or "UTC"
92
92
  except Exception:
93
93
  tz_name = "UTC"
94
+ elif tz_name and tz_name.lower() == "utc":
95
+ # Canonical "utc" (the value get_display_tz_pref / normalize_display_tz_value
96
+ # emit) -> the portable IANA key "UTC". macOS's case-insensitive
97
+ # filesystem resolves ZoneInfo("utc") to UTC, but Linux's case-sensitive
98
+ # /usr/share/zoneinfo raises ZoneInfoNotFoundError, which would emit a
99
+ # spurious "invalid timezone 'utc'" warning below. Mirrors
100
+ # resolve_display_tz, which maps canonical "utc" -> ZoneInfo("Etc/UTC").
101
+ tz_name = "UTC"
94
102
  try:
95
103
  ZoneInfo(tz_name)
96
104
  except (ZoneInfoNotFoundError, Exception):
@@ -1890,13 +1890,13 @@ class _DashboardUpdateCheckThread(threading.Thread):
1890
1890
  snapshot_ref: "_SnapshotRef | None" = None,
1891
1891
  ) -> None:
1892
1892
  super().__init__(name="cctally-update-check")
1893
- self._stop = stop_event
1893
+ self._stop_event = stop_event
1894
1894
  self._hub = hub
1895
1895
  self._ref = snapshot_ref
1896
1896
 
1897
1897
  def run(self) -> None:
1898
1898
  c = _cctally()
1899
- while not self._stop.is_set():
1899
+ while not self._stop_event.is_set():
1900
1900
  try:
1901
1901
  # Self-heal runs every tick (every 30 min by default),
1902
1902
  # NOT gated by `_is_update_check_due`'s 24h TTL. Catches
@@ -1938,7 +1938,7 @@ class _DashboardUpdateCheckThread(threading.Thread):
1938
1938
  )
1939
1939
  except Exception:
1940
1940
  pass
1941
- self._stop.wait(c.UPDATE_DASHBOARD_CHECK_POLL_S)
1941
+ self._stop_event.wait(c.UPDATE_DASHBOARD_CHECK_POLL_S)
1942
1942
 
1943
1943
 
1944
1944
  def cmd_update(args) -> int:
package/bin/cctally CHANGED
@@ -2720,7 +2720,19 @@ def _post_command_update_hooks(command: str | None, args) -> None:
2720
2720
  ``_spawn_background_update_check`` here would create ``config.json``,
2721
2721
  ``update-state.json``, and ``update.log`` on a fresh install where
2722
2722
  the existing-install gate already makes the symlink work a no-op.
2723
- Same rationale as doctor."""
2723
+ Same rationale as doctor.
2724
+
2725
+ Test/CI seam: ``CCTALLY_DISABLE_UPDATE_CHECK`` short-circuits the whole
2726
+ hook (mirrors ``CCTALLY_DISABLE_DEV_AUTODETECT``). The background
2727
+ ``_spawn_background_update_check`` is a DETACHED process that
2728
+ ``mkdir``s APP_DIR to write ``update-state.json`` / ``update.log``; a
2729
+ fixture harness that runs a command and then asserts on APP_DIR (e.g.
2730
+ ``cctally-setup-test``'s uninstall-purge "data dir is gone" check)
2731
+ otherwise races that detached writer re-creating the dir after the
2732
+ command returns. Setting this env var makes such harnesses
2733
+ deterministic without disabling the feature for real users."""
2734
+ if os.environ.get("CCTALLY_DISABLE_UPDATE_CHECK"):
2735
+ return
2724
2736
  if command == "setup" and getattr(args, "uninstall", False):
2725
2737
  return
2726
2738
  if command == "doctor":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cctally",
3
- "version": "1.27.0",
3
+ "version": "1.27.1",
4
4
  "description": "Claude Code usage tracker and local dashboard for Pro/Max subscription limits - weekly cost-per-percent trend, quota forecasts, threshold alerts. ccusage-compatible.",
5
5
  "homepage": "https://github.com/omrikais/cctally",
6
6
  "repository": {