Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,4 @@ dmypy.json

# Browser network captures (may contain session cookies / PII)
*.har
.tokensave
14 changes: 14 additions & 0 deletions src/kudosy/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from kudosy.settings import get_settings
from kudosy.sport_types import ALL_SPORT_TYPES, fetch_sport_types, merge_sport_types
from kudosy.store import (
append_run_history,
bootstrap,
log_path,
mark_kudoed,
Expand Down Expand Up @@ -128,6 +129,19 @@ async def _run_job(dry_run: bool | None = None) -> Any:
act["has_kudoed"] = True
write_activity_cache(acts, result.started_at.isoformat())
log.debug("Activity cache updated (%d entries)", len(acts))
# Persist a compact history entry for every completed run
if result:
append_run_history(
{
"started_at": result.started_at.isoformat(),
"finished_at": result.finished_at.isoformat(),
"dry_run": result.dry_run,
"total": result.total,
"would_give": result.would_give,
"given": result.given,
"success": result.success,
}
)
return result
finally:
await client.aclose()
Expand Down
38 changes: 35 additions & 3 deletions src/kudosy/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
read_athlete_avatars,
read_athlete_labels,
read_log,
read_run_history,
read_settings,
read_user_config,
write_activity_cache,
Expand All @@ -51,13 +52,31 @@
async def serve_index() -> HTMLResponse:
"""Serve index.html with versioned asset URLs for cache busting."""
v = __version__
importmap = json.dumps({"imports": {"./i18n.js": f"./i18n.js?v={v}"}})
# All local ES modules are mapped to their cache-busted ?v= variant so that
# a new release immediately invalidates every module in every browser.
_MODULES = [
"./i18n.js",
"./state.js",
"./dom.js",
"./api.js",
"./format.js",
"./schedule-matrix.js",
"./athletes.js",
"./config.js",
"./settings.js",
"./feed.js",
"./status.js",
"./stats.js",
"./tabs.js",
"./main.js",
]
importmap = json.dumps({"imports": {m: f"{m}?v={v}" for m in _MODULES}})
content = (_STATIC_DIR / "index.html").read_text()
content = content.replace('href="styles.css"', f'href="styles.css?v={v}"')
content = content.replace(
'<script src="app.js" type="module"></script>',
'<script src="main.js" type="module"></script>',
f'<script type="importmap">{importmap}</script>\n '
f'<script src="app.js?v={v}" type="module"></script>',
f'<script src="main.js?v={v}" type="module"></script>',
)
return HTMLResponse(content=content, headers={"Cache-Control": "no-store"})

Expand Down Expand Up @@ -420,6 +439,19 @@ async def post_single_kudos(activity_id: str) -> dict[str, Any]:
await client.aclose()


# ── History ───────────────────────────────────────────────────────────────────


@router.get("/api/history")
async def get_history(limit: int = 100) -> list[dict[str, Any]]:
"""Return the *limit* most-recent run-history entries (newest first).

Each entry is a compact dict with: started_at, finished_at, dry_run,
total, would_give, given, success.
"""
return read_run_history(limit=max(1, min(limit, 500)))


# ── Log ───────────────────────────────────────────────────────────────────────


Expand Down
31 changes: 31 additions & 0 deletions src/kudosy/static/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// ── Kudosy UI — api.js ───────────────────────────────────────────────────────
// Thin fetch wrappers: structured-error handling + JSON serialisation.

import { t } from './i18n.js';

export async function fetchJson(url, opts = {}) {
const res = await fetch(url, opts);
if (!res.ok) {
const body = await res.json().catch(() => ({}));
// detail may be a structured {code, message} object or a plain string
const detail = body.detail;
if (detail && typeof detail === 'object' && detail.code) {
const key = `error.${detail.code}`;
const translated = t(key);
// if key not found, fall back to the message field
throw new Error(translated !== key ? translated : (detail.message || `HTTP ${res.status}`));
}
throw new Error(
(typeof detail === 'string' ? detail : null) || body.error || `HTTP ${res.status}`
);
}
return res.json();
}

export async function putJson(url, data) {
return fetchJson(url, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
}
Loading
Loading