Roles & Permissions

Every admin account carries exactly one role. Role determines what menu items appear, what API calls succeed, and which confirmation requirements apply.

RoleWho should have itKey capabilities
super_admin Owner / technical operator Everything โ€” including admin management, system settings, all login history, and server settings.
admin Trusted staff Full waffle management (create, edit, archive, delete), reports, audit log, login history for self and waffle_managers. Cannot manage other admins.
waffle_manager Helpers who run drops Create and manage waffles, view reports. No archive/delete, no admin management, no audit log, no other users' login history.

Detailed permission matrix

Actionsuper_adminadminwaffle_manager
Create / edit wafflesโœ…โœ…โœ…
Manage spots (pay / release)โœ…โœ…โœ…
Set / change / clear winnerโœ…โœ…โœ…
Archive / unarchive wafflesโœ…โœ…โŒ
Delete waffles (permanent)โœ…โœ…โŒ
View reportsโœ…โœ…โœ…
View audit logโœ…โœ…โŒ
View all login historyโœ…โŒโŒ
View own login historyโœ…โœ…โœ…
View waffle_manager login historyโœ…โœ…โŒ
Create / deactivate adminsโœ…โŒโŒ
Change another admin's roleโœ…โŒโŒ
Reset another admin's passwordโœ…โŒโŒ
System settingsโœ…โŒโŒ
View users registry (top-level nav)โœ…โœ…โœ…

Logging In

Navigate to /admin/login and enter your username and password.

If you are already logged in and visit the login page, you are redirected to the dashboard automatically โ€” you will not see the login form.

Login Lockout

The throttle tracks failed attempts by the combination of IP address + username. After 5 consecutive failed attempts, that IP/username pair is locked out for 15 minutes. The lockout counter is held in memory (not persisted to the database) and is automatically cleaned up after it expires.

A successful login resets the counter for that IP/username pair. If you are locked out, wait 15 minutes for the lockout to expire automatically โ€” there is no manual unlock UI. The lockout is per-IP, so a super_admin cannot unlock your account early; you must wait out the timer or try from a different IP.

โš ๏ธ Lockout is in-memory only
Restarting the app container clears all active lockouts. This is by design โ€” container restarts are not under buyer control.

Forgot Your Password?

Click Forgot password? on the login page. Enter your username. A single-use reset token is generated.

โ„น๏ธ No email delivery
Project Syrup has no email server. The forgot-password API response intentionally does not expose the token. A super_admin must look up the token in the database and share it with you out-of-band, or use the Reset Password button in Admin Management to set a new password directly.

Reset tokens expire after 1 hour and are single-use. Once used, they cannot be replayed even if the expiry hasn't passed. To complete the reset, navigate to /admin/reset-password and submit the token and new password.

The admin top bar contains the following links, visible based on your role:

Nav itemVisible toDescription
DashboardAllActive and archived waffle list
New WaffleAllShortcut to the create waffle form
ReportsAllAnalytics โ€” drought list, power buyers, monthly activity, spot velocity
UsersAllBuyer Instagram handle registry with search and pagination
Admin Tools dropdownadmin, super_adminContains: Audit Log (admin+), Admin Users (super_admin only), Server Settings (super_admin only)
Your display name (top-right)AllProfile Settings, My Login History, About, Theme Toggle, Logout
โ„น๏ธ Login History moved
My Login History is now accessed from the display name dropdown โ†’ My Login History, which links to /admin/settings#login-history. The full admin login history page (admin+ roles) is at Admin Tools โ†’ Login History.

How your name appears in the nav

The top-right button shows your name using this priority:

  1. Display Name โ€” if set and non-empty, it is used as-is
  2. First Name + Last Name โ€” if both are set
  3. First Name alone โ€” if only first name is set
  4. Username โ€” fallback when no profile fields are filled in

Set these fields under Settings โ†’ Profile.

Dashboard

The dashboard is your home screen after login. It shows all waffles and a summary card for each one.

Two tabs control which waffles are shown:

Each waffle card shows: title, price per spot, total spots, status badge (Active / Completed), counts for available / pending / paid spots, a fill progress bar, and an image thumbnail if an image_url is set on the waffle. The nav bar footer also shows total waffle count and active waffle count.

From a waffle card you can click:

Creating a Waffle

Click New Waffle from the dashboard. Fill in the form fields described below.

Fields & Validation

