9.7 KiB
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:
-
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
-
Keyboard navigation — When a member modal is open:
↑/↓arrows navigate between members (respecting search filter)Escapecloses the modal
-
Name search filter — Type in the search box to filter members. Uses diacritic-insensitive matching (e.g., typing "novak" matches "Novák").
-
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:
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:
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:
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:
<!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.