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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
+
+
+
+
+
+
+
Top Petitions by Signatures
+
+
+
+
+
+
+
All Members
+
+
+
+
+
+
+
+
+
+
+ | # Name ↕ |
+ Email ↕ |
+ DOB ↕ |
+ State ↕ |
+ Suburb ↕ |
+ PC ↕ |
+ Phone |
+ Joined ↕ |
+
+
+ | 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