FieldRequiredValidationNotes
titleโœ…Non-empty string Used as the page heading and generates the URL slug (e.g. "Blue Wig #3" โ†’ /waffle/blue-wig-3). Slug is set at creation time and cannot be changed.
total_spotsโœ…Integer โ‰ฅ 2 Creates exactly that many numbered spots (1 through N) in a single transaction. Cannot be changed after creation.
spot_priceโœ…Integer > 0 (cents) Display only โ€” Syrup does not process payments. Stored as an integer (e.g. 5 = $5). Used in revenue totals in reports.
descriptionโŒFree text Optional subtitle shown below the title on the manage page and public waffle page.
payment_infoโŒFree text Optional payment instructions (e.g. Venmo/CashApp username). Displayed to buyers on the public page.
instagram_media_links[]โŒURL array One or more Instagram post links showing the item. Empty strings are stripped out automatically. Rendered as "IG Post" buttons on both admin and public pages.

Quick-fill Templates

The New Waffle form includes four pre-fill buttons to jumpstart common configurations:

TemplateTotal SpotsPrice per Spot
Small10$2
Standard25$5
Medium50$3
Large100$2

Clicking a template fills the Total Spots and Spot Price fields โ€” you still need to enter a title and any media links.

Editing a Waffle

From the dashboard card or the manage view, click Edit. You can change:

โš ๏ธ Total spots is fixed
The number of spots is locked in at creation time. You cannot add or remove spots from a waffle after it is created.

All edits require a valid CSRF token (automatically included in the form). The edit is recorded in the audit log as update_waffle.

Archive & Delete

Archiving

Archive a waffle to move it off the active dashboard without destroying any data. Archived waffles appear under the Archived tab. Buyer stats, activity history, and all spot records are preserved. Only admin and super_admin can archive.

Click Unarchive from the Archived tab to restore it to active if you archived by mistake.

Both actions are recorded in the audit log as archive_waffle / unarchive_waffle.

Permanent Deletion

Permanent deletion requires two confirmations to prevent accidents:

  1. Type the word DELETE exactly (case-sensitive) into the confirmation field.
  2. Enter your current admin password.

Both checks run server-side. If either fails, the delete is aborted and you are redirected back. When it succeeds, the waffle, all its spots, and its activity history are removed permanently. The audit entry for the delete is still written because it is recorded before the deletion occurs.

๐Ÿ—‘๏ธ Permanent and immediate
There is no recycle bin. Archive instead if there is any chance you will need the data again.

CSV Export

Export a waffle's full spot list as a CSV file for external reconciliation or record keeping. The export button appears in the waffle info header on the manage page.

The export is also available without authentication at GET /api/waffles/:slug/export, so you can link buyers directly to a post-draw results file if you choose.

Columns in the export: spot number, status, claimed_by_handle, claimed timestamp, paid timestamp.


Manage View

Click Manage on any waffle card. This is your primary working screen during a live drop. It has five sections: a waffle info header, a stats bar, the spot grid, a pending claims list, and the winner section.

Stats Bar

A row of tiles at the top of the manage view shows real-time counts:

TileWhat it counts
AvailableSpots no one has claimed yet
PendingSpots claimed but payment not confirmed
PaidSpots with confirmed payment
TotalCalculated revenue โ€” paid spots ร— spot price (updates live)

Spot Grid

A grid of numbered buttons โ€” one per spot. Each button is color-coded by status:

StatusColorDisplayed infoClickable?
AvailableGreenSpot number onlyNo (buyers claim these)
PendingYellow/amberSpot number + Instagram handleYes โ€” opens pay/release actions
PaidRedSpot number + Instagram handleNo (use pending claims list to release)
WinnerPurple/secondary + ๐Ÿ†Spot numberNo
LoserGrey, dimmedSpot numberNo

Loser is a derived status set automatically on all paid spots (other than the winner) when a winner is entered. It feeds into buyer win/loss statistics. Buyers do not see the loser label on the public page โ€” they just see the spot as completed.

A WebSocket connection icon in the top-right of the grid shows live connection status. Updates from other admin sessions or buyer claims appear instantly without a page refresh.

Bulk Pay Mode

For fast payment reconciliation, click Bulk Pay Mode in the top-right of the spot grid. The grid enters selection mode: tap any pending spots to select them (they highlight). A footer bar shows how many are selected and a Mark Paid button that sends all selected spots to the pay API simultaneously.

Bulk pay mode is only shown when the waffle is still active (not completed).

Pending Claims List

Below the spot grid, the Pending Claims panel shows every pending spot as a card with:

This is the most efficient way to work through pending claims one at a time as DM payments come in. When there are no pending claims, it shows "No pending claims."

Spot Actions

Mark Paid

Transitions a Pending spot to Paid. Available from both the pending claims list and the spot grid click-handler. A WebSocket broadcast updates all connected clients immediately. Recorded in the audit log as mark_paid.

Release

Transitions a Pending or Paid spot back to Available. Use this when a buyer claims a spot but doesn't pay, or if you accidentally marked the wrong spot paid. Broadcasts a spot update to all clients. Recorded as release_spot.

Winner Flow

The Set Winner section appears at the bottom of the manage page for active waffles. Type the winning spot number and click Set Winner.

