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

9.7 KiB
Raw Blame History

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:

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

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.