diff --git a/bincio/serve/routers/budget.py b/bincio/serve/routers/budget.py
new file mode 100644
index 0000000..577f768
--- /dev/null
+++ b/bincio/serve/routers/budget.py
@@ -0,0 +1,131 @@
+"""Budget transparency endpoints (/api/budget)."""
+from __future__ import annotations
+
+import json
+import uuid
+from pathlib import Path
+
+from fastapi import APIRouter, Cookie, Depends, HTTPException, Request
+from fastapi.responses import JSONResponse
+
+from bincio.serve import deps
+from bincio.serve.db import User
+
+router = APIRouter()
+
+_BUDGET_FILE = "budget.json"
+
+
+def _budget_path() -> Path:
+ return deps._get_data_dir() / _BUDGET_FILE
+
+
+def _load() -> dict:
+ p = _budget_path()
+ if not p.exists():
+ return {"monthly_target_eur": None, "entries": []}
+ try:
+ return json.loads(p.read_text(encoding="utf-8"))
+ except (json.JSONDecodeError, OSError):
+ return {"monthly_target_eur": None, "entries": []}
+
+
+def _save(data: dict) -> None:
+ _budget_path().write_text(
+ json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8"
+ )
+
+
+@router.get("/api/budget")
+async def get_budget() -> JSONResponse:
+ return JSONResponse(_load())
+
+
+@router.post("/api/budget/settings")
+async def update_settings(
+ request: Request,
+ _: User = Depends(deps._require_admin),
+) -> JSONResponse:
+ body = await request.json()
+ data = _load()
+ if "monthly_target_eur" in body:
+ v = body["monthly_target_eur"]
+ data["monthly_target_eur"] = round(float(v), 2) if v is not None else None
+ _save(data)
+ return JSONResponse({"ok": True})
+
+
+@router.post("/api/budget/entries")
+async def add_entry(
+ request: Request,
+ _: User = Depends(deps._require_admin),
+) -> JSONResponse:
+ body = await request.json()
+ entry_type = body.get("type")
+ if entry_type not in ("donation", "expense"):
+ raise HTTPException(400, "type must be 'donation' or 'expense'")
+ label = str(body.get("label", "")).strip()
+ if not label:
+ raise HTTPException(400, "label is required")
+ try:
+ amount = round(float(body["amount_eur"]), 2)
+ except (KeyError, TypeError, ValueError):
+ raise HTTPException(400, "amount_eur must be a number")
+ month = str(body.get("month", "")).strip()
+ if len(month) != 7 or month[4] != "-":
+ raise HTTPException(400, "month must be YYYY-MM")
+ note = str(body.get("note", "")).strip()
+
+ entry = {
+ "id": str(uuid.uuid4())[:8],
+ "type": entry_type,
+ "label": label,
+ "amount_eur": amount,
+ "month": month,
+ "note": note,
+ }
+ data = _load()
+ data.setdefault("entries", []).append(entry)
+ _save(data)
+ return JSONResponse(entry, status_code=201)
+
+
+@router.patch("/api/budget/entries/{entry_id}")
+async def update_entry(
+ entry_id: str,
+ request: Request,
+ _: User = Depends(deps._require_admin),
+) -> JSONResponse:
+ body = await request.json()
+ data = _load()
+ entry = next((e for e in data.get("entries", []) if e["id"] == entry_id), None)
+ if not entry:
+ raise HTTPException(404, "Entry not found")
+ if "label" in body:
+ entry["label"] = str(body["label"]).strip()
+ if "type" in body:
+ if body["type"] not in ("donation", "expense"):
+ raise HTTPException(400, "type must be 'donation' or 'expense'")
+ entry["type"] = body["type"]
+ if "amount_eur" in body:
+ entry["amount_eur"] = round(float(body["amount_eur"]), 2)
+ if "month" in body:
+ entry["month"] = str(body["month"]).strip()
+ if "note" in body:
+ entry["note"] = str(body["note"]).strip()
+ _save(data)
+ return JSONResponse(entry)
+
+
+@router.delete("/api/budget/entries/{entry_id}")
+async def delete_entry(
+ entry_id: str,
+ _: User = Depends(deps._require_admin),
+) -> JSONResponse:
+ data = _load()
+ before = len(data.get("entries", []))
+ data["entries"] = [e for e in data.get("entries", []) if e["id"] != entry_id]
+ if len(data["entries"]) == before:
+ raise HTTPException(404, "Entry not found")
+ _save(data)
+ return JSONResponse({"ok": True})
diff --git a/bincio/serve/server.py b/bincio/serve/server.py
index 4c77417..3d2182e 100644
--- a/bincio/serve/server.py
+++ b/bincio/serve/server.py
@@ -55,7 +55,7 @@ app.add_middleware(
CORSMiddleware,
allow_origin_regex=r"https?://localhost(:\d+)?|https://[a-z0-9-]+\.bincio\.org",
allow_credentials=True,
- allow_methods=["GET", "POST", "DELETE"],
+ allow_methods=["GET", "POST", "PATCH", "DELETE"],
allow_headers=["Content-Type"],
)
diff --git a/site/src/layouts/Base.astro b/site/src/layouts/Base.astro
index 044a1f2..acfda8d 100644
--- a/site/src/layouts/Base.astro
+++ b/site/src/layouts/Base.astro
@@ -263,9 +263,9 @@ try {
class="hidden sm:inline text-xs text-zinc-500 hover:text-white transition-colors px-1"
>Ideas
About
+ >Support
Ideas
- About
+ style="color: var(--text-4)">Support
Settings
diff --git a/site/src/pages/about/index.astro b/site/src/pages/about/index.astro
index 6d18530..ad58b0e 100644
--- a/site/src/pages/about/index.astro
+++ b/site/src/pages/about/index.astro
@@ -1,252 +1,6 @@
---
-import Base from '../../layouts/Base.astro';
const baseUrl = import.meta.env.BASE_URL ?? '/';
-const labels = {
- community: 'Community',
- members: 'member',
- members_pl: 'members',
- day: 'day',
- days: 'days',
- invited_by: 'invited by',
- founder: 'founder',
- loading: 'Loading…',
-};
+const target = `${baseUrl}support/`;
---
-
Open-source, self-hosted activity tracking
- -
- - BincioActivity is a free, open-source platform for tracking your outdoor activities — - cycling, running, hiking, and more. It is designed to be self-hosted: you (or someone - you trust) run the server, and your data stays under your control. -
-- Activities are stored in an open JSON format called BAS (BincioActivity Schema), - which is designed to be readable and portable. The platform has no hidden analytics, - no advertising, and no third-party data sharing. -
-- This instance is invite-only. To join, you need an invite link from an existing - member — each link is single-use and tied to a unique code. -
-- Once you have an account, you can generate up to 3 invite links to - share with people you trust. You can manage your invites from the invites page - (requires login). -
-
- When you upload a FIT, GPX, or TCX file, the server converts it to BAS format.
- By default the original source file is also kept in your account's
- originals/ folder.
- You can opt out of this at upload time by unchecking "Keep original file on server".
-
- Keeping originals is recommended during these early stages of the project: if the - processing pipeline improves (better elevation smoothing, speed calculation, lap - detection, etc.) you can re-import your files to take advantage of the changes. - If you chose not to keep originals, you would need to upload the files again manually. -
-- When syncing from Strava, the raw activity data fetched from the Strava API can - similarly be stored locally. This is controlled by an instance-wide setting - configured by the server operator. -
-- BincioActivity is under active development. The data format, processing pipeline, - and server API may change between versions. Breaking changes are possible, especially - at this stage. When they occur, re-importing your original files is the safest way - to bring your data up to date. -
-- There is no guarantee of uptime, data integrity, or forward compatibility for - any particular version. Use this software at your own risk, and keep your own - backups of important data. -
-- BincioActivity is provided "as is", without - warranty of any kind. The authors and server operators accept no responsibility for: -
-- You are responsible for securing your account with a strong password, reviewing - what data you share, and making your own backups. GPS and health data can be - sensitive — think carefully about what you upload and who can see it. -
-- BincioActivity is open-source software. You are free to inspect the code, - self-host your own instance, and contribute improvements. -
-Open-source, self-hosted activity tracking
+ + ++ BincioActivity is a free, open-source platform for tracking your outdoor activities — + cycling, running, hiking, and more. It is designed to be self-hosted: you (or someone + you trust) run the server, and your data stays under your control. +
++ Activities are stored in an open JSON format called BAS (BincioActivity Schema), + which is designed to be readable and portable. The platform has no hidden analytics, + no advertising, and no third-party data sharing. +
++ This instance is invite-only. To join, you need an invite link from an existing + member — each link is single-use and tied to a unique code. +
++ Once you have an account, you can generate up to 3 invite links to + share with people you trust. You can manage your invites from the + invites page + (requires login). +
+
+ When you upload a FIT, GPX, or TCX file, the server converts it to BAS format.
+ By default the original source file is also kept in your account's
+ originals/ folder.
+ You can opt out of this at upload time by unchecking "Keep original file on server".
+
+ Keeping originals is recommended during these early stages of the project: if the + processing pipeline improves (better elevation smoothing, speed calculation, lap + detection, etc.) you can re-import your files to take advantage of the changes. +
++ BincioActivity is under active development. The data format, processing pipeline, + and server API may change between versions. Breaking changes are possible, especially + at this stage. When they occur, re-importing your original files is the safest way + to bring your data up to date. +
++ There is no guarantee of uptime, data integrity, or forward compatibility for + any particular version. Use this software at your own risk, and keep your own + backups of important data. +
++ BincioActivity is provided "as is", without + warranty of any kind. The authors and server operators accept no responsibility for: +
++ You are responsible for securing your account with a strong password, reviewing + what data you share, and making your own backups. GPS and health data can be + sensitive — think carefully about what you upload and who can see it. +
++ BincioActivity is open-source software. You are free to inspect the code, + self-host your own instance, and contribute improvements. +
+