Add multi-project sync wrapper and timer docs
This commit is contained in:
@@ -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
|
||||
|
||||
```bash
|
||||
|
||||
Executable
+91
@@ -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())
|
||||
Reference in New Issue
Block a user