Admin Login
Welcome to Event Setup. Get your competition weekend live in 4 simple steps.
π Before you continue:
1. Open your Scoring Program and go to Admin β Generate Emails. Save the Studio.txt file.
2. While in the Scoring Program, also save the "Program by Competition for Printer" PDF β you'll upload it in Step 2.
3. Go to the Import Studios tab and upload your Studio.txt to link studios to the event.
4. Once studios are imported, come back here and complete the steps below.
Choose the competition weekend you're setting up
The "Program by Competition for Printer" from your Scoring Program. This powers "Find Your Routine" on /live and validates your uploads.
Parsing PDF...
Add custom stage URLs, or leave empty to use the default starzdancecomp.com stream.
Leave empty = defaults to starzdancecomp.com livestream page. Supports Rumble, YouTube, Vimeo β any embed URL.
Should parents browsing /live be able to see uploaded photos & videos without an access code?
Your event is configured and ready.
QR Code URL (print & laminate)
media.turnthat.com/live
Paste your studio assignment text file. Auto-creates accounts, links to event, maps studio numbers.
Select an event
Select event + type, then drop your folder. Studio # subfolders auto-map using your imported studio list.
Media/Critiques: folder with studio # subfolders (1/, 2/, 3/...)
Awards: any files β goes to private admin storage
Upload shared photos visible to ALL studios and /live visitors. Drag a folder β subfolders preserved.
Drag & drop a folder here
Subfolders like "Awards" and "Improv" appear separately on /live
Uploading...
Drop your final compiled backup. Checks every file against S3 and the audit log β skips duplicates, skips files you intentionally deleted/renamed, and only uploads what's genuinely missing.
We'll scan everything, check audit log, and show you exactly what needs uploading.
Select File Manager tab to browse S3...
Navigate to the destination folder, then click "Move Here"
Find misfiled uploads. Drag a box around them. Mass delete or move.
No scan yet.
| Name | Contact | Created |
|---|
Manually link a studio and assign their folder number for uploads. This is the number that matches the folder name in your upload (e.g. folder "11" = studio #11).
| Name | Date | Location | Studios / Media | Status |
|---|
Configure what appears on the public event page (/live). People scan your QR code β land here.
Add stages for multi-room events. Leave empty to use default starzdancecomp.com stream. Works with Rumble, YouTube, Vimeo β any embed URL.
π QR Code Link
Print this as a QR code for your event β people scan it and get instant access to photos, videos, livestream, and schedule.
Bucket: starz-media-vault. Final path: starz-media-vault/[prefix]/
/live?topshot=1. Test a real charge with your own card, refund yourself in Square dashboard, then check "Public visible" to make it live for everyone.
For in-person payments. One buyer can purchase multiple photos across multiple routines. Worker copies photos automatically within 1 minute.
Tap a photo to add/remove. Tap the eye to preview full size.
Send a branded email to every studio linked to an event. Includes their login URL, credentials, and instructions for sharing with families.
Parents subscribe on the /live page to get emailed when their kid's routine is coming up. Manage and reset here.
Create accounts for trusted employees. They can upload media and browse files for events you assign them to.
Loading...
Checks every routine for missing judge critiques (MP3 audio + MP4 video per judge). Shows exactly what's missing per studio, and lets you export HD videos for routines that need re-judging.
Find duplicate videos (-001 retries), corrupt files, and leftover .tmp files in one scan.
Run during or after each competition day. Checks every routine that should have performed by now β flags missing photos, missing videos, misfiled folders, and swapped numbers. Only checks routines scheduled up to the current time.
View the full backstage performance log for any event. Shows every routine that went on stage, in order, with timestamps. Ground truth for fixing mislabeled videos.
Guided step-by-step fixer. Resolves duplicates first (they cascade into fixing everything else), then misfiled, then missing. Uses backstage queue, JPG chain, and your eyes.
Upload a routine video and generate a rejudge link for a specific judge. The judge records their critique, and you get notified by email when it's done.
πΉ Drop video here or
Manage judge profiles, credentials, private info, and track earnings.
Loading...
Generate single-use codes for studios. Each code allows one free critique submission.
Click refresh to load codes.
All submitted critiques β queue, in progress, and completed.
Click refresh to load.
Controls where photos and videos are served from. Auto mode lets the system detect NAS availability.
When enabled, /live and /studio show a maintenance message instead of media.
Updated May 2, 2026 β Click any section to expand. See π May 1 + May 2 Updates cards below for latest changes.
Server (AWS EC2):
πΉ Instance: t3.large (2 vCPU, 8GB RAM) β ~$60/month
πΉ Elastic IP: 3.131.5.193
πΉ OS: Ubuntu 22.04 / PHP 8.1 / MySQL / Nginx
πΉ SSL: Let's Encrypt auto-renew via certbot
πΉ ffmpeg: Installed for re-judge video merging
πΉ Web root: /var/www/starz/public/
πΉ Config: /var/www/starz/config.php
πΉ DB: turnt_arena (shared with TURNT Arena, starz_ prefix tables)
Storage β S3:
πΉ Bucket: starz-media-vault (us-east-2)
πΉ CloudFront CDN: d1udxlxsuq202.cloudfront.net (backup only β NAS is primary)
πΉ All viewing AND downloads go through NAS. Uploads go direct to S3. PDFs use presigned S3 URLs.
πΉ S3 egress: near zero β only PDFs, uploads, admin QA checks
Storage β NAS (Synology DS925+):
πΉ Hardware: 4Γ4TB WD Red Plus (SHR ~12TB usable) + 32GB RAM + 1TB WD Black NVMe
πΉ NIC: 2.5GbE (confirmed 2500Mb/s via ethtool)
πΉ Location: Home with T-Mobile 2Gbps fiber (static IP)
πΉ DSM: 7.3.2 β Web Station (PHP 8.1 enabled, nginx backend)
πΉ Public IP: 66.33.15.170 (T-Mobile static β confirmed not CGNAT)
πΉ Local IP: 192.168.1.134 (DHCP reservation)
πΉ Domain: files.turnthat.com β 66.33.15.170
πΉ SSL: Let's Encrypt on NAS (renewed April 3, 2026)
πΉ CloudSync: S3 β NAS (download only, syncs automatically)
πΉ Port forwards: 22, 80, 443, 5001 TCP β 192.168.1.134
πΉ Router: T-Mobile gateway at 192.168.1.1 (admin/a1b5c1ae, SSID CXNK01D2ADBE)
πΉ SSH: ssh Twisted701@66.33.15.170
πΉ QuickConnect: quickconnect.to/twisted701
πΉ Web Station service: "files" (native script language, PHP 8.1, nginx)
πΉ Web Station portal: 740033ca-0448-4243-9c33-4c7a8510d68b
πΉ Web Station PHP service: 28b01abb-817b-4d4d-9e83-79253fe9d701
πΉ CORS config: /usr/local/etc/nginx/conf.d/28b01abb-817b-4d4d-9e83-79253fe9d701/user.conf
πΉ ZIP script: /volume1/media/zip.php β builds ZIPs from local disk, streams directly to browser
πΉ Document root: /volume1/media
πΉ Timeouts: connection 60s, send 3600s, read 3600s (for large ZIP downloads)
πΉ Benchmark: EC2βNAS 155MB video in 1.4s = ~920 Mbps (limited by EC2 network, NAS can do 2.5Gbps)
πΉ Purpose: Serve ALL media viewing + downloads + ZIP downloads. Zero EC2 media bandwidth.
πΉ β οΈ CORS note: When Web Station service changes, CORS user.conf path changes. Check /var/tmp/nginx/test/plugin_config/ for current service ID.
DNS (GoDaddy):
πΉ media.turnthat.com β 3.131.5.193 (EC2)
πΉ files.turnthat.com β 66.33.15.170 (NAS)
πΉ play.turnthat.com β 3.131.5.193 (TURNT Arena)
πΉ book.turnthat.com β 3.131.5.193
β οΈ Costs (after NAS migration):
πΉ EC2 t3.large: ~$60/month
πΉ S3 Storage (~863GB): ~$20/month
πΉ EC2 EBS: ~$15/month
πΉ S3 egress: ~$0-2/month (PDFs + admin QA only)
πΉ Total: ~$95-100/month (was $150+ with CloudFront + S3 egress)
Pre-Setup:
π Admin β Generate Emails β save Studio.txt
π Program by Competition for Printer β save schedule PDF
In Admin:
1οΈβ£ Import Studios β upload Studio.txt, set event label & year
2οΈβ£ Event Setup β upload schedule PDF β configure livestream β set media visibility
3οΈβ£ Set event status to Active (enables overlay auto-detect)
4οΈβ£ Upload Media β desktop app uploads to S3
5οΈβ£ QA Inspector β Daily Media Report + Critique Validator + Temp Cleanup
6οΈβ£ Notify Studios β sends login email via SendGrid
During Event:
π€ Scorer Station records critiques β uploads to S3
π‘ Backstage advances routines β Now Performing overlay updates
πΊ Production staff manages livestream embed URLs
Post-Event:
π Run QA report β fix misfiled videos, missing photos, timestamp mismatches
π€ Custom Judge Critiques β upload video, generate judge link
π§ Resend studio emails individually from Studio Activity
starz-media-vault/
_Deleted/ β soft-deleted files (auto-cleaned after 7 days)
2026/
Des Moines/
Studio Name(#1)/
Media/
0041/ β routine photos folder
IMG_101528.JPG
1-041 - 07 March 2026 - 05-18-36 PM.mp4 β video
Critiques/
Judge_1_RoutineName.mp3 β audio
Judge_1_RoutineName.mp4 β merged video (480p, 15% original audio)
scoring.pdf
Custom/ β custom judge critiques
StudioNum/
RoutineName.mp4 β uploaded HD video
_Deleted/
Daily Media Report:
πΉ Missing videos, missing photos, misfiled videos (wrong studio folder)
πΉ Timestamp mismatch detection (Β±60min window) with rename/keep options
πΉ Duplicate JPG detection across studios with overlap delete button
πΉ Wrong-date video detection (outside event dates Β±1 day)
πΉ Photo/video preview, delete buttons per issue
πΉ Confirmed scratches from starz_scratches table (gold) vs likely scratched (gray)
πΉ Ignore system saved to audit log
πΉ "β This IS #X β Keep It Here" button for false positives
πΉ folderByRoutine prefers correct studio per schedule when duplicates exist
Critique Validator:
πΉ 5-pass name matching + saved pairings
πΉ Skips confirmed scratches (loads from starz_scratches)
πΉ Per-routine list per judge with video-ready status
πΉ "β οΈ Upload video to enable rejudge" warnings
πΉ Generate rejudge links per judge with all missing routines
Video Checker:
πΉ Finds duplicate videos (original + -001 retry from ffmpeg)
πΉ Compare sizes, preview both, delete corrupt one
πΉ "Delete All Corrupt" bulk button
Temp File Cleanup:
πΉ Scan for .tmp files (including .tmp.mp4 from interrupted recordings)
πΉ Manual delete button + automatic hourly cron cleanup
Custom Judge Critique:
πΉ Select event β studio β type routine name β pick judge β upload HD video
πΉ Video uploads to S3: Year/EventLabel/Critiques/Custom/StudioNum/RoutineName.ext
πΉ Generates rejudge link (/rejudge/TOKEN)
πΉ Judge watches video, records critique β server merges via ffmpeg
πΉ Saves Judge_X_RoutineName.mp3 + .mp4 to studio's Critiques/ folder
πΉ Email notification via SendGrid when judge finishes
How It Works:
1οΈβ£ Admin runs Critique Validator β sees missing critiques per judge
2οΈβ£ Clicks "Generate Link for Judge X" β creates rejudge session in DB
3οΈβ£ Session contains: judge label, list of routines with videoKey references
4οΈβ£ Judge opens /rejudge/TOKEN on any device (phone, laptop, tablet)
Judge Flow:
πΉ Welcome screen β shows routine count, judge name
πΉ Mic Check β records 3 seconds, plays back for verification
πΉ Judging screen β video plays at FULL volume, mic records simultaneously
πΉ Review β listen to recording, redo or submit
πΉ Auto-advances to next routine
Server Processing (on submit):
πΉ Receives audio as WebM blob
πΉ Converts to MP3 via ffmpeg
πΉ Downloads video from S3 (uses precache β video downloaded while judge watches)
πΉ ffmpeg merges: original video (15% audio) + judge recording (100% audio) β 480p MP4
πΉ Uploads Judge_X_RoutineName.mp3 and .mp4 to Critiques/ folder on S3
πΉ Marks routine as completed in session
Custom Critique (single routine):
πΉ Admin uploads HD video via QA β Custom Judge Critique tool
πΉ Stored in starz_custom_critiques table (token, video key, studio, routine name)
πΉ Same judge flow β /rejudge/TOKEN works for both regular and custom
πΉ Info endpoint checks custom_critiques table first, then rejudge_sessions
πΉ Submit endpoint handles both β custom saves to studio's Critiques/ folder
πΉ Sends email via SendGrid when complete
Key Tables:
πΉ starz_rejudge_sessions: token, judge_label, routines (JSON), completed (JSON), status
πΉ starz_custom_critiques: token, event_id, studio_num, studio_name, routine_name, judge_num, video_s3_key, critique_mp3_key, critique_mp4_key, notify_email, status
Key Endpoints:
πΉ rejudge/TOKEN/info β session data (checks custom first, then regular)
πΉ rejudge/TOKEN/video-url β presigned S3 URL for video playback
πΉ rejudge/TOKEN/precache β downloads video to server cache while judge watches
πΉ rejudge/TOKEN/submit β receives audio, ffmpeg merge, upload to S3
Components:
πΉ /live/overlay.php β standalone overlay for vMix/OBS (1920Γ1080, transparent bg)
πΉ /live/embed.php β embeddable player with overlay for WordPress/external sites
πΉ /live page β overlay built into livestream viewer
Overlay Shows:
πΉ Now Performing: routine #, name, studio, division
πΉ Ahead/behind schedule timing
πΉ Up Next: next 2-3 routines
πΉ Custom text from backstage (e.g. "IMPROV")
πΉ Polls /api/public/now-performing every 3 seconds
Embed Page (embed.php):
πΉ Loads stream URL from event config (supports Rumble, YouTube)
πΉ Overlay sits below video (not on top β avoids blocking player controls)
πΉ Custom fullscreen button (top-right) β fullscreens video + overlay together
πΉ Hidden on mobile (mobile uses native Rumble fullscreen)
πΉ Auto-detects active event if no event_id specified
URLs:
πΉ /live/overlay.php?stage=A β vMix/OBS browser source
πΉ /live/embed.php?event_id=X&stage=A β WordPress embed
πΉ /live/embed.php?stage=A β auto-detect active event
Production Staff Login:
πΉ Username: production / Password: starz1 (desktop_role='production' in starz_staff)
πΉ Sees ONLY the livestream setup page β no admin, no QA, no files
πΉ Workflow: select event β paste Rumble/YouTube URL β save β copy embed code
πΉ Auto-converts Rumble page URLs and embed scripts to iframe URLs
πΉ WordPress instructions + copy button for iframe embed code
Event Status:
πΉ Events tab has status dropdown: Active / Upcoming / Completed / Cancelled
πΉ Active events show overlay links and embed code copy buttons
πΉ public/active-event endpoint returns the active event for auto-detect
Modes:
πΉ Upload Machine: uploads photos/videos/critiques from local folders to S3
πΉ Scorer Station: monitors judge critique recordings in real-time
πΉ Monitor: remote view of scorer status + scratch management
Upload Machine:
πΉ Select event β pick folder β scans files β S3 verification runs in BACKGROUND
πΉ UI is usable immediately β no blocking scan overlay
πΉ Status shows: "15,200 pending β verified 5,000/20,458"
πΉ Deleted files (via QA) return -1 from check-exists β skipped, not re-uploaded
πΉ Temp files (.tmp), files <5 seconds old, files with .tmp companion are skipped
πΉ Quarantine for small files: audio <500KB, video <2MB β play/upload/skip buttons
Scorer Station:
πΉ Maps 3 judge folders β monitors for new MP3/MP4 files every 10 seconds
πΉ Grid: Routine | Judge 1 | Judge 2 | Judge 3 (β /β/β οΈ per file type)
πΉ Smart alerts: 30-second timer before alerting missing judge files
πΉ Live activity ticker: color-coded per judge
πΉ Progress bar: "47 / 497 (499 scheduled, 2 scratched)"
πΉ Scratch system: dropdown from schedule, syncs to starz_scratches table
Build:
πΉ Electron app β npm start to run, npm run build for .exe
πΉ Source: src/index.html (UI), src/main.js (Node backend), src/preload.js (bridge)
/studio β Studio Portal:
πΉ Login with email/password β see events β media/critiques/codes
πΉ Photos grouped by routine with dancer names from schedule
πΉ Delete moves to _Deleted/ on S3, download via CloudFront/NAS
πΉ Access codes for parents with media/critiques/both filter
/gallery/CODE β Parent Access:
πΉ Same UI as studio but view/download only β no delete, no codes
πΉ Respects code expiration, use count, category filter
/live β Public Event Page:
πΉ QR code landing page: Photos, Videos, Livestream, Schedule, Find Routine, Notify Me
πΉ Livestream: single room goes straight to stream, multi-room shows picker
πΉ Only rooms with URLs configured are shown (empty URLs filtered out)
πΉ Now Performing overlay on livestream viewer
πΉ Stream iframe killed when navigating back (stops background audio)
πΉ Schedule PDF loaded on demand (presigned URL generated per-request)
πΉ Push notifications via web-push + SendGrid email
πΉ Browse S3 bucket with folder tree navigation
πΉ Search: search all S3 files by name (scans entire bucket, 30s timeout)
πΉ Move, rename, delete files (audit logged)
πΉ Bulk select + delete with progress
πΉ Delete moves to _Deleted/ (soft delete) β uploader skips deleted files
Hourly Cron (api/cron/cleanup?key=starz-cleanup-2026):
πΉ Deletes expired sessions (30-day lifetime)
πΉ Cleans rejudge video cache (/tmp/rejudge_cache/) older than 24 hours
πΉ Permanently deletes S3 _Deleted/ files older than 7 days
πΉ Deletes .tmp and .tmp.mp4 files from S3
πΉ Cleans old scorer status data (7+ days)
Server Crontab (root):
πΉ 0 * * * * curl -s "https://media.turnthat.com/api/cron/cleanup?key=starz-cleanup-2026"
Email: SendGrid
πΉ API key in config.php (SMV_SENDGRID_KEY)
πΉ Used for: studio notifications, custom critique completion, push notification emails
πΉ From: [email protected] / Starz Dance Competition
Tables:
πΉ starz_events, starz_studios, starz_event_studios, starz_event_studio_numbers
πΉ starz_sessions, starz_staff, starz_staff_events
πΉ starz_access_codes, starz_schedule_entries
πΉ starz_file_audit, starz_media (legacy)
πΉ starz_rejudge_sessions, starz_custom_critiques
πΉ starz_scratches (event_id, routine_num, routine_name)
πΉ starz_scorer_status (event_id, status_json)
πΉ starz_stage_queue, starz_notify_*, starz_room_chat
New Endpoints (this session):
πΉ public/active-event β returns current active event (for overlay auto-detect)
πΉ public/schedule-url β generates presigned URL for single event's schedule PDF
πΉ admin/set-event-status β change event status (active/upcoming/completed/cancelled)
πΉ admin/create-custom-critique β upload video + create rejudge link
πΉ admin/resend-studio-welcome β resend login email to single studio (test_mode param)
πΉ admin/files/search β search entire S3 bucket by filename
πΉ admin/presign-upload β alias for admin/upload-url (web admin bulk upload)
πΉ cron/cleanup β maintenance endpoint (key=starz-cleanup-2026)
Overview:
πΉ 4th button on /build home page β "π§ Email Studios"
πΉ Upload schedule PDF β pdfplumber parses all routines β groups by studio
πΉ Pulls studio emails from event-studio-map
πΉ Draft/Final version toggle, custom subject + message
πΉ Select all/none studios, send one at a time with progress bar
πΉ Test email sends to [email protected] with [TEST] prefix
πΉ Preview button opens popup with print-friendly styles
πΉ Session caching β remembers last parse on refresh
Per-Studio Email HTML:
πΉ Event header, studio name, routine count, version label
πΉ Routines grouped by Day/Room, sorted by time
πΉ Color-coded levels (S=gold, RS=green, NS=gray in email)
πΉ Award ceremony markers inline in schedule
πΉ TITLE badge (purple) for routines with + in category
πΉ Dancer names listed under each routine
πΉ Print styles: white bg, alternating gray rows, ink-friendly
Backend:
πΉ send_schedule_email action in /build backend (not /api/admin/)
πΉ SendGrid HTML email β no attachments, renders in inbox
πΉ Logs to starz_schedule_emails table
Category Parsing Fix:
πΉ COL_CATEGORY boundary set from "Grp" header right edge + 5px
πΉ Fixed "Contemporary" at x=268.6 being skipped by hard-coded x>270
πΉ cat_full field added as fallback β raw "Contemporary S 15-18" text
Overview:
πΉ Phone-based awards tracker for audience members during ceremonies
πΉ All data stored in localStorage β zero server load
πΉ Gold-bordered button on /live hub (only shows when schedule entries exist)
Selection Modes:
πΉ All Routines β track everything
πΉ Your Studio β pick studio from dropdown
πΉ Choose Routines β search/select specific routines
Two Tabs (matching award ceremony flow):
πΉ π Adjudication β Judges Award, G/HG/P/DE buttons, Category placement X/total
πΉ π High Points β routines grouped by HP pool in read order (youngestβoldest, SoloβProd, NSβRSβS)
πΉ HP tab has level filter: All / New Starz / Rising Starz / Starz
Per-Routine Tracking:
πΉ Judges Award β themed modal prompt, "JA" badge on header
πΉ Adjudication β G/HG/P (+ DE for Starz only), badge on header
πΉ Category Placement β X / total (auto-filled from block count, level-separated)
πΉ High Point Placement β X / top-N (5/10/15/20 from /build thresholds: 24+=10, 49+=15, 75+=20)
πΉ Overall Placement β X / total (RS and S only, not NS)
UX:
πΉ Cards collapsed by default β tap to expand, auto-closes others
πΉ Badges update in real-time on collapsed headers
πΉ atSave() merges with existing data (doesn't wipe other tab's data)
πΉ Themed modals replace native prompt()/confirm()
πΉ Sorting: R# Order or By Room/Day. Filters: Day + Room
Share Results:
πΉ π€ Share button generates trophy-case HTML with medal chips
πΉ html2canvas renders to PNG image at 2x resolution
πΉ Mobile: native share sheet with image file (Instagram/text/email)
πΉ Desktop: downloads PNG file
πΉ Responsive: single column on phone, 2-col tablet, 3-col print/desktop
πΉ Light theme for share (white cards, dark header, color chips)
Level Colors (brand):
πΉ New Starz: pink #d94b7a
πΉ Rising Starz: yellow #f5d623
πΉ Starz: blue #2196F3
Level Column:
πΉ starz_schedule_entries.level β VARCHAR(10), stores NS/RS/S
πΉ pdf.js parser (admin) can't reliably extract level from text items
πΉ Solution: after pdf.js save, /build pdfplumber parser runs on same PDF
πΉ Pdfplumber extracts level reliably β admin/update-schedule-levels UPDATEs the DB
πΉ This happens automatically in setupUploadSchedule() step 4
Division String Format (from pdf.js):
πΉ "Solo 1 Contemporary S 15-18" or "D/T 2 Jazz T 12-14"
πΉ "Producti" β pdf.js splits "Production" across text items
πΉ All regex patterns must handle: Producti(?:on)?|Prod|Production
HP Key Format:
πΉ AGE-DIVISION-LEVEL (e.g. "T-Solo-RS", "S-D/T-S")
πΉ Category block key: division string + "|" + level (separates NS/RS/S)
What it does:
πΉ Shows per-studio balances for any competition β who owes, who paid, who has credit
πΉ Calculates fees from actual registration data (not line items) β routines Γ dancer count Γ pricing
πΉ Verified accurate against coding guy's site ($3,165 for 5-6-7-8 Dance Studio)
Pricing Formula (regionals, level=1):
πΉ Solo: $120/routine Β· D/T: $150/routine (2-3 dancers) Β· Groups: $50/dancer
πΉ Improv: $40/entry Β· Title (Mr/Miss): $40/entry
πΉ Media Fee: $35 Γ unique dancers (per event)
πΉ Nationals (level=2): $130 solo, $170 D/T, $55/dancer groups, $50 improv/title β same media
Early Payment Discount:
πΉ Paid on/before Oct 1 prev year β 15% Β· 120+ days β 10% Β· 90-119 days β 5%
πΉ Applied to REGISTRATION only (not media fee)
πΉ Eligible portion capped at regTotal; excess covers media at full price
πΉ Our calc compared to coding guy's β flagged red if they differ (don't trust his blindly)
Features:
πΉ Sortable, searchable, filterable studio table with per-row fee/payment breakdown
πΉ π§ Remind / β οΈ Dancers / β Payment buttons per row
πΉ π Payment reconciliation (finds missing Authorize.net payments)
πΉ π§ͺ Test email buttons before bulk sends
What it is:
πΉ Full daily clone of coding guy's SQL Server (starz1) into local MySQL (starz1_mirror)
πΉ 118 tables, 5.1 million rows, 139 seconds to sync, 755MB on disk
πΉ 3-layer protection: live mirror + S3 dumps + NAS CloudSync
πΉ If coding guy pulls the plug, you have every routine, dancer, payment, score already
Script Locations:
πΉ /var/www/starz/mssql_mirror.php β sync script
πΉ /var/log/mssql_mirror.log β run log
πΉ s3://starz-media-vault/backups/starz1/latest.sql.gz (67MB compressed)
πΉ Cron: 0 2 * * * (daily at 2am)
Table Naming:
πΉ dbo.Payments_Made β dbo__Payments_Made (schemas flattened with __)
πΉ build.Program β build__Program Β· auth.Transactions β auth__Transactions
Check Commands (SSH into EC2):
# View last 100 lines of sync log:
tail -100 /var/log/mssql_mirror.log
# Sync history (last 10 runs):
mysql -u turnt -pTurntGame2026! starz1_mirror -e \\
"SELECT sync_time, tables_done, total_rows, errors, duration_sec FROM _sync_log ORDER BY id DESC LIMIT 10;"
# Manual sync now:
php /var/www/starz/mssql_mirror.php
# Query the mirror (example):
mysql -u turnt -pTurntGame2026! starz1_mirror -e \\
"SELECT studio_name, email FROM dbo__dance_studios LIMIT 10;"
# Disk usage:
sudo du -sh /var/lib/mysql/starz1_mirror/
Verified Working:
πΉ First sync April 14, 2026: 118/118 tables, 5,123,077 rows, 0 errors
πΉ Biggest tables: build.dance_dancers_routines (1.26M), build.dance_dancers (921K), build.dance_teachers (649K)
π― THE VISION
Build a single unified, multi-tenant, configurable competition management platform that replaces DanceCompetition.exe, the coding guy's registration site, and every janky spreadsheet in the pipeline. Copyright it. Eventually license it to other competition companies (dance, horse shows, gymnastics, baseball, swimming β any event with categories, judges, scoring, scheduling, awards).
Core principle: EVERYTHING IS CONFIGURABLE
πΉ Terminology: "routines" β user sets to "acts", "horses", "entries", "bouts"
πΉ Categories: user creates their own (Ballet, Jazz, Hip Hop β or Western, English, Dressage)
πΉ Pricing: per-category rules, early-payment discounts, bundles β all user-defined
πΉ Group sizes: user defines (Solo=1, Duet=2, etc)
πΉ Judge count: 2, 3, 5, 10 β user picks
πΉ Scoring ranges: user defines (0-100, 0-10, tier-based)
πΉ Age groups: user defines (Mini, Pre-teen, Teen, Adult β or 10U, 12U, 14U)
πΉ Reports: USER BUILDS THEM. Pick fields, sort order, grouping, filters, name it, save it, reuse it
β THE PAIN WE'RE SOLVING
πΉ 20+ hours/week of manual corrections (Dance Unlimited style emails)
πΉ Schedule breaks when categories change (lyrical β jazz = renumber nightmare)
πΉ Download β edit offline β re-import β duplicates and lost assignments
πΉ Studios can't self-correct; they email you spelling errors
πΉ Three databases (Access local, SQL Express judges, MSSQL online) that fall out of sync
πΉ 14-year-old VB.NET code with empty Differences button (literally unfinished)
πΉ SQL injection everywhere in the current code
πΉ Dependent on coding guy who forgets his own code
π THE 5-PHASE WORKFLOW (What We Build Around)
Phase 1: REGISTRATION OPEN (July/Aug β 4 weeks out)
πΉ Studios reserve routines, add placeholders (no dancers yet)
πΉ Admin sets max per event (350, 450, etc) β system enforces hard caps
πΉ Admin can whitelist specific studios over the cap
πΉ State: OPEN
Phase 2: REGISTRATION CLOSED / BUILDING (4 weeks out)
πΉ Registration locks β no studio changes allowed
πΉ Admin builds schedule: sort by day, time, category, keep categories together
πΉ NO NUMBERING YET (this is critical)
πΉ State: LOCKED
Phase 3: CORRECTIONS WINDOW (4 β 2 weeks out)
πΉ Studios submit corrections via portal: name spellings, dancer swaps, category fixes
πΉ Admin approves/rejects each one in a queue
πΉ Approved changes update schedule, still no numbers
πΉ System distinguishes cosmetic (always OK) vs schedule-affecting (warning)
πΉ State: CORRECTIONS_OPEN
Phase 4: FIRST DRAFT PUBLISHED (2 weeks out)
πΉ Schedule numbered, sent individually to each studio
πΉ Wait 3-7 days for studio review
πΉ Studios submit final corrections via portal
πΉ Admin approves/rejects
πΉ State: DRAFT_PUBLISHED
Phase 5: FINALIZED (1 week out β event)
πΉ Final schedule published
πΉ Any late changes get SQUEEZE-IN numbering (334A, 334B, 334C)
πΉ Don't renumber everything β announcer can handle 334 β 334A β 335
πΉ State: FINALIZED β IN_PROGRESS (event day) β COMPLETE
β THE CORRECTION APPROVAL QUEUE
Every studio change request becomes a record:
πΉ Admin sees queue of pending requests
πΉ Batch approve/reject
πΉ Every change logged forever (legal receipt if studio disputes)
πΉ Changes classified:
- COSMETIC (name spelling, dancer add/remove) β auto-approve option
- CATEGORY CHANGE (lyricalβjazz) β blocked after LOCKED state
- AGE/LEVEL CHANGE (Pre-teenβMini) β warning, requires admin approval
- SQUEEZE-IN (new routine after Final) β gets letter suffix (334A)
π οΈ THE 6-MONTH BUILD PLAN (Off-season 2026)
Month 1-2 (June-July 2026): Foundation
πΉ Decompile & document DanceCompetition.exe (DONE β source saved)
πΉ Design new unified MySQL schema (companies, events, studios, routines, dancers, corrections, scores, etc)
πΉ Build auth system (studios log in β migrate existing credentials from dbo__dance_studios)
πΉ Build admin dashboard at portal.starzdancecomp.com (read-only viewer first)
Month 3-4 (Aug-Sep 2026): Portal & Corrections
πΉ Studio-facing portal: see events, routines, balances, submit corrections
πΉ Admin correction queue with approval/rejection
πΉ State machine per event (OPEN β LOCKED β CORRECTIONS_OPEN β etc)
πΉ Reservation system with caps + whitelist (your $5/routine pre-reserve idea)
Month 5 (Oct 2026): Schedule Builder
πΉ Drag-and-drop routines between days/rooms
πΉ Auto-detect quick changes (highlight, don't just show on button click like old code)
πΉ Sort by division/category/age/level
πΉ Insert breaks, recalc times automatically
πΉ Squeeze-in support (334A, 334B numbering)
Month 6 (Nov 2026): Judge App + Reports
πΉ Electron desktop app for judges (offline-capable, LAN-syncable)
πΉ SQLite local cache, syncs to EC2 MySQL when online
πΉ USER-DEFINED REPORT BUILDER: pick fields, order, grouping, name it, save, reuse
πΉ Default report templates: Program, Scoring, High Points, Awards, Standout Dancer, Title Winner
Parallel Run (Dec 2026 - Dec 2027)
πΉ Run NEW system alongside old for 2027 season
πΉ Use both, compare outputs, find bugs, fix
πΉ Keep coding guy's system as fallback until full confidence
Industry Launch (Jan 2028)
πΉ Approach 2-3 friendly competitor companies as beta testers
πΉ Refine, polish, fix onboarding
πΉ Public launch mid-2028
ποΈ THE TECH STACK
πΉ Backend: PHP on EC2 (matches existing stack), REST API + WebSocket server
πΉ Database: MySQL (new unified starz database, multi-tenant with company_id)
πΉ Studio Portal: React SPA at portal.starzdancecomp.com
πΉ Admin Dashboard: React SPA at admin.starzdancecomp.com
πΉ Judge Desktop App: Electron wrapper around React components + SQLite local cache
πΉ Mobile: Capacitor wrapper around studio portal β App Store + Play Store
πΉ File storage: S3 (already have starz-media-vault)
πΉ Payments: Stripe (tokenized, stored methods, auto-charge for discount tiers)
πΉ Email: SendGrid (already configured)
πΉ Real-time updates: WebSocket (admin sees studio corrections flow in live)
π¨ MULTI-TENANT FROM DAY 1
πΉ Every table has company_id β Starz is company_id = 1, others are 2, 3, 4...
πΉ Subdomain routing: starz.dancecomppro.com, abc.dancecomppro.com
πΉ White-label branding per company (logo, colors, email from address)
πΉ Self-serve onboarding with CSV import wizard
πΉ Per-company configuration: terminology, pricing, categories, age groups, judges
π° BUSINESS MODEL (Industry SaaS)
πΉ Target: ~500+ regional dance competition companies in US
πΉ Plus: horse shows, gymnastics, figure skating, martial arts, cheer, baseball tournaments β anything with categories/judges/scoring/scheduling/awards
πΉ Pricing: $500-$2,000/month per company (scales with event count)
πΉ 100 companies Γ $5k/mo = $6M/year ARR
πΉ Exit strategy: sell to industry consolidator (Dance Informa, DanceWeek) in 5-7 years
πΉ Alternative: keep as $1-5M/yr side business with small team
π FEATURES THAT MAKE COMPETITORS CRY
πΉ Studio self-service corrections with approval workflow (NOBODY has this)
πΉ Hard caps + whitelisting per event
πΉ Squeeze-in numbering (334A, 334B) β real pro move
πΉ Live parent notifications: "Your daughter is up in 5 min, Stage 2"
πΉ Photos, videos, awards all in one portal (most use 3 vendors)
πΉ AI correction suggestions based on learned patterns
πΉ Reservation system with early bird pricing
πΉ Fully configurable terminology (works for any sport/activity)
πΉ User-built report engine (not locked to predefined reports)
β οΈ CRITICAL SUCCESS FACTORS
πΉ Build for Starz first, industry second β battle-test at your events
πΉ Parallel run for full 2027 season before trusting solo
πΉ Multi-tenant architecture from day 1 (impossible to bolt on later)
πΉ Copyright/trademark the name before public launch
πΉ Document everything (code, workflows, pricing logic) for future sale
πΉ LLC the product separately from Turn That (or Starz) for clean sale path
πΉ Judge app reliability is PARAMOUNT β zero downtime at events
π SOURCE CODE ARCHIVE
πΉ DanceCompetition v5.2.3.4 decompiled (ILSpy, April 14, 2026)
πΉ 121,083 lines of VB.NET (decompiled to C#)
πΉ Key files: Functions.cs (22k lines), Reporting.cs (18k), ManageCompetition.cs (10k), JudgeCompetition.cs (7.5k), Import_Data.cs (5.9k)
πΉ Three DBs discovered: MSSQL (starz1), MySQL (starzcompinfo, starzexport, ielaa)
πΉ Owner: MWSDC Corporation / Tyler Tomesh (paid to develop over 14 years)
πΉ Legal right to decompile/modify/rebuild confirmed
π― NEXT STEPS (Resume Here)
1οΈβ£ Finish spring 2026 season on current system (5-6 events left)
2οΈβ£ June 2026: Start schema design session for new unified database
3οΈβ£ Investigate starzcompinfo and ielaa GoDaddy MySQL databases (unknown contents)
4οΈβ£ Register product trademark (suggested names: StageCast, CompPilot, DanceOps, EventRunner)
5οΈβ£ Form separate LLC for the SaaS product (keep clean from Starz operations)
6οΈβ£ Begin building portal.starzdancecomp.com as read-only mirror viewer
π₯ New Tab: Recent Uploads
πΉ Where: Admin β π₯ Recent Uploads tab (between File Manager and QA Inspector)
πΉ Purpose: Find misfiled uploads. Drag a box around them. Mass delete or move.
πΉ Use case: Photographer downloaded Sioux City photos into Sioux Falls (~1000 misfiled JPGs). Or videos from Monticello uploaded into Sioux City studio folders.
πΉ How: Pick event β time window (24h/3d/7d/14d/30d/all) β file type filter β π Scan
πΉ Shows only files modified in window, grouped by Studio/Folder/Day, sortable by date/path/size
πΉ Selection: click row to toggle, Shift+Click for range, drag empty space to box-select rows (rubber band), Ctrl/Cmd+drag = additive
πΉ Bulk ops: Delete (soft-deletes to /_Deleted/, 7-day auto-purge) or Move To... (reuses File Manager move dialog)
πΉ Parallelized 8 deletes at a time. Status bar shows live progress.
πΉ Backend: walks S3 via recursive admin/files/browse calls (admin/files/list-all returns no timestamps). 8 parallel workers, prunes _Deleted/ branches. Logs scan summary to console as window._ruDebug.
πΉ Date parsing: uses ruParseDate() helper β converts S3 PHP format ('2026-05-01 20:12:40') to ISO before Date.parse(), prevents browser timezone drift bug.
β° TIMEZONE BUG FIXED β Top Shot + Critique Expiry + Access Codes
β οΈ ROOT CAUSE: /var/www/starz/config.php:47 has `date_default_timezone_set('America/Chicago')`
πΉ MySQL stores datetime columns in UTC (because forms send ISO/UTC strings)
πΉ PHP-FPM reads with Chicago tz β strtotime() interprets stored '20:00:00' as 8 PM CDT = 1 AM UTC tomorrow β comparisons against time() (always UTC) fail
πΉ Symptom: Set Top Shot start to 3 PM CDT, saved correctly as 20:00 UTC, but is_open returned 0 because PHP read 20:00 as Chicago time
πΉ Detection method: php-r CLI test showed tz=UTC (no config.php loaded), but FPM-served endpoint showed end_ts off by exactly 18000 seconds (5 hours = CDT offset)
FIX PATTERN β when reading datetime FROM DB, force UTC interpretation:
$ts = (new DateTime($cfg->start_at, new DateTimeZone("UTC")))->getTimestamp();
Patches deployed (May 1, 2026):
πΉ /var/www/starz/public/index.php line 4803-4804: public/topshot/config β start_ts/end_ts UTC parse
πΉ /var/www/starz/public/index.php line 4874-4875: public/topshot/charge β start/end check UTC parse (CRITICAL: was rejecting customers at payment)
πΉ /var/www/starz/public/index.php line 1941: access code expires_at check (gallery code expiry)
πΉ /var/www/starz/public/critiques/download0.php + download2.php: all download_expires checks (5 lines each file)
πΉ Backups created: .bak-topshot-tz-*, .bak-charge-tz-*, .bak-tz-* in same directories
πΉ Verify count: grep -rn 'DateTimeZone("UTC")' /var/www/starz/public/ should show 11+ hits
π¨ STILL VULNERABLE β strtotime() on DB datetime is broken everywhere this pattern remains. Audit before each new feature:
grep -rn "strtotime(\$" /var/www/starz/public/ --include="*.php"
π Top Shot Setup β Required Steps for Each Event
πΉ Where: Admin β Events tab β click event β scroll to Top Shot section
πΉ Required fields: Enable, Public Visible (uncheck for testing only), Start datetime, End datetime, Price (cents), Prize Label, S3 Entries Prefix
πΉ Times in form are LOCAL (Central) β form converts to UTC on save automatically
πΉ Test mode: uncheck Public Visible, browse /live?topshot=1, button only appears with that query param
πΉ Top Shot button location: inside the photo lightbox β only visible after tapping a photo, NOT on main /live page
πΉ Hidden when: photo is already a TopShot entry copy (in /TopShot/ folder), is_open is 0, public_visible is 0 without ?topshot=1, photo already entered
πΉ Public flow: tap photo β lightbox β Top Shot button β buyer info β Square card or Apple Pay β photo copied to s3_entries_prefix folder, entry recorded as paid+copied
πΉ Tables: starz_topshot_config (one row per event), starz_topshot_entries (paid/copied/refunded), starz_topshot_corrections (buyer notes about wrong dancer info)
πΉ Manual entry: Admin β Events β Top Shot section β Add Manual Entries panel (for cash sales / comps)
πΉ Refund: admin/topshot/refund endpoint β refunds Square charge AND deletes entry copy from S3
π Boot Sequence Race Fix
πΉ admin/index.php boot order changed: loadDashboard().then(() => ruInit if recent tab active)
πΉ Before fix: reload while on Recent Uploads tab β events dropdown empty (because restoreTab() ran ruInit before loadDashboard finished populating window._events)
πΉ After fix: ruInit always rebuilds dropdown, falls back to admin/dashboard fetch if window._events empty
πΉ Same race fix pattern useful elsewhere β any tab whose init depends on dashboard data should re-fire after loadDashboard resolves
π‘οΈ Backstage Notification Guard β Completed Events Locked
β οΈ INCIDENT: May 2, 2026 ~6:52 AM CDT β Employee accidentally typed "GOOD MORNING!" into Monticello backstage queue (event ended April 26, status=completed) instead of Sioux City. Push notification fired to all subscribed parents from Monticello. Tammy Christopherson reported it.
πΉ Root cause: Backstage app event picker remembered last-used event (Monticello). Operator opened backstage in the morning, typed habitual greeting, hit Send. Backend had no state guard β accepted writes on any event regardless of status.
πΉ Diagnostic SQL used to trace:
SELECT id, event_id, stage, status, custom_text, started_at FROM starz_stage_queue WHERE custom_text LIKE '%GOOD MORNING%' ORDER BY id DESC LIMIT 20;
πΉ Showed event_id=14 (Monticello, completed) had a row created at 11:52 UTC today β confirmed mistake, not data leak.
FIX: Added state-check guard to backstage write endpoints that broadcast notifications:
$ev_status = DB::fetch('SELECT status FROM starz_events WHERE id = ?', [$event_id]);
if ($ev_status && in_array($ev_status->status, ['completed','cancelled','draft']))
json_error('Event is '.$ev_status->status.' β queue is locked');
πΉ Patched in /var/www/starz/public/index.php:
πΉ Line 3219: backstage/queue-add (custom text + routine adds)
πΉ Line 3246: backstage/queue-next (advance queue β this is what fires the broadcast loop)
πΉ Backup: /var/www/starz/public/index.php.bak-completed-event-guard-*
πΉ Verify: grep -c "queue is locked" /var/www/starz/public/index.php β should return 2
πΉ NOT patched (intentionally): backstage/queue-remove and backstage/queue-reorder. Different signature (no event_id in POST), don't trigger notifications, low harm. Skipped to avoid unnecessary DB lookups.
π§ͺ How to test the guard manually
// In admin browser console after login:
const fd = new FormData();
fd.append('token', localStorage.getItem('smv_admin_token'));
fd.append('event_id', '14'); // any completed event ID
fd.append('stage', 'Room A');
fd.append('custom_text', 'TEST GUARD');
const r = await fetch('/api/backstage/queue-add', {method:'POST', body:fd}).then(r=>r.json());
console.log(r); // should be {success:false, error:"Event is completed β queue is locked"}
π¦ Bulk Studio Removal (Studios Map)
πΉ Where: Admin β Studios tab β Studio Map β pick event
πΉ Use case: Nationals Wisconsin Dells imported with wrong studio number set (49 studios mismapped). Need to wipe and re-import.
πΉ New UI: Per-row checkbox + header select-all checkbox + bulk action bar (X selected Β· Remove Selected Β· π₯ Remove ALL N studios)
πΉ Behavior: Parallelized 8 deletes at a time. Live progress bar. Removed rows fade and strike through.
πΉ Bug fix bundled: single-row removeStudioMapping no longer calls loadDashboard() on success β only refreshes the studio map. This eliminates the "second click does nothing" bug where dashboard refetch (~800ms) was clobbering the events dropdown mid-click.
πΉ Endpoint: admin/remove-studio-mapping (existing, unchanged) β accepts event_id + studio_id, deletes from starz_event_studio_numbers and starz_event_studios.
πΉ Already-uploaded S3 files in studio folders are NOT deleted β they remain accessible via File Manager. Only the numberβstudio mapping and event link are cleared.
π Pattern Audit β State Guards on Time-Bounded Writes
πΉ The notification leak revealed a broader pattern: operations that broadcast to subscribers must check parent resource state.
πΉ Audit candidates for similar guards:
πΉ admin/notify-studios β sends welcome emails. Should refuse if event status='completed'.
πΉ admin/schedule-email-studio β sends schedule emails. Same risk.
πΉ admin/scorer-push (and the desktop scorer station) β silent if event over, but check.
πΉ critique notify_at queue β should skip if event is far enough in the past.
πΉ General principle: any write that triggers email/push to humans should validate the parent event/resource is in an active state, not just that the foreign key resolves.
Copy this entire block and paste it into a new AI conversation to fully onboard an assistant.
Welcome! Set up the livestream for your event.
Paste your Rumble embed code, YouTube embed URL, or any stream URL. The system will automatically extract the correct link.
π WordPress Instructions:
Click to see what viewers see (stream + overlay):