What happens when a winner is set:

Recorded in the audit log as set_winner.

Changing or Clearing the Winner

After a winner is set, the manage page shows two additional controls:

Clear Winner

Removes the current winner. The winning spot reverts to Active. All loser spots revert to Paid. Buyer win/loss stats are fully recalculated for all affected spots on the waffle. The waffle status reverts to Active. Fires a winner_cleared WebSocket broadcast. Recorded as clear_winner.

Change Winner

Reassigns the win to a different spot number without clearing first. The old winning spot reverts to Paid, the new spot becomes Winner, and all loser spots are re-evaluated. Buyer stats are recalculated for all affected spots. Fires a winner_changed WebSocket broadcast. Recorded as change_winner.

โš ๏ธ Stats recalculation is synchronous
Changing or clearing a winner triggers an immediate full recalculation of buyer win/loss stats for all spots on the waffle. This is a synchronous database operation โ€” on very large waffles (100+ spots) there may be a brief delay before the API responds.

Admin User Management

Available to super_admin only. Navigate to Admin Tools โ†’ Admin Users. The page shows a table of all admin accounts โ€” active and inactive โ€” with username, email, role, status, and last login. An inline role-permissions guide is shown at the top of the page (collapsed on mobile, always visible on desktop) to help you choose the right role when creating a new admin.

Creating an Admin

Click New Admin and fill in the form:

FieldRequiredNotes
usernameโœ…Must be unique. Used at login. Cannot be changed after creation.
emailโœ…Must be unique. Currently used for password reset lookup and profile only (no automated email).
passwordโœ…Must pass the password policy (see below). The admin should change it on first login.
roleโœ…One of super_admin, admin, waffle_manager. Defaults to admin if an invalid value is submitted.
first_nameโŒOptional โ€” used in the display name priority chain.
last_nameโŒOptional.
display_nameโŒOptional โ€” takes top priority in the nav display name.

All form errors are returned inline on the page (username required, email required, password required, password policy failures). The new admin is created active and with timezone set to UTC by default.

Recorded as create_admin.

Changing a Role

Click the role badge on an admin row in the table. Select the new role from the dropdown. Role changes for demotions (e.g. admin โ†’ waffle_manager) require your current password as confirmation before the API call proceeds.

โš ๏ธ You cannot change your own role
The current admin's own row is marked and role changes are blocked on it. A different super_admin must change your role.

Recorded as update_admin_role.

Resetting Another Admin's Password

Click Reset Password on an admin row. Enter a new temporary password (must pass the password policy). The admin's current session is not invalidated โ€” they will need to log out and back in with the new password. Send the temporary password to them securely and ask them to change it immediately.

โ„น๏ธ Cannot reset your own password here
The API rejects attempts to reset your own password via this route. Use Settings โ†’ Change Password for your own account.

Recorded as reset_admin_password.

Deactivating an Admin

Click Deactivate on an admin row and confirm your password. The account is set to active = false. Deactivated admins cannot log in โ€” authentication returns an "account deactivated" error even with a correct password. Their audit history, login history, and any waffles they created are fully preserved.

To reactivate a deactivated admin, use the API directly (PATCH /api/admin/admins/:id with {"active": true}) โ€” there is no UI reactivation button at this time.

Recorded as deactivate_admin.


Reports

Navigate to Reports in the top nav. Available to all roles. Reports are rendered client-side from JSON endpoints โ€” the page loads first, then data is fetched via JavaScript.

Each report has a date range selector. The default range varies by report.

Drought List

Shows buyers who have been entering waffles but haven't won in a long time. Sorted by longest_drought โ€” the number of days since their last win (buyers who have never won are listed as 99,999 days).

Columns: Instagram handle, total waffle entries in the date range, last entry date, days since last win.

Use this to identify loyal buyers who are overdue for attention โ€” or to reward buyers who've been entering for a long time without a win.

Power Buyers

Ranks buyers by the number of paid spots across all waffles in the date range. Shows your most financially committed participants.

Columns: Instagram handle, total spots claimed (paid), total amount spent, win rate percentage.

Win rate is calculated as: wins รท (wins + losses) ร— 100, rounded to one decimal place. A buyer with no completed waffles (only pending spots) shows 0%. Default limit: top 20 buyers.

Monthly Activity

Aggregates three streams of activity by calendar month:

This is the primary tool for tracking the health and growth of your waffle operation over time.

Spot Velocity

Measures how quickly your waffles fill up, grouped by waffle status (active vs completed):

Useful for understanding buyer demand and deciding how large to make your waffles.


Audit Log

Available to admin and super_admin. Navigate to Admin Tools โ†’ Audit Log.

