Add multi-project sync wrapper and timer docs

This commit is contained in:
2026-09-14 14:56:04 +02:00
parent 26041efd8d
commit 83de749b87
2 changed files with 116 additions and 0 deletions
+25
View File
@@ -109,6 +109,31 @@ tmux new -s zed-hours -d 'python3 tools/time_tracking/zed_tracker.py --project-r
--- ---
## Automatic sync (systemd user timer)
`sync_all.py` syncs every configured project in one run. Configuration lives
in `~/.config/zed-hours-sync/`:
| File | Purpose |
|---|---|
| `env` | `ZED_HOURS_SERVER_URL`, `ZED_HOURS_SERVER_TOKEN`, `ZED_HOURS_DEVICE_ID` (keep mode 600) |
| `projects.txt` | One project per line: `PATH [SLUG]`; slug defaults to the directory name |
A systemd user pair drives it:
- `zed-hours-sync.service` (oneshot; runs `sync_all.py` with the env file)
- `zed-hours-sync.timer` (every 15 minutes, `Persistent=true` to catch up after sleep)
Useful commands:
```sh
systemctl --user start zed-hours-sync.service # run a sync now
systemctl --user list-timers zed-hours-sync.timer # next scheduled run
journalctl --user -u zed-hours-sync.service -f # live logs
```
---
## Generating a report ## Generating a report
```bash ```bash
Executable
+91
View File
@@ -0,0 +1,91 @@
#!/usr/bin/env python3
"""Sync every configured Zed Hours project to the server in one run.
Reads a project list from a simple config file (one project per line:
`PATH [SLUG]`), validates the shared ZED_HOURS_* environment once, and
invokes sync.py for each project that has a `.zed-hours/` directory.
Designed for a systemd user timer: per-project failures are reported but do
not stop the remaining projects; the run exits nonzero if any sync failed.
"""
import argparse
import os
import subprocess
import sys
from pathlib import Path
DEFAULT_CONFIG = Path.home() / ".config" / "zed-hours-sync" / "projects.txt"
REQUIRED_SETTINGS = ("ZED_HOURS_SERVER_URL", "ZED_HOURS_SERVER_TOKEN", "ZED_HOURS_DEVICE_ID")
def parse_config(config_path: Path) -> list[tuple[Path, str | None]]:
"""Return (project_root, slug_or_None) pairs from the config file.
Paths with spaces are not supported; keep the `PATH [SLUG]` format simple.
"""
projects: list[tuple[Path, str | None]] = []
for lineno, raw in enumerate(config_path.read_text(encoding="utf-8").splitlines(), 1):
line = raw.strip()
if not line or line.startswith("#"):
continue
parts = line.split()
if len(parts) == 1:
root, slug = parts[0], None
elif len(parts) == 2:
root, slug = parts
else:
raise ValueError(f"{config_path}:{lineno}: expected 'PATH [SLUG]', got: {line}")
root_path = Path(os.path.expanduser(root))
projects.append((root_path, slug))
return projects
def main() -> int:
parser = argparse.ArgumentParser(description="Sync all configured Zed Hours projects.")
parser.add_argument("--config", type=Path, default=DEFAULT_CONFIG, help="Project list file (default: %(default)s)")
args = parser.parse_args()
missing = [name for name in REQUIRED_SETTINGS if not os.environ.get(name, "").strip()]
if missing:
print(f"Missing environment variables: {', '.join(missing)}", file=sys.stderr)
return 2
if not args.config.exists():
print(f"Config file not found: {args.config}", file=sys.stderr)
return 2
try:
projects = parse_config(args.config)
except (OSError, ValueError) as exc:
print(f"Invalid config: {exc}", file=sys.stderr)
return 2
if not projects:
print(f"No projects configured in {args.config}.")
return 0
sync_script = Path(__file__).resolve().parent / "sync.py"
failed: list[str] = []
for root, slug in projects:
if not (root / ".zed-hours").is_dir():
print(f"Skipping {root}: no .zed-hours directory")
continue
command = [sys.executable, str(sync_script), "--project-root", str(root)]
if slug:
command += ["--project-slug", slug]
print(f"--- Syncing {root}" + (f" as '{slug}'" if slug else "") + " ---", flush=True)
result = subprocess.run(command, check=False)
if result.returncode != 0:
failed.append(str(root))
if failed:
print(f"Sync finished with failures: {', '.join(failed)}", file=sys.stderr)
return 1
print("Sync finished OK.")
return 0
if __name__ == "__main__":
raise SystemExit(main())