From 69f77b1e46195073d1dcc375242c5b047bf9a31b Mon Sep 17 00:00:00 2001 From: King Omar Date: Wed, 1 Jul 2026 04:32:53 +1000 Subject: [PATCH] Add admin dashboard + member DB storage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit worker.js: - Store join submissions in new 'members' D1 table (in addition to email) - Add GET /api/admin/members — full member list, password gated - Add GET /api/admin/stats — member counts, by-state breakdown, recent signups, top petitions, site stats; all password gated ('dictator') admin.html: - Password gate → full dashboard - AEC progress bar (X/2000 with % and members-to-go) - Stat cards: total members, this week, site users, proposals, votes - State breakdown with mini bars - Recent signups (last 10, NEW badge for this week) - Top 10 petitions by signature count - Full searchable/filterable/sortable member table with NEW badges - CSV export (respects active search + state filter) Co-Authored-By: Claude Sonnet 4.6 --- admin.html | 399 +++++++++++++++++++++++++++++++++++++++++++++++++++++ worker.js | 61 ++++++++ 2 files changed, 460 insertions(+) create mode 100644 admin.html diff --git a/admin.html b/admin.html new file mode 100644 index 0000000..4b34715 --- /dev/null +++ b/admin.html @@ -0,0 +1,399 @@ + + + + + +RADICAL — Admin + + + + + + + + +
+

RADICAL

+

Admin access only

+
+ + +
+
+
+ + +
+
+
RADICAL · ADMIN
+
+ + + +
+
+ +
+ + +
+
+ AEC Registration Progress + / 2,000 +
+
+
05001,0001,5002,000
+
+
+ + +
+
+
Total Members
+
+
of 2,000 AEC goal
+
+
+
This Week
+
+
new sign-ups
+
+
+
Site Users
+
+
anonymous + logged in
+
+
+
Proposals
+
+
active petitions
+
+
+
Total Votes
+
+
all time
+
+
+ + +
Members by State
+
Loading…
+ + +
+
+
Recent Sign-ups
+
Loading…
+
+
+
Top Petitions by Signatures
+
Loading…
+
+
+ + +
+
All Members
+
+ + + + +
+
+ + + + + + + + + + + + + + +
#  Name ↕Email ↕DOB ↕State ↕Suburb ↕PC ↕PhoneJoined ↕
Loading…
+
+
+ +
+
+ + + + diff --git a/worker.js b/worker.js index c4a1a31..2e27a2b 100644 --- a/worker.js +++ b/worker.js @@ -147,6 +147,10 @@ export default { return await validateLogin(request, env); } else if (path === '/api/join' && request.method === 'POST') { return await handleJoinMovement(request, env); + } else if (path === '/api/admin/members' && request.method === 'GET') { + return await getAdminMembers(request, env); + } else if (path === '/api/admin/stats' && request.method === 'GET') { + return await getAdminStats(request, env); } else if (path === '/api/petition-stats' && request.method === 'GET') { return await getPetitionStats(request, env); } else if (path === '/api/petition-svg' && request.method === 'GET') { @@ -2486,6 +2490,17 @@ async function handleJoinMovement(request, env) { const timestamp = new Date().toLocaleString('en-AU', { timeZone: 'Australia/Sydney' }); + // Store in DB + const memberId = 'member_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5); + try { + await env.DB.prepare(` + INSERT INTO members (id, full_name, dob, address, suburb, state, postcode, email, phone, joined_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `).bind(memberId, fullName, dob || null, address || null, suburb || null, state || null, postcode || null, email, phone || null, Date.now()).run(); + } catch (dbErr) { + console.error('Failed to store member in DB:', dbErr); + } + if (env.RESEND_API_KEY) { await fetch('https://api.resend.com/emails', { method: 'POST', @@ -2523,4 +2538,50 @@ async function handleJoinMovement(request, env) { } catch (error) { return createResponse({ error: 'Failed to process membership application', details: logError(error, { action: 'join_movement' }) }, 500); } +} + +function checkAdminAuth(request) { + const url = new URL(request.url); + const pw = url.searchParams.get('password') || request.headers.get('X-Admin-Password'); + return pw === 'dictator'; +} + +async function getAdminMembers(request, env) { + if (!checkAdminAuth(request)) return createResponse({ error: 'Unauthorized' }, 401, 0); + try { + const members = await env.DB.prepare( + `SELECT id, full_name, dob, address, suburb, state, postcode, email, phone, joined_at + FROM members ORDER BY joined_at DESC` + ).all(); + return createResponse({ total: members.results.length, goal: 2000, members: members.results }, 200, 0); + } catch (error) { + return createResponse({ error: 'Failed to fetch members', details: logError(error, { action: 'admin_members' }) }, 500); + } +} + +async function getAdminStats(request, env) { + if (!checkAdminAuth(request)) return createResponse({ error: 'Unauthorized' }, 401, 0); + try { + const memberCount = await env.DB.prepare(`SELECT COUNT(*) as c FROM members`).first(); + const byState = await env.DB.prepare(`SELECT state, COUNT(*) as c FROM members WHERE state IS NOT NULL GROUP BY state ORDER BY c DESC`).all(); + const recent = await env.DB.prepare(`SELECT full_name, state, suburb, joined_at FROM members ORDER BY joined_at DESC LIMIT 10`).all(); + const proposals = await env.DB.prepare(`SELECT COUNT(*) as c FROM proposals WHERE hidden = 0 OR hidden IS NULL`).first(); + const totalVotes = await env.DB.prepare(`SELECT COUNT(*) as c FROM votes`).first(); + const totalUsers = await env.DB.prepare(`SELECT COUNT(*) as c FROM users`).first(); + const topPetitions = await env.DB.prepare(` + SELECT p.id, p.text, + (SELECT COUNT(*) FROM votes WHERE proposal_id = p.id AND vote_type = 'upvote') as signatures + FROM proposals p WHERE (p.hidden = 0 OR p.hidden IS NULL) + ORDER BY signatures DESC LIMIT 10 + `).all(); + const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000; + const newMembersWeek = await env.DB.prepare(`SELECT COUNT(*) as c FROM members WHERE joined_at >= ?`).bind(weekAgo).first(); + return createResponse({ + members: { total: memberCount.c, goal: 2000, thisWeek: newMembersWeek.c, byState: byState.results, recent: recent.results }, + site: { proposals: proposals.c, votes: totalVotes.c, users: totalUsers.c }, + topPetitions: topPetitions.results + }, 200, 0); + } catch (error) { + return createResponse({ error: 'Failed to fetch stats', details: logError(error, { action: 'admin_stats' }) }, 500); + } } \ No newline at end of file