Every state change performed by any admin is written to the audit_log table. Writes are asynchronous (in a goroutine) so they do not slow down the operation that triggered them. Each entry records: timestamp, the acting admin's ID, the action code, the affected entity type and ID, a human-readable detail string, and the admin's IP address at time of action.

Audited Actions

Filters & Export

The audit log page has filter controls:

Results are paginated (default 20 per page). Click Export CSV to download the current filter results as a CSV file. The export URL includes URL-encoded filter parameters so it is reproducible and shareable.

Audit log entries older than the configured retention period (default 90 days) are automatically purged. Change this under Server Settings.

Login History

Navigate to your username dropdown โ†’ My Login History, or Admin Tools โ†’ Login History for the full admin view.

Each login entry captures:

FieldDescription
ip_addressIP address the login came from
browserBrowser name parsed from the User-Agent header
osOperating system parsed from User-Agent
device_typeDesktop, mobile, or tablet โ€” parsed from User-Agent
ip_orgOrganization name from WHOIS lookup (async, may be empty for private IPs)
ip_countryCountry from WHOIS lookup
ip_cityCity from WHOIS lookup
ip_asnASN from WHOIS lookup
whois_serverWhich WHOIS server was queried (recorded for auditability)

WHOIS lookups are asynchronous with a 10-second timeout โ€” they do not slow down the login flow. Private/RFC1918 addresses (10.x, 172.16โ€“31.x, 192.168.x) are skipped and WHOIS fields are left empty.

Visibility by role

Login history entries older than the configured retention period (default 90 days) are purged automatically. Change this under Server Settings.

Users Registry

Navigate to Users in the top nav. Available to all roles.

The users registry is a deduplicated list of every Instagram handle that has ever claimed a spot. On startup, the app backfills all existing claimed_by_handle values from the spots table into the users table automatically. Every new spot claim upserts the handle if it isn't already present.

Columns

ColumnDescription
Instagram HandleClickable link โ€” opens the public buyer stats page at /buyer/:handle showing their full win/loss history.
Created AtWhen this handle was first seen by the system (first claim, or backfill timestamp).
Updated AtWhen the record was last touched (e.g. a new claim by the same handle).

Search and pagination


Settings โ€” Timezone

Click your username โ†’ Settings. The timezone dropdown accepts any valid IANA timezone string (e.g. America/New_York, Europe/London, Asia/Tokyo). Invalid values are rejected by the server.

All timestamps shown in the admin UI โ€” waffle creation dates, spot claimed/paid times, login history โ€” are displayed in your selected timezone.

Settings โ€” Change Password

From Settings, scroll to the Change Password section. You must provide your current password before a new password is accepted.

Password policy

The minimum password length is configurable (default 8, stored in password_min_length system setting). Passwords are also checked against a blocklist of common passwords. Blocked values include:

password  password1  password123  12345678  123456789
qwerty  qwerty123  admin  admin123  letmein  welcome
welcome123  iloveyou  monkey  dragon  football  baseball
abc123  111111  00000000

Passwords are hashed with bcrypt before storage. The plaintext is never logged or stored.

Settings โ€” Profile & Social Links

From Settings, the profile section allows you to set:

FieldNotes
first_nameUsed in the display name priority chain
last_nameCombined with first_name for nav display
emailMust be unique across all admin accounts. Used for password reset. No automated email is sent.
social_linksArray of platform + handle pairs

Social links

You can store up to 10 social links. Each link has a platform and a handle. Valid platforms:

instagramtiktokxfacebookyoutubediscord

Handles are limited to 100 characters each. Duplicate platforms in the same admin's profile are not allowed. These are stored for your profile only โ€” they are not displayed publicly.

System Settings

super_admin only. Navigate to Admin Tools โ†’ Server Settings. Each setting is stored as a key/value pair in the system_settings table, so changes persist across restarts.

Setting keyDefaultDescription
whois_serverwhois.pwhois.org WHOIS server used for async IP enrichment on admin logins. Must be a reachable hostname. Changes take effect on the next login.
jwt_expiration_hours24 How long admin JWT tokens remain valid (in hours). Changing this does not invalidate existing tokens โ€” they expire at their original time. Must be a positive integer.
password_min_length8 Minimum character length for admin passwords. Applied to new passwords and password changes. Does not enforce retroactively on existing passwords.
audit_retention_days90 Audit log entries older than this many days are purged by the automatic retention job. Set to a higher value to retain more history.
login_history_retention_days90 Login history entries older than this many days are purged. Separate from audit log retention so you can tune them independently.

Changes to any system setting are recorded in the audit log as update_setting (or update_whois_settings for WHOIS specifically), with the old key and new value captured in the details field.

๐Ÿ’ก Settings take effect immediately
All settings are read from the database on every use โ€” there is no in-memory cache with a TTL. A change to password_min_length will apply the next time someone submits a password form. A change to audit_retention_days will apply the next time the retention cleanup job runs.