Admin Manual
Complete operational guide for Project Syrup administrators.
Roles & Permissions
Every admin account carries exactly one role. Role determines what menu items appear, what API calls succeed, and which confirmation requirements apply.
| Role | Who should have it | Key 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
| Action | super_admin | admin | waffle_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.
Forgot Your Password?
Click Forgot password? on the login page. Enter your username. A single-use reset token is generated.
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.
Navigation
The admin top bar contains the following links, visible based on your role:
| Nav item | Visible to | Description |
|---|---|---|
| Dashboard | All | Active and archived waffle list |
| New Waffle | All | Shortcut to the create waffle form |
| Reports | All | Analytics โ drought list, power buyers, monthly activity, spot velocity |
| Users | All | Buyer Instagram handle registry with search and pagination |
| Admin Tools dropdown | admin, super_admin | Contains: Audit Log (admin+), Admin Users (super_admin only), Server Settings (super_admin only) |
| Your display name (top-right) | All | Profile Settings, My Login History, About, Theme Toggle, Logout |
/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:
- Display Name โ if set and non-empty, it is used as-is
- First Name + Last Name โ if both are set
- First Name alone โ if only first name is set
- 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:
- Active (default) โ non-archived waffles, newest first
- Archived โ waffles you've archived after completion
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:
- Manage โ opens the spot management view
- Edit โ opens the edit form
- Archive / Unarchive โ toggles archived state (admin or higher)
Creating a Waffle
Click New Waffle from the dashboard. Fill in the form fields described below.
Fields & Validation
| Field | Required | Validation | Notes |
|---|---|---|---|
| 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:
| Template | Total Spots | Price per Spot |
|---|---|---|
| Small | 10 | $2 |
| Standard | 25 | $5 |
| Medium | 50 | $3 |
| Large | 100 | $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:
- Title
- Description
- Spot price
- Payment info
- Instagram media links (add, remove, or reorder)
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:
- Type the word
DELETEexactly (case-sensitive) into the confirmation field. - 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.
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:
| Tile | What it counts |
|---|---|
| Available | Spots no one has claimed yet |
| Pending | Spots claimed but payment not confirmed |
| Paid | Spots with confirmed payment |
| Total | Calculated revenue โ paid spots ร spot price (updates live) |
Spot Grid
A grid of numbered buttons โ one per spot. Each button is color-coded by status:
| Status | Color | Displayed info | Clickable? |
|---|---|---|---|
| Available | Green | Spot number only | No (buyers claim these) |
| Pending | Yellow/amber | Spot number + Instagram handle | Yes โ opens pay/release actions |
| Paid | Red | Spot number + Instagram handle | No (use pending claims list to release) |
| Winner | Purple/secondary + ๐ | Spot number | No |
| Loser | Grey, dimmed | Spot number | No |
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:
- Spot number
- Instagram handle of the claimer (
@handle) - Timestamp of when the spot was claimed
- Mark Paid button
- Release button
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:
- The winning spot's status is changed to Winner.
- All other spots with status Paid are updated to Loser.
- The winner's buyer stats (
total_wins) increment by 1. - All losing buyers' stats (
total_losses) increment by 1. - The waffle status changes from Active to Completed.
- A
waffle_completedWebSocket broadcast fires to all connected clients. - A winner banner appears at the top of the manage page.
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.
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:
| Field | Required | Notes |
|---|---|---|
| username | โ | Must be unique. Used at login. Cannot be changed after creation. |
| โ | 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.
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.
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:
- Waffles โ number of waffles created that month
- Spots Claimed โ number of spots claimed (any status) that month
- Revenue โ sum of
spot_pricefor all spots marked paid that 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):
- Waffle Count โ number of waffles in the group
- Avg Time to First Claim โ average hours from waffle creation until the first spot was claimed
- Avg Time to Completion โ average hours from waffle creation until
completed_at(only meaningful for completed waffles)
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
- create_waffle Waffle created โ includes the title
- update_waffle Waffle edited โ includes the new title
- archive_waffle Waffle archived
- unarchive_waffle Waffle restored from archive
- delete_waffle Waffle permanently deleted
- mark_paid Spot marked paid โ includes spot number
- release_spot Spot released back to available โ includes spot number
- set_winner Winner set โ includes winning spot number
- clear_winner Winner cleared
- change_winner Winner changed โ includes new winning spot number
- create_admin New admin created โ includes username and role
- update_admin_role Admin role changed โ includes new role
- update_admin_profile Admin profile updated
- reset_admin_password Another admin's password was reset
- deactivate_admin Admin deactivated โ includes username
- update_whois_settings WHOIS server setting updated โ includes new value
- update_setting Any system setting updated โ includes key and new value
Filters & Export
The audit log page has filter controls:
- Admin โ filter by acting admin (super_admin can see all; admin sees self and waffle_managers)
- Action โ filter by action code (e.g.
mark_paid) - Target type โ filter by entity type (
waffle,spot,admin,settings) - Date range โ from/to timestamps
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:
| Field | Description |
|---|---|
| ip_address | IP address the login came from |
| browser | Browser name parsed from the User-Agent header |
| os | Operating system parsed from User-Agent |
| device_type | Desktop, mobile, or tablet โ parsed from User-Agent |
| ip_org | Organization name from WHOIS lookup (async, may be empty for private IPs) |
| ip_country | Country from WHOIS lookup |
| ip_city | City from WHOIS lookup |
| ip_asn | ASN from WHOIS lookup |
| whois_server | Which 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
- waffle_manager โ own entries only
- admin โ own entries + all waffle_manager entries
- super_admin โ all entries; can also filter by admin
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
| Column | Description |
|---|---|
| Instagram Handle | Clickable link โ opens the public buyer stats page at /buyer/:handle showing their full win/loss history. |
| Created At | When this handle was first seen by the system (first claim, or backfill timestamp). |
| Updated At | When the record was last touched (e.g. a new claim by the same handle). |
Search and pagination
- A search box at the top filters by Instagram handle (substring match). A Clear button appears when a search is active.
- Results are paginated โ 25 per page by default, up to 100 per page via the
per_pagequery param. - The pagination footer shows current page, total pages, and total user count.
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:
| Field | Notes |
|---|---|
| first_name | Used in the display name priority chain |
| last_name | Combined with first_name for nav display |
| Must be unique across all admin accounts. Used for password reset. No automated email is sent. | |
| social_links | Array of platform + handle pairs |
Social links
You can store up to 10 social links. Each link has a platform and a handle. Valid platforms:
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 key | Default | Description |
|---|---|---|
| whois_server | whois.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_hours | 24 |
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_length | 8 |
Minimum character length for admin passwords. Applied to new passwords and password changes. Does not enforce retroactively on existing passwords. |
| audit_retention_days | 90 |
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_days | 90 |
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.
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.