docs: experiment with generated documentation, let's keep it in git for
All checks were successful
Deploy to K8s / deploy (push) Successful in 8s

now
This commit is contained in:
2026-03-11 11:57:30 +01:00
parent e83d6af1f5
commit 9b99f6d33b
17 changed files with 2367 additions and 0 deletions

View File

@@ -0,0 +1,256 @@
# Web Application Documentation
## Overview
The FUJ Management web application is a Flask-based dashboard that provides real-time visibility into club finances. It renders server-side HTML with embedded CSS and JavaScript — no build tools, no npm, no framework. The UI follows a distinctive **terminal-inspired aesthetic** with monospace fonts, green-on-black colors, and dashed borders.
## Routes
### `GET /` — Index (Redirect)
Redirects to `/fees` via an HTML meta refresh tag. This exists so the root URL always leads somewhere useful.
### `GET /fees` — Attendance & Fees Dashboard
**Template**: `templates/fees.html`
Displays a table of all adult members with their calculated monthly fees based on attendance. Each cell shows the fee amount (in CZK), the number of practices attended, or a dash for months with zero attendance.
**Data pipeline**:
```
attendance.py::get_members_with_fees() → Filter to tier "A" (adults)
match_payments.py::fetch_exceptions() → Check for fee overrides
→ Format cells with override indicators
→ Render fees.html with totals row
```
**Visual features**:
- Fee overrides shown in **orange** with the original amount in parentheses
- Empty months shown in muted gray
- Monthly totals row at the bottom
- Performance timing in the footer (click to expand breakdown)
**Template variables**:
| Variable | Type | Content |
|----------|------|---------|
| `months` | `list[str]` | Month labels like "Jan 2026" |
| `results` | `list[dict]` | `{name, months: [{cell, overridden}]}` |
| `totals` | `list[str]` | Monthly total strings like "3750 CZK" |
| `attendance_url` | `str` | Link to the attendance Google Sheet |
| `payments_url` | `str` | Link to the payments Google Sheet |
### `GET /reconcile` — Payment Reconciliation
**Template**: `templates/reconcile.html` (802 lines — the most complex template)
The centerpiece of the application. Shows a matrix of members × months with payment status, plus summary sections for credits, debts, and unmatched transactions.
**Data pipeline**:
```
attendance.py::get_members_with_fees() → All members + fees
match_payments.py::fetch_sheet_data() → All payment transactions
match_payments.py::fetch_exceptions() → Fee overrides
match_payments.py::reconcile() → Match payments ↔ fees
→ Render reconcile.html
```
**Cell statuses**:
| Status | CSS Class | Display | Meaning |
|--------|-----------|---------|---------|
| `empty` | `cell-empty` | `-` | No fee expected, no payment |
| `ok` | `cell-ok` | `OK` | Paid in full (green) |
| `partial` | `cell-unpaid` | `300/750` | Partially paid (red) |
| `unpaid` | `cell-unpaid` | `UNPAID 750` | Nothing paid (red) |
| `surplus` | — | `PAID 200` | Payment received but no fee expected |
**Interactive features**:
1. **Member detail modal** — Click the `[i]` icon next to any member name to see:
- Status summary table (month, attendance count, expected, paid, status)
- Fee exceptions (if any, shown in amber)
- Full payment history with dates, amounts, senders, and messages
2. **Keyboard navigation** — When a member modal is open:
- `↑` / `↓` arrows navigate between members (respecting search filter)
- `Escape` closes the modal
3. **Name search filter** — Type in the search box to filter members. Uses diacritic-insensitive matching (e.g., typing "novak" matches "Novák").
4. **QR Payment** — Hover over an unpaid/partial cell to reveal a "Pay" button. Clicking it opens a QR code modal with:
- A Czech SPD-format QR code (scannable by Czech banking apps)
- Pre-filled account number, amount, and payment message
- The QR image is generated server-side via `GET /qr`
**Client-side data**:
The template receives a full JSON dump of member data (`member_data`) embedded in a `<script>` tag. This powers the modal without additional API calls:
```javascript
const memberData = {{ member_data | safe }};
const sortedMonths = {{ raw_months | tojson }};
```
**Summary sections** (rendered below the main table):
| Section | Shown when | Content |
|---------|-----------|---------|
| Credits | Any member has positive balance | Names with surplus amounts |
| Debts | Any member has negative balance | Names with outstanding amounts (red) |
| Unmatched Transactions | Any transaction couldn't be matched | Date, amount, sender, message |
### `GET /payments` — Payments Ledger
**Template**: `templates/payments.html`
Displays all bank transactions grouped by member name. Each member section shows their transactions in reverse chronological order.
**Data pipeline**:
```
match_payments.py::fetch_sheet_data() → All transactions
→ Group by Person column
→ Strip [?] markers
→ Handle comma-separated people
→ Sort by date descending
→ Render payments.html
```
**Multi-person handling**: If a transaction's "Person" field contains comma-separated names (e.g., "Alice, Bob"), the transaction appears under both Alice's and Bob's sections.
### `GET /qr` — QR Code Generator
Returns a PNG image containing a Czech SPD (Short Payment Descriptor) QR code.
**Query parameters**:
| Parameter | Default | Description |
|-----------|---------|-------------|
| `account` | `BANK_ACCOUNT` env var | IBAN or Czech account number |
| `amount` | `0` | Payment amount |
| `message` | *(empty)* | Payment message (max 60 chars) |
**SPD format**: `SPD*1.0*ACC:{account}*AM:{amount}*CC:CZK*MSG:{message}`
This format is recognized by all Czech banking apps and generates a pre-filled payment order when scanned.
## UI Design System
### Color Palette
| Element | Color | Hex |
|---------|-------|-----|
| Background | Near-black | `#0c0c0c` |
| Base text | Medium gray | `#cccccc` |
| Headings, accents, "OK" | Terminal green | `#00ff00` |
| Unpaid, debts | Alert red | `#ff3333` |
| Fee overrides | Amber/orange | `#ffa500` / `#ffaa00` |
| Empty/muted | Dark gray | `#444444` |
| Borders | Subtle gray | `#333` (dashed), `#555` (solid) |
### Typography
All text uses the system monospace font stack:
```css
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
```
Base font size is 11px with 1.2 line-height — intentionally dense for a data-heavy dashboard.
### Navigation
A persistent nav bar appears at the top of every page:
```
[Attendance/Fees] [Payment Reconciliation] [Payments Ledger]
```
The active page's link is highlighted with inverted colors (black text on green background).
### Shared Footer
Every page includes a click-to-expand performance timer showing total render time and a per-step breakdown.
## Flask Application Architecture
### Request Lifecycle
```
Request → @app.before_request (start timer) → Route handler → Template → Response
│ │
▼ ▼
g.start_time record_step("fetch_members")
g.steps = [] record_step("fetch_payments")
record_step("process_data")
@app.context_processor
inject_render_time()
{{ get_render_time() }}
in template footer
```
### Module Loading
The Flask app adds the `scripts/` directory to `sys.path` at startup, allowing direct imports from scripts:
```python
scripts_dir = Path(__file__).parent / "scripts"
sys.path.append(str(scripts_dir))
from attendance import get_members_with_fees, SHEET_ID
from match_payments import reconcile, fetch_sheet_data, fetch_exceptions, normalize
```
### Environment Variables
| Variable | Default | Purpose |
|----------|---------|---------|
| `BANK_ACCOUNT` | `CZ8520100000002800359168` | Bank account for QR code generation |
| `FIO_API_TOKEN` | *(none)* | Fio API token (used by `fio_utils.py`) |
### Error Handling
The application has minimal error handling:
- If Google Sheets returns no data, routes return a simple "No data." text response
- No custom error pages for 404/500
- Exceptions propagate to Flask's default error handler (debug mode in development, 500 in production)
## Template Architecture
All three page templates share a common structure:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FUJ [Page Name]</title>
<style>
/* ALL CSS is inline — no external stylesheets */
/* ~150-400 lines of CSS per template */
</style>
</head>
<body>
<div class="nav"><!-- 3-link navigation --></div>
<h1>Page Title</h1>
<div class="description"><!-- Source links --></div>
<!-- Page-specific content -->
<div class="footer"><!-- Render time --></div>
<script>
/* Page-specific JavaScript (only in reconcile.html) */
</script>
</body>
</html>
```
There is no shared base template (no Jinja2 template inheritance). CSS is duplicated across templates with small variations.
---
*Web application documentation generated from comprehensive code analysis on 2026-03-03.*