Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit deceea9

Browse files
feat(cli): upgrade status to an active telemetry dashboard (#50)
- refactor(core): decouple large file detection (`has_large_files`) from daemon into `ops.py` for safe CLI querying. - feat(cli): surface real-time power telemetry (Eco-Mode/Critical states) in the system status panel. - feat(cli): integrate zero-latency cached remote drift warnings and pipeline blocker alerts into the repo status dashboard. - fix(test): dynamically mock the system strategy factory to prevent `MacOSStrategy` from leaking AppleScript notifications during test execution. - test: add parameterized coverage for dynamic health thresholds and UI telemetry rendering. - docs: update architecture maps and documentation to reflect the new Active Observability feature.
1 parent ad092f5 commit deceea9

9 files changed

Lines changed: 302 additions & 79 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ In a distributed environment (Laptop ↔ Desktop), state drift is inevitable.
6262
- **Out-of-Band Indexing:** Backups are stored in a configured namespace (default: `refs/heads/wip/pulsar/...`). Your `git status`, `git branch`, and `git log` remain completely clean.
6363
- **Distributed Sessions:** Hop between machines. Pulsar tracks sessions per device and lets you `sync` to pick up exactly where you left off.
6464
- **State-Aware Diagnostics:** The `doctor` command correlates transient log events with active system health to prevent alert fatigue, and proactively scans for pipeline blockers like strict git hooks or broken `systemd` configurations.
65+
- **Active Observability:** The `status` dashboard provides zero-latency power telemetry (e.g., Eco-Mode throttling) and immediately surfaces cached warnings for remote session drift and oversized files.
6566
- **Zero-Interference:**
6667
- Uses a temporary index so it never messes up your partial `git add`.
6768
- Detects if you are rebasing or merging and waits for you to finish.
@@ -186,7 +187,7 @@ This bootstraps the current directory with:
186187

187188
| Command | Description |
188189
| :--- | :--- |
189-
| `git pulsar status` | Show detailed daemon state and repository-specific commit/push history. |
190+
| `git pulsar status` | Show real-time daemon telemetry, active health blockers, and repository status. |
190191
| `git pulsar config` | Open the global configuration file in your default editor. |
191192
| `git pulsar list` | Show all watched repositories and their status. |
192193
| `git pulsar pause` | Temporarily suspend backups for this repo. |

src/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ The `src/` directory contains the package source code. The architecture strictly
1212
- **Safety:** Implements `GIT_INDEX_FILE` isolation to ensure it never locks or corrupts the user's active git index.
1313
- **`git_pulsar/ops.py`**: High-level Business Logic.
1414
- **Role:** The "Controller." It orchestrates complex multi-step operations like `finalize` (Octopus Merges), `restore`, and drift detection.
15-
- **Logic:** Calculates the "Zipper Graph" topology to merge shadow commits back into the main branch, and manages atomic file I/O for cross-process state tracking.
15+
- **Logic:** Calculates the "Zipper Graph" topology to merge shadow commits back into the main branch, manages atomic file I/O for cross-process state tracking, and evaluates pipeline blockers (e.g., oversized files).
1616
- **`git_pulsar/config.py`**: Configuration Engine.
1717
- **Role:** The "Source of Truth."
1818
- **Logic:** Implements a cascading hierarchy (Defaults → Global → Local) to merge settings from `~/.config/git-pulsar/config.toml` and project-level `pulsar.toml` or `pyproject.toml`.
@@ -35,8 +35,8 @@ The `src/` directory contains the package source code. The architecture strictly
3535
### 4. The Interface
3636

3737
- **`git_pulsar/cli.py`**: The User Entry Point & Diagnostic Engine.
38-
- **Role:** Argument parsing, UI rendering, and system health evaluation.
39-
- **Logic:** Uses `rich` for terminal visualization. Beyond routing subcommands to `ops.py` and `daemon.py`, it presents the `doctor` diagnostics. It correlates repository state against transient event logs, and relies on `ops.py` to evaluate topological drift across distributed sessions and scan for host-environment pipeline blockers (e.g., strict git hooks, missing `systemd` linger).
38+
- **Role:** Argument parsing, UI rendering, real-time observability, and system health evaluation.
39+
- **Logic:** Uses `rich` for terminal visualization. Beyond routing subcommands to `ops.py` and `daemon.py`, it presents the `doctor` diagnostics and the zero-latency `status` dashboard (surfacing power telemetry, dynamic health constraints, and cached drift warnings). It correlates repository state against transient event logs, and relies on `ops.py` to evaluate topological drift across distributed sessions and scan for host-environment pipeline blockers (e.g., strict git hooks, missing `systemd` linger).
4040

4141
---
4242

@@ -46,4 +46,4 @@ The `src/` directory contains the package source code. The architecture strictly
4646
2. **Zero-Destruction:** The `prune` logic in `ops.py` relies on strictly namespaced refspecs (`refs/heads/wip/pulsar/...`) and never touches standard heads.
4747
3. **Identity Stability:** The `system` module guarantees that a Machine ID persists across reboots, preventing "Split Brain" backup histories.
4848
4. **Configuration Precedence:** Local project configuration MUST always override global user settings to ensure repo-specific constraints (e.g., large file limits) are respected.
49-
5. **State Over Events:** The diagnostic engine (`cli.py`) MUST prioritize current repository and environmental state over historical log events to prevent alert fatigue from self-healing anomalies.
49+
5. **State Over Events (Zero-Latency):** The diagnostic engine (`cli.py`) MUST prioritize current repository state and local caches (e.g., `.git/pulsar_drift_state`) over historical log events or live network calls, ensuring the CLI never blocks the user's terminal while evaluating system health.

src/git_pulsar/cli.py

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,12 @@ def _analyze_logs(seconds: int = 86400) -> list[str]:
8989
return errors
9090

9191

92-
def _check_repo_health(path: Path) -> str | None:
92+
def _check_repo_health(path: Path, config: Config) -> str | None:
9393
"""Evaluates the health of a repository, checking for stale backups or stalled states.
9494
9595
Args:
9696
path (Path): The file system path to the repository.
97+
config (Config): The configuration instance for the repository.
9798
9899
Returns:
99100
str | None: A warning message if an issue is detected,
@@ -119,11 +120,12 @@ def _check_repo_health(path: Path) -> str | None:
119120
logger.debug(f"Failed to retrieve backup timestamp for {path.name}: {e}")
120121
return f"Has changes, but NO backup found. (Error: {e})"
121122

122-
# Check against the stale threshold (e.g., 2 hours).
123+
# Check against the dynamic stale threshold (2x commit interval).
123124
# If changes are pending and no backup has occurred recently,
124125
# the daemon may be stalled.
125-
if time.time() - last_backup_ts > 7200:
126-
return "Stalled: Changes pending > 2 hours."
126+
stale_threshold = config.daemon.commit_interval * 2
127+
if time.time() - last_backup_ts > stale_threshold:
128+
return f"Stalled: Changes pending > {stale_threshold // 60} mins."
127129

128130
except Exception as e:
129131
return f"Unable to verify git status: {e}"
@@ -189,7 +191,26 @@ def show_status() -> None:
189191

190192
system_content = Text()
191193
system_content.append("Daemon: ", style="bold")
192-
system_content.append(status_text, style=status_style)
194+
system_content.append(status_text + "\n", style=status_style)
195+
196+
# --- Power Telemetry Integration ---
197+
conf = Config.load()
198+
sys_strat = system.get_system()
199+
pct, plugged = sys_strat.get_battery()
200+
201+
system_content.append("Power: ", style="bold")
202+
if plugged:
203+
system_content.append("AC (Unrestricted)", style="green")
204+
elif pct < conf.daemon.min_battery_percent:
205+
system_content.append(
206+
f"Critical {pct}% (All Backups Suspended)", style="bold red"
207+
)
208+
elif pct < conf.daemon.eco_mode_percent:
209+
system_content.append(
210+
f"Eco-Mode {pct}% (Pushes Suspended)", style="bold yellow"
211+
)
212+
else:
213+
system_content.append(f"Battery {pct}% (Normal)", style="green")
193214

194215
console.print(Panel(system_content, title="System Status", expand=False))
195216

@@ -254,8 +275,46 @@ def show_status() -> None:
254275
else:
255276
repo_content.append("Mode: Active", style="green")
256277

278+
# --- 1. Health Integration ---
279+
health_warning = None
280+
if ops.has_large_files(cwd, conf):
281+
limit_mb = int(conf.limits.large_file_threshold / (1024 * 1024))
282+
health_warning = (
283+
f"Daemon stalled.\n File >{limit_mb}MB detected. "
284+
"Run 'git pulsar doctor' for details."
285+
)
286+
else:
287+
health_warning = _check_repo_health(cwd, conf)
288+
289+
if health_warning:
290+
repo_content.append("\n\n⚠ WARNING: ", style="bold yellow")
291+
repo_content.append(health_warning, style="yellow")
292+
257293
console.print(Panel(repo_content, title="Repository Status", expand=False))
258294

295+
# --- 2. Roaming Radar Integration (Cached) ---
296+
_, warned_ts = ops.get_drift_state(cwd)
297+
298+
# Ensure we only warn if the cached remote timestamp is strictly newer
299+
# than our local backup reference.
300+
if warned_ts > 0 and commit_str != "Never" and warned_ts > int(commit_ts):
301+
minutes_ago = int((time.time() - warned_ts) / 60)
302+
drift_content = Text()
303+
drift_content.append(
304+
"⚠ A remote machine pushed a newer session\n", style="bold yellow"
305+
)
306+
drift_content.append(
307+
f" ~{minutes_ago} mins ago. Run 'git pulsar sync'", style="yellow"
308+
)
309+
console.print(
310+
Panel(
311+
drift_content,
312+
title="Session Drift",
313+
border_style="yellow",
314+
expand=False,
315+
)
316+
)
317+
259318
# Display global repository count if not currently in a repository.
260319
elif REGISTRY_FILE.exists():
261320
count = len(system.get_registered_repos())
@@ -508,7 +567,8 @@ def run_doctor() -> None:
508567
issues = []
509568
for p in paths:
510569
if p.exists():
511-
if problem := _check_repo_health(p):
570+
repo_config = Config.load(p)
571+
if problem := _check_repo_health(p, repo_config):
512572
issues.append(f"{p.name}: {problem}")
513573

514574
for hook_warning in _check_git_hooks(p):

src/git_pulsar/daemon.py

Lines changed: 1 addition & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -189,46 +189,6 @@ def is_repo_busy(repo_path: Path, interactive: bool = False) -> bool:
189189
return False
190190

191191

192-
def has_large_files(repo_path: Path, config: Config) -> bool:
193-
"""Scans untracked or modified files for sizes exceeding the limit.
194-
195-
Args:
196-
repo_path (Path): The path to the repository.
197-
config (Config): The configuration instance for this repository.
198-
199-
Returns:
200-
bool: True if a large file is found, False otherwise.
201-
"""
202-
limit = config.limits.large_file_threshold
203-
204-
# Only scan files git knows about or sees as untracked.
205-
try:
206-
cmd = ["git", "ls-files", "--others", "--modified", "--exclude-standard"]
207-
candidates = subprocess.check_output(cmd, cwd=repo_path, text=True).splitlines()
208-
except subprocess.CalledProcessError as e:
209-
logger.warning(f"Large file scan failed for {repo_path.name}: {e}")
210-
return False
211-
212-
for name in candidates:
213-
file_path = repo_path / name
214-
try:
215-
if file_path.stat().st_size > limit:
216-
# Dynamic size formatting (Bytes -> MB)
217-
limit_mb = int(limit / (1024 * 1024))
218-
219-
logger.warning(
220-
f"WARNING {repo_path.name}: Large file detected ({name}). "
221-
"Backup aborted."
222-
)
223-
SYSTEM.notify("Backup Aborted", f"File >{limit_mb}MB detected: {name}")
224-
return True
225-
except OSError as e:
226-
logger.warning(f"Failed to check size of file {name}: {e}")
227-
continue
228-
229-
return False
230-
231-
232192
def prune_registry(original_path_str: str) -> None:
233193
"""Removes a missing repository path from the registry file.
234194
@@ -386,7 +346,7 @@ def run_backup(original_path_str: str, interactive: bool = False) -> None:
386346
return
387347

388348
# Pass config to has_large_files (re-added safety check)
389-
if has_large_files(repo_path, config):
349+
if ops.has_large_files(repo_path, config):
390350
return
391351

392352
try:

src/git_pulsar/ops.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from rich.panel import Panel
1414

1515
from . import system
16+
from .config import Config
1617
from .constants import APP_NAME, BACKUP_NAMESPACE
1718
from .git_wrapper import GitRepo
1819

@@ -598,3 +599,45 @@ def add_ignore(pattern: str) -> None:
598599
except Exception as e:
599600
logger.warning(f"Failed to remove tracked files: {e}")
600601
pass
602+
603+
604+
def has_large_files(repo_path: Path, config: Config) -> bool:
605+
"""Scans untracked or modified files for sizes exceeding the limit.
606+
607+
Args:
608+
repo_path (Path): The path to the repository.
609+
config (Config): The configuration instance for this repository.
610+
611+
Returns:
612+
bool: True if a large file is found, False otherwise.
613+
"""
614+
limit = config.limits.large_file_threshold
615+
616+
# Only scan files git knows about or sees as untracked.
617+
try:
618+
cmd = ["git", "ls-files", "--others", "--modified", "--exclude-standard"]
619+
candidates = subprocess.check_output(cmd, cwd=repo_path, text=True).splitlines()
620+
except subprocess.CalledProcessError as e:
621+
logger.warning(f"Large file scan failed for {repo_path.name}: {e}")
622+
return False
623+
624+
for name in candidates:
625+
file_path = repo_path / name
626+
try:
627+
if file_path.stat().st_size > limit:
628+
# Dynamic size formatting (Bytes -> MB)
629+
limit_mb = int(limit / (1024 * 1024))
630+
631+
logger.warning(
632+
f"WARNING {repo_path.name}: Large file detected ({name}). "
633+
"Backup aborted."
634+
)
635+
system.get_system().notify(
636+
"Backup Aborted", f"File >{limit_mb}MB detected: {name}"
637+
)
638+
return True
639+
except OSError as e:
640+
logger.warning(f"Failed to check size of file {name}: {e}")
641+
continue
642+
643+
return False

tests/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Verifies the "State Reconciliation" engine and primitive operations.
3434
- **Octopus Merges:** Simulates complex multi-head merge scenarios (e.g., merging 3 different machine streams into `main`) to ensure the DAG (Directed Acyclic Graph) is constructed correctly without conflicts.
3535
- **State Management:** Verifies atomic file I/O operations (`set_drift_state`) to ensure cross-process thread safety between the background daemon and foreground CLI.
3636
- **Drift Detection:** Tests the core logic for identifying when remote sessions leapfrog local ones, simulating various network failures and detached HEAD states.
37+
- **Pipeline Blockers:** Validates decoupled checks for oversized files (`has_large_files`), ensuring they safely abort operations and trigger system notifications without polluting the daemon's event loop.
3738

3839
### 5. Configuration Hierarchy (`test_config.py`)
3940

@@ -46,6 +47,7 @@ Ensures the **Cascading Configuration** system behaves deterministically.
4647

4748
Validates the state-aware diagnostic engine and user-facing CLI commands.
4849

50+
- **Dashboard Observability:** Validates the `status` command's rendering of power telemetry (Eco-Mode vs. Critical), dynamic health thresholds, and zero-latency caching for drift/blocker warnings.
4951
- **State vs. Event Correlation:** Tests the `doctor` command by decoupling repository health (state) from daemon logs (events). We mock dynamic lookback windows to verify that naturally resolved transient anomalies are suppressed, while active correlated failures trigger alerts.
5052
- **Environment Simulation:** Uses `tmp_path` and `mocker` to synthesize restrictive `.git/hooks`, offline networks, and Linux `systemd` configurations (`loginctl`) without executing side effects on the host.
5153
- **UI Determinism:** Ensures commands like `status` and `config` parse timestamps and route to standard system editors (`$EDITOR`, `nano`) correctly.

0 commit comments

Comments
 (0)