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
|
## Generating a report
|
||||||
|
|
||||||
```bash
|
```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