- _server_timezone(): detects IANA timezone from /etc/timezone or
/etc/localtime symlink at startup; used as the CONFIG default
- CONFIG: new "timezone" key set to server's detected timezone
- AppSettingsUpdate: new optional timezone field
- _display_tz(), _fmt_dt(), _now_display() helpers: convert stored UTC
datetimes to the configured timezone for display; falls back to server
local if the setting is empty or the zone name is invalid
- receipt(): transaction timestamp uses _fmt_dt() instead of raw UTC slice
- statement(): row timestamps and "Generated" line use _fmt_dt()/_now_display()
- Admin settings: Timezone text input (IANA name) in General section
- app.js: loadAdminSettings/saveSettings handle timezone field
https://claude.ai/code/session_01JuRTR5Xjx8emQsyerBgGU7
Receipts:
- Font size raised to 11pt base (labels 9pt, amounts 13pt bold)
- Each field now shows LABEL (small, uppercase, gray) above VALUE —
two cells per row in a 1fr/1fr CSS grid, matching the provided samples
- Business header: left column = address lines, right column = Tel/Email/Web
- Charge receipt: STAFF+TRANSACTION / CHARGE+TIME / AMOUNT+BALANCE
- Top-up/Withdrawal receipt: STAFF+TXN / TRANSFER_TYPE+TIME /
AMOUNT+BALANCE / TRANSFER_TYPE+TRANSFER_REF
- Print button moved into the paper-size controls bar
Statement:
- Reduced from 9 to 7 columns: Date, Reference, Type, Venue, Staff,
Amount (+/-), Balance — removes the separate Charge/Credit split
- Amount shown as "+ £X.XX" (green) or "- £X.XX" (red)
- Sub-row shows "Transfer type: X — Ref" for top-ups/withdrawals,
or the note text for charges
Logo:
- New POST /admin/logo endpoint: accepts image upload, saves to
static/logo.{ext}, auto-updates logo_url setting
- New logo_max_width / logo_max_height config fields (default 200×80px)
- Admin branding section: file upload input + max-width/height fields
- python-multipart added to requirements.txt (needed for file upload)
https://claude.ai/code/session_01JuRTR5Xjx8emQsyerBgGU7
Fixes garbled display ("â€"") on Windows where the UTF-8 bytes for U+2014
were being misread as Windows-1252. All em-dash occurrences in index.html,
app.js, and common.js are now expressed as HTML entities.
https://claude.ai/code/session_01JuRTR5Xjx8emQsyerBgGU7
- CONFIG: add business address/contact, logo URL+alignment, venue names,
transaction ref prefix, transfer types list, and 14 localizable receipt
labels plus per-receipt-type footer fields
- DB: add transfer_type and transfer_ref columns to ledger_entries
(init_db + migrate_db); fix duplicate return in pos_user
- Pydantic: extend AppSettingsUpdate with all new settings;
add transfer_type/transfer_ref to TopupRequest and WithdrawalRequest
- /topup and /withdrawal: persist transfer_type and transfer_ref
- /config: return transfer_types as parsed array for frontend dropdowns
- New helpers: _txn_ref(), _logo_html(), _biz_header_html()
- New RECEIPT_CSS with 2-column grid layout; receipt() fully redesigned
with business header, auto-generated TXN reference, separate charge vs
top-up/withdrawal layouts; statement() adds Reference column and
transfer-detail sub-rows
- index.html: Transfer Type + Transfer Reference fields on cashier/
withdrawal panels; admin settings expanded into organized sections
(General, Business Address, Branding, Transactions, Receipt Labels,
Receipt Footers)
- app.js: populateTransferTypes() called on startup and after settings
save; doTopup/doWithdrawal send transfer fields; clearCashierSelection
clears new fields; loadAdminSettings/saveSettings handle all new fields
https://claude.ai/code/session_01JuRTR5Xjx8emQsyerBgGU7
Instead of a per-transaction checkbox at point of sale, the overdraft
override is now a persistent flag on each member, set via the Edit Member
modal by the appropriate role.
Schema:
- members.overdraft_override INTEGER DEFAULT NULL
(NULL = follow global policy, 1 = explicitly allowed, 0 = explicitly blocked)
- migrate_db(): ALTER TABLE members ADD COLUMN ... for existing databases
Charge logic (combining global policy + member flag):
- never: always block (member flag ignored)
- always: always allow (member flag ignored)
- staff_override: allow only if member flag = 1
- admin_override: allow only if member flag = 1 (only admin can set it via UI)
- staff_block: allow unless member flag = 0 (explicitly blocked)
Edit Member modal:
- Shows "Allow overdraft for this member" for staff_override / admin_override
(admin_override hidden from non-admins)
- Shows "Block overdraft for this member" for staff_block
- Hidden for never / always policies
- Checkbox state pre-populated from current member.overdraft_override value
Removed the per-transaction barOverrideRow that was added in the previous
commit — it has been superseded by this per-member approach.
https://claude.ai/code/session_01JuRTR5Xjx8emQsyerBgGU7
Policies:
- never – not allowed (default)
- always – allowed for all charges
- staff_override – default no; staff sees checkbox to allow per charge
- admin_override – default no; only admins see the allow-per-charge checkbox
- staff_block – default yes; staff sees checkbox to block per charge
Backend:
- CONFIG: allow_negative_balance → overdraft_policy: "never"
- migrate_db(): converts old allow_negative_balance setting in app_settings
to the equivalent overdraft_policy value on first startup after upgrade
- ChargeRequest: new optional overdraft_override: bool = False
- POST /charge: full policy logic; admin_override enforced server-side
so a non-admin can't bypass it by sending override=true
- POST /admin/settings: validates policy value before saving
Frontend:
- Admin settings: checkbox replaced by <select> with five options
- Bar form: barOverrideRow (hidden by default); selectBarMember() shows
it with the right label when policy is staff_override / admin_override
(admin only) / staff_block; hidden for never and always
- clearBarSelection() resets the override checkbox and hides the row
https://claude.ai/code/session_01JuRTR5Xjx8emQsyerBgGU7
Cashiers can process credit withdrawals (cash-back) for members:
- Requires member PIN to authorize
- Always checks sufficient balance (overdraft not allowed for withdrawals)
- Appears as 'withdrawal' type in ledger, statement, and on receipt
- Receipt opens automatically, same as top-up/charge
Backend:
- migrate_db() now also recreates ledger_entries with type constraint
extended to include 'withdrawal' (existing rows preserved)
- POST /withdrawal endpoint (cashier_user required)
- Receipt label map updated to include Withdrawal
Frontend:
- Cashier tab now shows two panels side-by-side: Top Up and Withdrawal
- Each panel has its own message area so they don't overwrite each other
- Withdrawal panel includes PIN field; top-up panel unchanged
https://claude.ai/code/session_01JuRTR5Xjx8emQsyerBgGU7
Roles:
- cashier: Members + Cashier (top-up) tabs; POST /topup enforced at API level
- pos-staff: Members + Bar (charge) tabs; POST /charge enforced at API level
- admin: all tabs including Admin panel
Changes:
- migrate_db() recreates staff_accounts with new CHECK constraint and
converts any existing 'staff' rows to 'pos-staff' on startup
- cashier_user / pos_user FastAPI dependencies added; applied to /topup and /charge
- Role dropdowns in admin panel updated to the three new values
- startApp() hides irrelevant tabs per role on login
- doLogout() resets all tab visibility so the next login starts clean
- fmtRole() formats role names ('pos-staff' → 'POS Staff') in the accounts table
https://claude.ai/code/session_01JuRTR5Xjx8emQsyerBgGU7
The login overlay lacked the `hidden` class by default, so on every page
load it was visible and blocking the app. On fresh load with a valid session,
boot() skips showLogin() (no submit handler added), leaving the overlay
covering the screen with a non-functional form. Adding `hidden` to the HTML
and explicitly hiding the overlay in startApp() fixes both paths.
https://claude.ai/code/session_01JuRTR5Xjx8emQsyerBgGU7
Explains why NAT already blocks external access by default, what never
to do (port forwarding, reverse proxy, cloud VPS), how to verify from
outside the network, optional binding to local IP only, and Linux ufw
firewall rules. Renumbers old security notes to Part 8.
https://claude.ai/code/session_01JuRTR5Xjx8emQsyerBgGU7
Existing .venv without packages caused ModuleNotFoundError. pip install
is a no-op when everything is already up to date, so always running it
is safe and ensures deps are present.
https://claude.ai/code/session_01JuRTR5Xjx8emQsyerBgGU7
Creates .venv on first run, installs dependencies, then starts the server.
Subsequent runs skip straight to starting. Avoids externally-managed-environment
errors on Debian/Ubuntu 12+.
https://claude.ai/code/session_01JuRTR5Xjx8emQsyerBgGU7
Auth system
- staff_accounts table: name, username, bcrypt password, role (staff|admin)
- Session tokens in memory (8-hour TTL), httpOnly cookie
- POST /auth/login, /auth/logout, GET /auth/me
- All API endpoints now require a valid session
- Default admin account seeded on first run (admin/admin), printed to console
- Staff name for transactions comes from the session, no more dropdown
Currency input fix
- Amount inputs are now decimal (step=0.01); users enter 1.00 not 100
- Frontend multiplies by cfg.currency_divisor before POSTing
- TopupRequest/ChargeRequest no longer include staff_name (from session)
Admin area (4th tab, admin role only)
- App Settings: club name, currency symbol, major/minor unit names,
divisor, min/max topup, max charge, receipt footer, allow overdraft
- Settings persisted in app_settings DB table; merged with CONFIG defaults
at startup and refreshed after each save
- Staff Accounts: list with edit modal (name, username, password, role,
active flag) and delete; Add Account inline form
- /admin/settings GET/POST, /admin/staff-accounts CRUD
- /config endpoint exposes live settings to frontend on every page load
receipt_footer field rendered on both receipt and statement print views
https://claude.ai/code/session_01JuRTR5Xjx8emQsyerBgGU7
Removed product search field, results list, barProductLookup(),
and selectProduct() from the bar view in index.html, bar.html,
app.js, and bar.js. Backend /products endpoints are unchanged.
https://claude.ai/code/session_01JuRTR5Xjx8emQsyerBgGU7
- / now serves index.html (three-view SPA: Members, Cashier, Bar)
- /cashier and /bar remain as standalone pages (unchanged)
- Members view: Edit button on every row opens a modal to update
name, member number, and optionally PIN. Delete button only appears
when balance is exactly 0; confirmation dialog before deletion removes
the member and their ledger entries.
- PUT /members/{id}: updates any combination of name/member_number/pin;
guards against duplicate member numbers.
- DELETE /members/{id}: rejects with 400 if balance != 0, otherwise
deletes ledger entries then member row.
- Modal styles added to style.css; app.js rebuilt as combined SPA script
(loads common.js for shared helpers).
https://claude.ai/code/session_01JuRTR5Xjx8emQsyerBgGU7
Fix 1: Replace free-text staff name with a dropdown populated from staff.json
via GET/POST/DELETE /staff endpoints. Staff management panel on cashier page
(type name, Add button, chip list with × remove). Dropdown remembers last
selection per session via sessionStorage.
Fix 2: Split single-page app into /cashier (register + top-up + member list +
staff management) and /bar (charge only). Each page is its own HTML file
with two plain <a> nav links; / redirects to /cashier. Shared helpers
extracted to common.js; page logic in cashier.js and bar.js.
Fix 3: Statement view gains an A4/A5 radio toggle that rewrites a dynamic
<style> @page rule before the browser print dialog opens. Defaults to A4.
Fix 4: POST /topup and POST /charge now return entry_id. Each successful
transaction opens /receipt/{entry_id} in a new tab — server-rendered HTML
showing member name/number, type, amount, balance-after (computed as running
sum up to that entry), staff, note, timestamp. Same A4/A5 print toggle.
https://claude.ai/code/session_01JuRTR5Xjx8emQsyerBgGU7