Add opt-in server sync

This commit is contained in:
2026-09-14 13:14:12 +02:00
parent 2b33045e3f
commit 74d2bfd3a7
2 changed files with 132 additions and 2 deletions
+98 -2
View File
@@ -8,6 +8,7 @@ Usage:
time_track.py timesheet [week|month] -- generate a Markdown timesheet
time_track.py current -- show the active session, if any
time_track.py stop -- stop the active session
time_track.py sync -- upload finished sessions to a server
"""
import argparse
@@ -16,14 +17,19 @@ import os
import shutil
import subprocess
import sys
import uuid
from datetime import datetime, timedelta
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.parse import urlsplit
from urllib.request import Request, urlopen
DATA_DIR = Path(
os.environ.get("TIME_TRACK_DIR", Path.home() / ".local" / "share" / "time_track")
)
DATA_FILE = DATA_DIR / "data.json"
DEFAULT_TASK = ""
SYNC_TIMEOUT_SECONDS = 15
def load_data():
@@ -31,7 +37,22 @@ def load_data():
if DATA_FILE.exists():
try:
with DATA_FILE.open("r", encoding="utf-8") as f:
return json.load(f)
data = json.load(f)
migrated = False
entry_ids = set()
for entry in data.get("entries", []):
try:
entry_id = str(uuid.UUID(entry.get("id", "")))
except (AttributeError, TypeError, ValueError):
entry_id = ""
if not entry_id or entry_id in entry_ids:
entry_id = str(uuid.uuid4())
entry["id"] = entry_id
migrated = True
entry_ids.add(entry_id)
if migrated:
save_data(data)
return data
except (json.JSONDecodeError, OSError):
pass
return {"active": False, "current": None, "entries": []}
@@ -162,6 +183,7 @@ def toggle(task=None):
end = datetime.now()
duration = int((end - start).total_seconds())
entry = {
"id": str(uuid.uuid4()),
"task": data["current"].get("task", ""),
"start": data["current"]["start"],
"end": end.isoformat(timespec="seconds"),
@@ -242,6 +264,77 @@ def status():
print(json.dumps(waybar))
def sync():
"""Upload all finished entries using environment-only configuration."""
server_url = os.environ.get("TIME_TRACK_SERVER_URL", "").rstrip("/")
token = os.environ.get("TIME_TRACK_SERVER_TOKEN", "")
device_id = os.environ.get("TIME_TRACK_DEVICE_ID", "")
missing = [
name
for name, value in (
("TIME_TRACK_SERVER_URL", server_url),
("TIME_TRACK_SERVER_TOKEN", token),
("TIME_TRACK_DEVICE_ID", device_id),
)
if not value
]
if missing:
print(
"Sync is not configured. Set " + ", ".join(missing) + ".",
file=sys.stderr,
)
return 1
parsed_url = urlsplit(server_url)
if parsed_url.scheme not in {"http", "https"} or not parsed_url.netloc:
print(
"Sync is not configured: TIME_TRACK_SERVER_URL must be an absolute HTTP(S) URL.",
file=sys.stderr,
)
return 1
data = load_data()
entries = [
{
"id": entry["id"],
"task": entry.get("task", ""),
"start": entry["start"],
"end": entry["end"],
"duration": entry["duration"],
}
for entry in data["entries"]
]
payload = json.dumps({"device_id": device_id, "entries": entries}).encode("utf-8")
request = Request(
f"{server_url}/api/v1/sync/time-track",
data=payload,
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
},
method="POST",
)
try:
with urlopen(request, timeout=SYNC_TIMEOUT_SECONDS) as response:
status = response.status
response.read()
except HTTPError as error:
print(f"Sync failed: server returned HTTP {error.code}.", file=sys.stderr)
return 1
except URLError as error:
reason = error.reason
print(f"Sync failed: could not reach server ({reason}).", file=sys.stderr)
return 1
except (OSError, ValueError) as error:
print(f"Sync failed: request error ({error}).", file=sys.stderr)
return 1
print(f"Synced {len(entries)} finished entr{'y' if len(entries) == 1 else 'ies'} (HTTP {status}).")
return 0
def current():
data = load_data()
if data["active"]:
@@ -358,6 +451,7 @@ def main():
subparsers.add_parser("status", help="emit Waybar JSON")
subparsers.add_parser("current", help="show the active session")
subparsers.add_parser("stop", help="stop the active session")
subparsers.add_parser("sync", help="upload finished sessions to the configured server")
p_log = subparsers.add_parser("log", help="show recent entries")
p_log.add_argument("count", nargs="?", type=int, default=10)
@@ -381,6 +475,8 @@ def main():
current()
elif args.command == "stop":
stop()
elif args.command == "sync":
return sync()
elif args.command == "log":
log_cmd(args.count)
elif args.command == "timesheet":
@@ -388,4 +484,4 @@ def main():
if __name__ == "__main__":
main()
sys.exit(main() or 0)