Files
fuj-management/docs/by-claude-opus/web-app.md
Jan Novak 9b99f6d33b
All checks were successful
Deploy to K8s / deploy (push) Successful in 8s
docs: experiment with generated documentation, let's keep it in git for
now
2026-03-11 11:57:30 +01:00

257 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.*