From fa14d913596ce217b80e9fb7c0ade53064fe9098 Mon Sep 17 00:00:00 2001 From: Davide Scaini Date: Wed, 3 Jun 2026 10:34:18 +0200 Subject: [PATCH] feat: Support page with budget transparency (replaces About) --- bincio/serve/routers/budget.py | 131 ++++++ bincio/serve/server.py | 2 +- site/src/layouts/Base.astro | 8 +- site/src/pages/about/index.astro | 252 +---------- site/src/pages/support/index.astro | 688 +++++++++++++++++++++++++++++ 5 files changed, 827 insertions(+), 254 deletions(-) create mode 100644 bincio/serve/routers/budget.py create mode 100644 site/src/pages/support/index.astro 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 + >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/`; --- - -
-
-

About BincioActivity

-
- EN - IT - ES - CA -
-
-

Open-source, self-hosted activity tracking

- -
- Satispay QR code — @brutsalvadi -
- -
- - - - -
-

What is this?

-

- 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. -

-
- -
-

Joining & invitations

-

- 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). -

-
- -
-

Your data on this server

-

- 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. -

-
- -
-

Early-stage software

-

- 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. -

-
- -
-

Disclaimer

-

- BincioActivity is provided "as is", without - warranty of any kind. The authors and server operators accept no responsibility for: -

-
    -
  • Loss, corruption, or unauthorised access to your activity data
  • -
  • Data exposed through misconfiguration of the server or infrastructure
  • -
  • Inaccuracies in computed statistics (distance, elevation, heart rate, etc.)
  • -
  • Any consequences of acting on information displayed by this application
  • -
-

- 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. -

-
- -
-

Open source

-

- BincioActivity is open-source software. You are free to inspect the code, - self-host your own instance, and contribute improvements. -

-
- -
-
- - - + + diff --git a/site/src/pages/support/index.astro b/site/src/pages/support/index.astro new file mode 100644 index 0000000..2710070 --- /dev/null +++ b/site/src/pages/support/index.astro @@ -0,0 +1,688 @@ +--- +import Base from '../../layouts/Base.astro'; +const baseUrl = import.meta.env.BASE_URL ?? '/'; +--- + +
+

Support

+

Open-source, self-hosted activity tracking

+ + +
+ + + +
+ + +
+
+ EN + IT + ES + CA +
+ + + + +
+
+

What is this?

+

+ 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. +

+
+ +
+

Joining & invitations

+

+ 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). +

+
+ +
+

Your data on this server

+

+ 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. +

+
+ +
+

Early-stage software

+

+ 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. +

+
+ +
+

Disclaimer

+

+ BincioActivity is provided "as is", without + warranty of any kind. The authors and server operators accept no responsibility for: +

+
    +
  • Loss, corruption, or unauthorised access to your activity data
  • +
  • Data exposed through misconfiguration of the server or infrastructure
  • +
  • Inaccuracies in computed statistics (distance, elevation, heart rate, etc.)
  • +
  • Any consequences of acting on information displayed by this application
  • +
+

+ 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. +

+
+ +
+

Open source

+

+ BincioActivity is open-source software. You are free to inspect the code, + self-host your own instance, and contribute improvements. +

+
+
+
+ + + + + + + +
+ + +