docs: experiment with generated documentation, let's keep it in git for
All checks were successful
Deploy to K8s / deploy (push) Successful in 8s
All checks were successful
Deploy to K8s / deploy (push) Successful in 8s
now
This commit is contained in:
256
docs/by-claude-opus/web-app.md
Normal file
256
docs/by-claude-opus/web-app.md
Normal 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.*
|
||||
Reference in New Issue
Block a user