mirror of
https://github.com/kbenestad/ClubLedger.git
synced 2026-06-18 09:44:33 +00:00
fix: remove all non-ASCII characters that cause Windows encoding issues
- PIN input: removed literal bullet placeholder (was the reported gibberish) - Search placeholders: ... -> ... (ASCII) - Custom... period option in statement: uses … entity - app.js/common.js/style.css/bar.js/cashier.js: en/em dashes in comments -> ASCII hyphens; Unicode minus sign U+2212 -> plain hyphen - main.py: arrows and dashes in comments -> ASCII - bar.html/cashier.html: standalone page titles and placeholders fixed Intentional: pound sign in currency defaults stays as UTF-8 (served with Content-Type: text/html; charset=utf-8 and correct HTTP headers). https://claude.ai/code/session_01JuRTR5Xjx8emQsyerBgGU7
This commit is contained in:
parent
59b49a3d3a
commit
ca344e3762
9 changed files with 31 additions and 31 deletions
24
main.py
24
main.py
|
|
@ -1,5 +1,5 @@
|
||||||
"""
|
"""
|
||||||
ClubLedger – Store Credit Web App
|
ClubLedger - Store Credit Web App
|
||||||
Hard defaults live in CONFIG below; everything is overridable via the Admin UI.
|
Hard defaults live in CONFIG below; everything is overridable via the Admin UI.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -96,7 +96,7 @@ DB_PATH = "clubledger.db"
|
||||||
STAFF_FILE = Path(__file__).parent / "staff.json"
|
STAFF_FILE = Path(__file__).parent / "staff.json"
|
||||||
static_dir = Path(__file__).parent / "static"
|
static_dir = Path(__file__).parent / "static"
|
||||||
|
|
||||||
# In-memory sessions: token → {user_id, name, role, expires}
|
# In-memory sessions: token -> {user_id, name, role, expires}
|
||||||
_sessions: dict = {}
|
_sessions: dict = {}
|
||||||
|
|
||||||
# Cached settings merged from CONFIG + DB app_settings
|
# Cached settings merged from CONFIG + DB app_settings
|
||||||
|
|
@ -232,7 +232,7 @@ def migrate_db():
|
||||||
conn.execute("DROP TABLE _ledger_entries_old")
|
conn.execute("DROP TABLE _ledger_entries_old")
|
||||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_ledger_member ON ledger_entries(member_id)")
|
conn.execute("CREATE INDEX IF NOT EXISTS idx_ledger_member ON ledger_entries(member_id)")
|
||||||
|
|
||||||
# --- app_settings: rename allow_negative_balance → overdraft_policy ---
|
# --- app_settings: rename allow_negative_balance -> overdraft_policy ---
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"SELECT value FROM app_settings WHERE key='allow_negative_balance'"
|
"SELECT value FROM app_settings WHERE key='allow_negative_balance'"
|
||||||
).fetchone()
|
).fetchone()
|
||||||
|
|
@ -260,8 +260,8 @@ def seed_admin():
|
||||||
("Administrator", "admin", pw, "admin")
|
("Administrator", "admin", pw, "admin")
|
||||||
)
|
)
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print(" Default admin created → username: admin password: admin")
|
print(" Default admin created -> username: admin password: admin")
|
||||||
print(" Change this immediately in the Admin → Staff Accounts area.")
|
print(" Change this immediately in the Admin -> Staff Accounts area.")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
def refresh_settings():
|
def refresh_settings():
|
||||||
|
|
@ -782,7 +782,7 @@ def _period_bounds(period: str, s: dict,
|
||||||
start = _local(fd); end = _local(td) + timedelta(days=1)
|
start = _local(fd); end = _local(td) + timedelta(days=1)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
start = _local(today); end = start + timedelta(days=1)
|
start = _local(today); end = start + timedelta(days=1)
|
||||||
else: # fallback → today
|
else: # fallback -> today
|
||||||
start = _local(today); end = start + timedelta(days=1)
|
start = _local(today); end = start + timedelta(days=1)
|
||||||
|
|
||||||
fmt = "%Y-%m-%d %H:%M:%S"
|
fmt = "%Y-%m-%d %H:%M:%S"
|
||||||
|
|
@ -869,7 +869,7 @@ def transactions(member_id: int, limit: int = 50, offset: int = 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Print views (no auth – opened as new-tab popups)
|
# Print views (no auth - opened as new-tab popups)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _print_size_script(s: dict):
|
def _print_size_script(s: dict):
|
||||||
|
|
@ -996,7 +996,7 @@ _STMT_PERIODS = [
|
||||||
("last_quarter", "Last quarter"),
|
("last_quarter", "Last quarter"),
|
||||||
("year", "This year"),
|
("year", "This year"),
|
||||||
("last_year", "Last year"),
|
("last_year", "Last year"),
|
||||||
("custom", "Custom…"),
|
("custom", "Custom…"),
|
||||||
]
|
]
|
||||||
|
|
||||||
def _stmt_period_selector(period: str, from_date: str, to_date: str) -> str:
|
def _stmt_period_selector(period: str, from_date: str, to_date: str) -> str:
|
||||||
|
|
@ -1287,7 +1287,7 @@ def remove_staff(name: str, user: dict = Depends(current_user)):
|
||||||
return {"staff": staff}
|
return {"staff": staff}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Admin – staff accounts
|
# Admin - staff accounts
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
@app.get("/admin/staff-accounts")
|
@app.get("/admin/staff-accounts")
|
||||||
|
|
@ -1354,7 +1354,7 @@ def delete_staff_account(account_id: int, user: dict = Depends(admin_user)):
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Admin – app settings
|
# Admin - app settings
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
@app.get("/admin/settings")
|
@app.get("/admin/settings")
|
||||||
|
|
@ -1379,7 +1379,7 @@ def update_admin_settings(body: AppSettingsUpdate, user: dict = Depends(admin_us
|
||||||
return _settings
|
return _settings
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Admin – logo upload
|
# Admin - logo upload
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
@app.post("/admin/logo")
|
@app.post("/admin/logo")
|
||||||
|
|
@ -1400,7 +1400,7 @@ async def upload_logo(file: UploadFile = File(...), user: dict = Depends(admin_u
|
||||||
return {"url": url}
|
return {"url": url}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Config (public – loaded by frontend before login screen shows)
|
# Config (public - loaded by frontend before login screen shows)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
@app.get("/config")
|
@app.get("/config")
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* ClubLedger – main SPA */
|
/* ClubLedger - main SPA */
|
||||||
|
|
||||||
let currentUser = null;
|
let currentUser = null;
|
||||||
let cashierMember = null;
|
let cashierMember = null;
|
||||||
|
|
@ -7,7 +7,7 @@ let editMemberId = null;
|
||||||
let editAccountId = null;
|
let editAccountId = null;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Boot – check session, then either show login or start the app
|
// Boot - check session, then either show login or start the app
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
(async function boot() {
|
(async function boot() {
|
||||||
// Load config first so the login page shows the club name
|
// Load config first so the login page shows the club name
|
||||||
|
|
@ -80,7 +80,7 @@ function populateTimezoneList() {
|
||||||
list.innerHTML = Intl.supportedValuesOf('timeZone')
|
list.innerHTML = Intl.supportedValuesOf('timeZone')
|
||||||
.map(z => `<option value="${z}">`)
|
.map(z => `<option value="${z}">`)
|
||||||
.join('');
|
.join('');
|
||||||
} catch (e) { /* older browser — plain text input still works */ }
|
} catch (e) { /* older browser -- plain text input still works */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startApp() {
|
async function startApp() {
|
||||||
|
|
@ -334,9 +334,9 @@ async function loadCashierStats() {
|
||||||
setCol('statsWithdrawals', 'statsWithdrawalsCount', d.withdrawals, 'stats-negative');
|
setCol('statsWithdrawals', 'statsWithdrawalsCount', d.withdrawals, 'stats-negative');
|
||||||
setCol('statsCharges', 'statsChargesCount', d.charges, 'stats-negative');
|
setCol('statsCharges', 'statsChargesCount', d.charges, 'stats-negative');
|
||||||
const netEl = document.getElementById('statsNet');
|
const netEl = document.getElementById('statsNet');
|
||||||
netEl.textContent = (d.net.negative ? '−' : '+') + d.net.display;
|
netEl.textContent = (d.net.negative ? '-' : '+') + d.net.display;
|
||||||
netEl.className = 'stats-col-value ' + (d.net.negative ? 'stats-negative' : 'stats-positive');
|
netEl.className = 'stats-col-value ' + (d.net.negative ? 'stats-negative' : 'stats-positive');
|
||||||
} catch (e) { /* silently ignore — widgets are non-critical */ }
|
} catch (e) { /* silently ignore -- widgets are non-critical */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Bar – ClubLedger</title>
|
<title>Bar - ClubLedger</title>
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h2>Charge Account</h2>
|
<h2>Charge Account</h2>
|
||||||
<div class="search-row">
|
<div class="search-row">
|
||||||
<input type="text" id="barSearch" placeholder="Search member…">
|
<input type="text" id="barSearch" placeholder="Search member...">
|
||||||
<button class="btn" onclick="barSearchMembers()">Search</button>
|
<button class="btn" onclick="barSearchMembers()">Search</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="barMemberList" class="member-pick-list"></div>
|
<div id="barMemberList" class="member-pick-list"></div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* ClubLedger – bar page */
|
/* ClubLedger - bar page */
|
||||||
|
|
||||||
let barMember = null;
|
let barMember = null;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Cashier – ClubLedger</title>
|
<title>Cashier - ClubLedger</title>
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -41,7 +41,7 @@
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h2>Top Up Account</h2>
|
<h2>Top Up Account</h2>
|
||||||
<div class="search-row">
|
<div class="search-row">
|
||||||
<input type="text" id="cashierSearch" placeholder="Search member…">
|
<input type="text" id="cashierSearch" placeholder="Search member...">
|
||||||
<button class="btn" onclick="cashierSearchMembers()">Search</button>
|
<button class="btn" onclick="cashierSearchMembers()">Search</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="cashierMemberList" class="member-pick-list"></div>
|
<div id="cashierMemberList" class="member-pick-list"></div>
|
||||||
|
|
@ -70,7 +70,7 @@
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h2>Members</h2>
|
<h2>Members</h2>
|
||||||
<div class="search-row">
|
<div class="search-row">
|
||||||
<input type="text" id="memberSearch" placeholder="Search name or number…">
|
<input type="text" id="memberSearch" placeholder="Search name or number...">
|
||||||
<button class="btn" onclick="searchMembers()">Search</button>
|
<button class="btn" onclick="searchMembers()">Search</button>
|
||||||
</div>
|
</div>
|
||||||
<table id="memberTable" class="data-table">
|
<table id="memberTable" class="data-table">
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* ClubLedger – cashier page */
|
/* ClubLedger - cashier page */
|
||||||
|
|
||||||
let cashierMember = null;
|
let cashierMember = null;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* ClubLedger – shared helpers */
|
/* ClubLedger - shared helpers */
|
||||||
|
|
||||||
let cfg = { currency_unit: 'pence', currency_symbol: '£', currency_divisor: 100, club_name: 'ClubLedger' };
|
let cfg = { currency_unit: 'pence', currency_symbol: '£', currency_divisor: 100, club_name: 'ClubLedger' };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h2>Members</h2>
|
<h2>Members</h2>
|
||||||
<div class="search-row">
|
<div class="search-row">
|
||||||
<input type="text" id="memberSearch" placeholder="Search name or number…">
|
<input type="text" id="memberSearch" placeholder="Search name or number...">
|
||||||
<button class="btn" onclick="searchMembers()">Search</button>
|
<button class="btn" onclick="searchMembers()">Search</button>
|
||||||
</div>
|
</div>
|
||||||
<table id="memberTable" class="data-table">
|
<table id="memberTable" class="data-table">
|
||||||
|
|
@ -154,7 +154,7 @@
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h2>Top Up Account</h2>
|
<h2>Top Up Account</h2>
|
||||||
<div class="search-row">
|
<div class="search-row">
|
||||||
<input type="text" id="cashierSearch" placeholder="Search member…">
|
<input type="text" id="cashierSearch" placeholder="Search member...">
|
||||||
<button class="btn" onclick="cashierSearchMembers()">Search</button>
|
<button class="btn" onclick="cashierSearchMembers()">Search</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="cashierMemberList" class="member-pick-list"></div>
|
<div id="cashierMemberList" class="member-pick-list"></div>
|
||||||
|
|
@ -225,7 +225,7 @@
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h2>Charge Account</h2>
|
<h2>Charge Account</h2>
|
||||||
<div class="search-row">
|
<div class="search-row">
|
||||||
<input type="text" id="barSearch" placeholder="Search member…">
|
<input type="text" id="barSearch" placeholder="Search member...">
|
||||||
<button class="btn" onclick="barSearchMembers()">Search</button>
|
<button class="btn" onclick="barSearchMembers()">Search</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="barMemberList" class="member-pick-list"></div>
|
<div id="barMemberList" class="member-pick-list"></div>
|
||||||
|
|
@ -253,7 +253,7 @@
|
||||||
<div class="pin-charge-amount" id="pinAmount"></div>
|
<div class="pin-charge-amount" id="pinAmount"></div>
|
||||||
<div class="pin-member-name" id="pinMember"></div>
|
<div class="pin-member-name" id="pinMember"></div>
|
||||||
<div class="pin-instruction">Please enter your PIN</div>
|
<div class="pin-instruction">Please enter your PIN</div>
|
||||||
<input type="password" id="barPin" class="pin-input" placeholder="••••"
|
<input type="password" id="barPin" class="pin-input"
|
||||||
maxlength="20" autocomplete="off" inputmode="numeric"
|
maxlength="20" autocomplete="off" inputmode="numeric"
|
||||||
onkeydown="if(event.key==='Enter') confirmCharge()">
|
onkeydown="if(event.key==='Enter') confirmCharge()">
|
||||||
<div id="pinMsg" class="msg"></div>
|
<div id="pinMsg" class="msg"></div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/* ClubLedger – GitHub-style theme */
|
/* ClubLedger - GitHub-style theme */
|
||||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
|
@ -43,7 +43,7 @@ nav {
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* On desktop the wrapper is invisible — buttons behave as direct nav children */
|
/* On desktop the wrapper is invisible -- buttons behave as direct nav children */
|
||||||
.nav-tabs { display: contents; }
|
.nav-tabs { display: contents; }
|
||||||
|
|
||||||
.brand {
|
.brand {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue