🎣 Brotherhood Sign In

⚠️ CHECK YOUR SPAM / JUNK FOLDER.
Gmail, Outlook, and Yahoo often filter our sign-in email on the first send. If you don't see it in your inbox within a minute, look in spam and mark it "Not spam" so future ones land in the inbox.

Note: Google sign-in is for administrators only. Brotherhood members — use the email option below to request a sign-in link.

or
New here? Request access →

Request access

⚠️ CHECK YOUR SPAM / JUNK FOLDER.
After you submit, we'll email you a sign-in link. Gmail, Outlook, and Yahoo often filter the first email into spam. If you don't see it in your inbox within a minute, look in spam and mark it "Not spam."

Note: Google sign-up is for administrators only. Brotherhood members — use the email option below.

or
Already approved? Sign in →

⏳ Waiting for approval

Hi — your account is registered. An admin will assign your role shortly. You'll have access as soon as they do.

🎣 Brotherhood Fishing Trip — Signups

September 2026 · $175 per person · live signup & payment tracker

Users

Approve new sign-ups and assign roles. pending = no access. user = read-only. manager = can edit dues / recon. super_user = everything + can manage users.

Loading…

Recent activity

Sign-ins and admin changes — newest first. Hover a row for details.

Loading…

📋 System Map — How this dashboard protects your data

A whiteboard walkthrough of how sign-ins, edits, and audit trails flow through the system. Use this to onboard a new co-admin or to answer "what happens when…?" questions.

1Who can sign in
Member registers
/api/register
→
brotherhood_users
role = pending
→
super_user approves
Users tab
→
role: user / manager / super_user
Roles ladder: pending (no access) → user (read-only) → manager (edit dues/recon) → super_user (everything + manage users)
2How "who's online" is tracked
User opens
dashboard
→
auth.js
requireRole()
↓
PATCH brotherhood_users.last_seen_at = now()
INSERT brotherhood_access_log (action, ip, ua, metadata)
Where you see it: Users tab → "Last seen" column (relative time). Hover for absolute time. Updates on every authed request, so refreshing reflects activity instantly.
3Audit trail — who changed what
💰 Mark paid / unpaid
action: fishing-mark-paid / -unpaid
metadata: { submission_id, name, paid }
🏦 Reconcile a deposit
action: recon-allocate
metadata: { txn_description, txn_amount, txn_date, allocations_count, allocations_total, categories }
📦 Archive a signup
action: fishing-archive / -unarchive
metadata: { submission_id, name, archived }
🔗 Bank alias change
action: recon-alias-create / -delete
metadata: { bank_name, member_id, signup_name }
👤 User role change
action: user-role-change
metadata: { target_email, prior_role, new_role }
📧 Sent sign-in link
action: users-notify
metadata: who it was sent to
Where you see it: Users tab → "Recent activity" panel. Mutation rows tint cream; sign-ins stay white. Filter dropdown narrows the view. Last 200 events.
4Two admins, one transaction — who wins?
10:00
Admin A opens transaction X
stores updated_at = 09:45
Admin B opens transaction X
stores updated_at = 09:45
10:01
Admin B saves
server: 09:45 == current? ✓
bumps updated_at → 10:01
10:02
Admin A saves
server: 09:45 == 10:01? ✗
returns 409 Conflict
↓
Admin A sees:
"⚠ Someone else edited this — reload?"
No silent overwrite
Why this matters: Before this guard, last-write-wins. Admin A's reconciliation would silently overwrite Admin B's — and nobody would know until later. Now the second saver is forced to refresh and re-evaluate.
5How fresh is what I'm seeing?
⏱ Trip Status / Fishing list
Auto-poll every 5 min · refreshes on every recon save · ↻ button forces immediate fetch
⏱ Reconciliation tab
Refreshes after every save · ↻ button forces immediate fetch · no auto-poll (admin-driven)
⏱ Users + Activity
Loads on tab switch · ↻ button forces immediate fetch · last 200 events
If two admins are editing live, the loser of a stale-edit conflict (above) is forced to reload. For non-conflicting work, the 5-minute auto-poll keeps Trip Status / Fishing in sync without manual refresh.
6Where everything lives (Supabase)
brotherhood_users
id · email · name · role · approved_at · approved_by · last_seen_at
brotherhood_access_log
user_id · email · action · ip · user_agent · metadata (jsonb) · created_at
brotherhood_bank_transactions
id · transaction_date · description · amount · notes · updated_at ← stale-edit guard
brotherhood_bank_allocations
transaction_id · category · amount · signup_id · member_id · fiscal_year · month
fishing_signups · fishing_trip_payments
Source of truth for who's signed up and the paid flag.
Bolded fields were added for the audit + concurrency system. Everything else predates this work.
7Fits any screen — phone, tablet, laptop
📱 Mobile (≤ 600px)
Tabs row wraps to 2 lines · auth card has side gutters · Users / Activity tables swipe horizontally · System Map cards stack arrows vertically · 16px input fonts so iOS doesn't auto-zoom
💻 Tablet (600–900px)
All tabs visible on one row · System Map cards 1-column · standard table widths
🖥️ Desktop (≥ 900px)
System Map cards 2-column · Recon dropdowns inline · full table layouts · max content width ~1100px (centered)
🛡️ Safety net
html, body { overflow-x: hidden; } + body { max-width: 100vw; } means even an unexpected wide element can't cause a full-page horizontal scroll
How to test: open the live URL on your phone in portrait + landscape, a tablet, and a laptop. Each tab should fit edge-to-edge without horizontal scroll. If anything spills, that's a bug worth reporting.
8How code changes go live
Code change
index.html or
functions/api/*.js
→
git push to main
GitHub
pvme2013-png/fishing-trip-signups
→
Cloudflare auto-build
~60–90 sec
then live worldwide
Rollback: if a deploy breaks something, Cloudflare Pages → Deployments → click any prior deploy → "Rollback to this deployment". Reverts the live site in seconds. Then fix the bug and push again. Schema changes (Supabase migrations) are NOT reverted by a Cloudflare rollback — those need a separate migration to undo.

Enter Passcode

For the brotherhood only.

Total signed up
—
all-time
Paid
—
—
$ Collected
—
manual paid + reconciled
$ Outstanding
—
remaining to collect
Trip year
—
0 selected
Loading signups…
⚠️ Showing cached signups. Jotform is temporarily unavailable. Paid status and money totals are still live from Supabase.
🔄 Treasurer: Re-sync from Jotform (replaces all signups)
Pulls Jotform's authoritative list and replaces the signup table with real submission IDs. Run this once after the daily quota clears to drop the synthetic CSV IDs. New signups stream in automatically via the webhook — you should rarely need this button.
🛟 Treasurer: Restore signups from a Jotform CSV (emergency)
Use this only if both the webhook AND Jotform's API are unavailable. Paste the Jotform Tables CSV export and the names will appear on the dashboard.
—
Fully paid
—
Partial
—
Not started
$—
Collected of $—
Loading payment status…

Treasurer Access

Different passcode from the fishing trip.

Members
—
active
$ Expected
—
members × $120/yr
$ Collected
—
paid this fiscal year
$ Outstanding
—
still owed
—
Loading dues…
+ Add member
📋 Paste from Excel

Copy rows from your Excel sheet (with a header row) and paste below. Columns should include Name and one column per month (Mar, Apr, …, Feb). Put any non-empty value (e.g., 1, x, $10) in a cell to mark that month as paid.

Treasurer Access

Reconciliation uses the same passcode as Dues.

$ Reconciled
—
allocated to a category
$ Unreconciled
—
still needs allocation
$ Trips
—
$ Dues
—
$ Rental
—
$ Other
—
📋 Paste / import bank CSV

Paste your Truist export — either the raw CSV file contents OR copy the rows from Excel/Numbers (tab-separated). Headers are optional. Duplicates are skipped automatically, so you can paste a full statement every time.

Loading transactions…
🔗 Bank-name aliases

Aliases auto-match bank names to members. Example: "JOYFUL REAL ESTATE LLC" → Rothstein Campbell.

Members Passcode

Same passcode as the fishing trip.

—
Members
—
$ Group Total
—
all contributions this year
$ Trips
—
$ Dues
—
Loading…