Changelog

[5.25.0] β€” 2026-09-18 β€” Anti-bot hardening: 6 prevention layers + R12

Built from the LORA LESSAN incident (Vodafone Qatar bot farm: self-vote + 77 scripted votes via temp-mail accounts from one /64).

Prevention (vote cast gates β€” verified live against all attack shapes)

  1. Fingerprint gate: votes with empty or all-zero fingerprint

(no-JS scripts / the LORA fake signature) rejected with state=jsrequired + audit vote.rejected_no_fingerprint.

  1. Timing token: vote forms carry HMAC-signed render timestamps;

submissions < 3s or > 1h after render rejected (state=toofast) β€” kills page-parsing bots that fake fingerprints.

  1. Disposable-email rejection at entry: voter form + award step-1 +

award/GoH full form reject temp-mail domains (error=tempmail / field error). Blocklist = built-in constant + runtime-extendable settings key block.temp_domains (no deploy for new domains).

  1. Per-candidate velocity tripwire: > 30 votes/hour for one

candidate escalates the foul-play incident live.

  1. Subnet awareness: votes now store voter_subnet (IPv6 /64,

IPv4 /24 β€” LORA's whole farm was one /64); β‰₯ 15 same-subnet votes for a candidate in an hour trips the incident.

  1. Device tripwire: β‰₯ 30 votes for one candidate from one

fingerprint trips the incident. Tripwires are flag-only: votes stay counted; incidents escalate to high severity + Telegram alert (throttled 10 min/candidate) with a direct dossier link.

Detection

  • R12 foreign networks: dossier shows % of subnet-tracked votes from

non-PK networks via cached RDAP lookups (subnet_asn_cache, 30-day TTL, max 3 fresh lookups per render). LORA's /64 resolves to QA-VODAFONEQATAR-20160113, country QA.

  • FoulPlay::flagVelocity() β€” live incident escalation from cast().
  • Migration 053: votes.voter_subnet (+composite index), subnet_asn_cache,

block.temp_domains setting.

Rollout note

One-time flush of ~62 vote.rejected_timing_token rejections as voters with pre-deploy cached pages bounced; self-healed within ~4 minutes (voters reloaded and retried successfully β€” vote flow stayed continuous).

[5.24.1] β€” 2026-09-18 β€” LinkedIn audit portal (export β†’ verify β†’ import)

Added

  • /admin/linkedin-audit (nav: Participants β†’ πŸ”— LinkedIn audit): LinkedIn

blocks all server-side checks (HTTP 999 to datacenter IPs), so verification is a browser workflow: export CSV β†’ check links in your own browser (batch link-checker extensions work) β†’ mark ok/dead β†’ upload results back.

  • Hygiene engine (LinkedInAudit): incremental idempotent scan β€” normalizes

URLs (strips utm tracking, forces https/canonical form; 1,096 fixed on first run), validates format (138 bad flagged: non-LinkedIn hosts, invalid /in/ slugs), and a slug-vs-name heuristic flags borrowed profiles (76 mismatches).

  • Import matches rows by user_id or email; records status, checked_at and

the checker; audited. Re-normalizing a URL resets its live-check to pending; unchanged URLs keep verified results.

  • Filter chips (bad format / mismatch / pending / ok / dead), search,

pagination, per-filter CSV export. Migration 052: linkedin_audit.

[5.24.0] β€” 2026-09-18 β€” Foul Play detection, Candidate 360 + AI analysis, GoH visit scheduling

Added β€” 🚨 Foul Play section (/admin/foul-play)

  • Automatic detection over all candidates: 11 rules β€” R1 device farms

(fingerprint concentration with shared-vs-farm spread discriminator), R2 disposable-email domains, R3 velocity bursts, R4 one-and-done voter concentration, R5 template reasons, R6 own-company block votes (info), R7 IP concentration (dossier), R8 bot cadence (median inter-vote gap), R10 self-voting, R11 deleted-account votes (info).

  • Score 0–100 + severity (high/medium/low), signal chips, filters

(severity/category/status), re-scan, 60s cached radar.

  • Dossier per candidate: signal breakdown, hourly timeline with bursts,

domain table, device table (farm vs café), funnel stats, incident status workflow (open→reviewing→resolved/dismissed + notes, audited), evidence CSV export. FLAG-ONLY: no holds, no restrictions, per policy.

  • First scan: 1,516 candidates β†’ 27 high / 184 medium flagged; found new

evidence incl. self-votes (Maryam Sana, Mohammad Umaid), multi-device farms (Hamza Γ—3), own-domain blocks (Osama 273, Abdul Rafay 210).

Added β€” 🧭 Candidate 360 (/admin/candidates/{id})

  • Full dossier: application (title/company/bio), questionnaire traits,

KPI strip (votes, avg account→vote gap, single-candidate-voter %, farm votes, temp-mail votes, foul play score), domain analytics with temp-mail/corporate/own-company flags, leadership trait chips.

  • "Who voted for them" evidence table: searchable + paginated, filter

chips (temp-mail / same company / 1-and-done / gap<5min / top device), per-voter account→vote gap, reason, device, domain class. Voters CSV.

  • Candidates index in admin nav (Participants group) with foul-play

score badges on every row.

Added β€” πŸ€– AI integrity & quality analysis

  • On-demand generation per candidate (MiniMax-M3): verdict, likelihood,

scenario, analysis bullets, recommendation β€” from anonymized aggregates only (no voter emails/names leave the system).

  • Stored in candidate_ai_analyses (verdict/likelihood/model/generated_by);

audited; nightly queue job (candidate.ai_bulk) pre-generates/refreshes 60 per night ordered by vote count.

  • Same analysis card embedded in the judge review page (visible to

judges, per board decision) with generated-at stamp.

Added β€” πŸŽ– GoH visit scheduling

  • GoH dashboard card: arrival/departure on a 15-minute grid within the

settings-driven window (default 14:00–17:00), "🏁 Full program β€” stay till the end" option, companions (0–10), note; reschedule/cancel anytime; staff notified; audited (goh.visit_scheduled/rescheduled/ cancelled).

  • Admin Visit Plan on /admin/schedule: arrival-sorted table, per-15-min

headcounts (arriving Β· on-site incl. companions), full-program badges, unscheduled GoH list.

  • Migration 051: foul_play_incidents, candidate_ai_analyses, goh_visits
    • goh.visit window settings.

Fixed

  • Vote reason now clamped server-side to 140 chars (frontend

maxlength was bypassable β€” one vote had 161 chars).

[5.23.1] β€” 2026-09-18 β€” Cloudflare real-IP fix + voter throttle hardening

Fixed

  • Cloudflare activated (~11:00) proxied all traffic but nginx had no real_ip

config β†’ REMOTE_ADDR became CF edge IPs β†’ voter-form 50/hr/IP limit blocked legitimate voters in aggregate ("Too many attempts from this network"), and all IP analytics/integrity panels showed Cloudflare edges. nginx now trusts CF-Connecting-IP from the 22 official CF ranges (15 v4 + 7 v6). Verified: audit logs real client IPs again.

Changed

  • Voter form rate limits: IP cap 50 β†’ 80/hr (CGNAT headroom); NEW per-email

cap 6/hr (the true mail-bomb gate, via countRecentMeta on audit meta).

  • Blocked submissions now audited as voter.rate_limited (scope: ip|email) β€”

previously silent, impact was unmeasurable.

  • Friendlier throttle copy: shows retry minutes, reassures the vote isn't

lost; separate per-email message with "open the most recent email" tip.

  • Audit::retryInMinutesMeta() added (meta-keyed twin of retryInMinutes).

[5.23.0] β€” 2026-09-18 β€” GoH roster redesign: data readiness + outreach filters

Added

  • GoH roster KPI bar: Total, βœ… Outreach-ready (name+LinkedIn+title+company),

With LinkedIn, Missing name, Invite pending, Rated β€” live counts.

  • Filter tabs: All Β· Ready Β· No LinkedIn Β· Incomplete Β· Invite pending Β·

Unrated Β· Deactivated (query-param based, shareable URLs).

  • Enriched cards: gender-matched avatar fallback (portrait_for), Title @

Company from form_json, LinkedIn/WhatsApp/photo βœ“/βœ— badges, bio snippet, completeness bar (20% each: name, LinkedIn, title, company, photo), registered date / never-registered, READY + INVITE PENDING badges.

  • CSV export per filter (/admin/goh-roster/export) β€” outreach list with

name, title, company, LinkedIn, WhatsApp, completeness, rating. Audited.

  • Deactivate/Reactivate junk GoH entries (admin only, POST

/admin/goh-roster/{id}/toggle) β€” audited goh.deactivated/reactivated; deactivated excluded from roster + counts, restorable from Deactivated tab.

  • Default sort: readiness β†’ completeness β†’ rating β†’ name (was rating-only,

useless with 143/144 unrated).

Changed

  • GohRating::roster(): fully batched (2 queries total, was ~290 for 144 GoH);

returns rows + stats; parses form_json (title/company/bio/linkedin β€” catches LinkedIn that user field misses). effective() untouched.

  • Deactivated the burpcollaborator.net scan artifact (user 1936).

[5.22.0] β€” 2026-09-16 β€” member self-service: profile edit, category switch, withdraw, photo toggle

Added

  • Profile Edit page (/profile/edit): name, title, company, bio β€” instant

save, every field audit-logged with old→new values.

  • Category Switch (one-time): dashboard button β†’ type SWITCH to confirm β†’

all votes soft-deleted, registration reset, redirect to new application form. category_switched_at column locks after first use.

  • Withdraw Application: type WITHDRAW β†’ votes soft-deleted, statusβ†’

withdrawn. Immediate reapply — createRegistration detects withdrawn rows and resets them (same pattern as visitor→award upgrade).

  • Photo Show/Hide Toggle: members can switch between their real photo and

the gender-matched illustrated avatar (photo_hidden column). portrait_for() respects the flag everywhere (profile, nominees, cards, compare).

  • Migration 050: photo_hidden + category_switched_at columns.
  • Route fix: /profile/edit routes now precede /profile/{token} wildcard.

[5.20.0] β€” 2026-09-16 β€” candidate profile redesign

Changed

  • /v/{token} profile rebuilt: 960px two-column layout with gold-gradient

banner hero (96px avatar, titleΒ·company meta, rank '#1 of 11', momentum 'β–² N today'), sticky vote card (desktop), trait persona rendered as gold horizontal bars, About section, 2-column supporter-quote grid, hover-zoom media gallery, category explore link. All state banners, receipt block, and vote-form field names preserved. Mobile: single column, vote first.

[5.19.0] β€” 2026-09-16 β€” election-night homepage + analytics brains

Added

  • Homepage: 6th stat tile (🎀 Guests of Honor), πŸ‘‘ women-leaders banner,

live HH:MM:SS countdown to voting close, 🎯 vote-goal meter (voting.goal setting, 72% at launch), rotating real supporter quotes, ticker now mixes anonymous application events (award/GoH/visitor) with vote events.

  • /admin/analytics: People & Pipeline panel (type breakdown + status bars,

33-name questionnaire chase list, GoH pipeline, visitor funnel, applications-by-day chart), vote projection chips (pace Γ— days-left), integrity panel (same-IP vote clusters with ⚠ flags, removed votes, email throttles).

[5.18.0] β€” 2026-09-16 β€” live homepage social proof + analytics mission control

Added

  • Homepage live stat band: Nominees Β· Votes cast Β· Companies represented Β·

Verified voters Β· Days to vote (live counts, FINAL DAY styling <24h).

  • Live activity ticker (⚑ "Ahmed K. just voted Β· 2 min ago") via

/api/activity β€” privacy-safe (first name + initial only, per-voter dedupe, anonymous fallback), 60s auto-refresh.

  • /admin/analytics mission control: pulse chips (gender split, peak voting

hour), Closest Races πŸ”₯, Category Race Board (17 categories with leader + margin), Vote Velocity (48h chart), Candidate Leaderboard (top 50 with viewβ†’vote conversion), Invite Effectiveness (sentβ†’openedβ†’voted), Company Leaderboard. Existing top-viewed/hot-categories upgraded to include all 212 live nominees (was approved-only).

[5.17.0 / 5.16.0 / 5.15.0] β€” 2026-09-15 β€” invite tracking, GoH conversion, gender avatars

Added (v5.15.0)

  • Invite lifecycle truth: sent β†’ opened (email verified) β†’ voted (vote cast);

existing votes retro-matched. CSV export + πŸ“£ badges + Top-inviters widget

    • live Email & voting-funnel card on /admin.
  • Growth loop: vote-invite and nomination emails now also invite recipients

to apply for awards and (for managers/seniors) to become Guests of Honor.

Added (v5.16.0)

  • Direct admin conversion: 🎀 Make GoH turns an award application into a

Guest of Honor registration (audit-snapshotted) and emails the person to complete their profile.

Added (v5.17.0)

  • Gender (required, male/female) collected on all application forms;

illustrated avatar pool now matches gender (male→variants 1/3, female→0/2).

  • bin/infer-gender.php classified 1,894 of 2,198 existing users (dictionary
    • MiniMax hybrid; unknown list for admin review).

[5.14.8] β€” 2026-09-15 β€” visitor β†’ award upgrade

Fixed

  • Visitors can now apply for an award. Previously BR-002 (one registration

per person per event) hard-blocked anyone who had registered as a visitor β€” they saw "You are already registered (Visitor)" forever no matter how many times they retried (live case: ceo@hack4bug.org, blocked across 4+ hours). Completing the award application now converts the visitor registration (type/category/form/status, capacity + waitlist respected), with the before-state audit-snapshotted (registration.upgraded_visitor_to_award). GoH and award registrations are never auto-converted.

Changed

  • Magic links are usable for their entire 15-minute life, unlimited uses.

Single-use semantics are gone: they added no real security (192-bit unguessable token + 15-min TTL + per-IP/per-email caps were always the protection) and existed only to let corporate mail gateways (Office 365, Zong/AWS, hoh.net rotating scanners) lock humans out. Every fetch within TTL logs in; reuse audited (auth.magic_reused); genuinely expired links land on the fresh-link recovery page. (Live case: zeeshan.ali@hoh.net β€” 6 links, all scanner-burned before any fix shipped.)

  • verify-email made idempotent: token is never nulled; scanner GETs and

late clicks both see the success page.

Fixed

  • GET-prefetching mail gateways no longer lock users out. Zong's AWS-hosted

email filter GETs links ~7s after delivery, before the human clicks β€” consuming one-time tokens even after the HEAD guard. Magic links now carry a 10-minute reuse grace window: a token stays usable after first use until it expires (15 min hard limit), so scanner-claimed links still work for the real recipient. Reuse is audited (auth.magic_reused_in_grace); links used past the grace window fall through to the self-recovery fresh-link page. (Live case: saleem.khan@zong.com.pk.)

Fixed

  • Corporate email scanners no longer burn one-time links. Office 365

SafeLinks (and similar gateways) prefetch URLs with HEAD requests BEFORE the human clicks β€” consuming single-use tokens and locking applicants out (live case: ali.awan@alidc.com.pk, scanner burned 6 links, each ~1s before his click). HEAD requests on /login/magic, /verify-email and /voter/verify now return 200 without touching the token; scanner skips are audited (auth.magic_scanner_skipped).

  • Burned-link self-recovery: invalid/expired magic links now show a

"Send me a fresh link" page with the recipient's email prefilled (resolved from the token row) instead of a dead-end error β€” plus a personal-email tip for chronically-scanned corporate domains.

[5.14.4] β€” 2026-09-15 β€” rate-limit rebalance (50/hr) + honest throttle UX

Fixed

  • GoH/award "silent fake success" β€” the step-1 rate limit used to render

"Check your email" without sending anything; users waited forever. All four registration branches (award/GoH Γ—2/visitor) now show an honest "too many requests β€” retry in N minutes" page with a live countdown.

  • Limits rebalanced for Pakistani shared IPs (CGNAT): magic links 5β†’50/hr

per IP, registration step-1 10β†’50/hr, voter form 10β†’50/hr. Access-log analysis showed 100% human traffic (top IP: 18 requests/day) β€” old limits only blocked legitimate applicants (live case: iPhone user throttled after 5 resend clicks in 60s).

  • New per-email cap: 10 magic links/hour per mailbox β€” single-mailbox

bombing stays impossible while offices/mobile groups work.

  • Resend cooldown: the email page's Resend button disables for 60s after

each click (kills the rapid-retry loop that caused today's throttle).

  • Countdowns computed from the audit window (Audit::retryInMinutes +

countRecentMeta helpers).

[5.14.3] β€” 2026-09-15 β€” domain migration to awards.server4sale.com.pk

Changed

  • Production domain is now https://awards.server4sale.com.pk (Let's Encrypt

cert, valid to Dec 14 2026). APP_URL, LinkedIn redirect URI, card art, config fallbacks and QA defaults all follow the single .env source.

  • Old domain fla.events.jugaar.ai 301-redirects (path + query preserved) so

in-flight magic links and shared URLs keep working.

  • Cached card PNGs wiped β€” regenerate with the new domain printed.
  • DNS: stale AAAA (old WordPress host) removed after it was hijacking IPv6

traffic and blocking cert issuance.

[5.14.2] β€” 2026-09-15 β€” H4 secrets hygiene

Fixed

  • .env 644β†’640 (API keys, DB pass, CNIC salt no longer world-readable);

storage dirs 750, logs/backups 640.

  • mail.log token redaction β€” one-time token URLs replaced with

[REDACTED] before anything touches disk; existing log truncated (260 live login URLs removed). Nightly log-rotate added to cron.

  • DB password rotated to a 48-char random secret (weak fla_dev_2026

retired); backup.sh picks it up from .env automatically.

[5.14.1] β€” 2026-09-15 β€” security fixes (C1, H1, H2, H3)

Fixed

  • C1 β€” voter identity hijack sealed: submitting the voter form with an

already-verified email no longer returns the live token in a redirect (and no longer marks the session verified without email proof). The token is rotated, the old one invalidated, and a fresh link emailed to the address owner. resolveVoterUser() refuses team-role accounts from voter sessions.

  • H1 β€” per-IP throttle spoofing sealed: Audit::clientIp() now uses

REMOTE_ADDR only (nginx terminates TLS locally; X-Forwarded-For is attacker data). All per-IP rate limits now count real addresses; raw XFF kept in audit meta (_xff) for forensics only.

  • H2 β€” applicant enumeration sealed: /v/{token} and /cards/{token}

now enforce the same visibility as /nominees (vetoed=0 + public statuses). Rejected/waitlisted/vetoed applicants 404; 4-digit short_code crawling dead.

  • H3 β€” email-bombing relays re-armed: voter form 10/hour/IP (was 0),

magic-link requests 5/hour/IP (was unlimited). Nomination 25/hr cap now effective thanks to H1.

[5.14.0] β€” 2026-09-15 β€” Leadership Trait Questionnaire (all-MCQ, persona profiles)

Added

  • Trait-based questionnaire β€” replaced the scored exam model. 170 questions

(10 per category), each with 4 options mapping to 8 leadership traits (Mentor Β· Empowerer Β· Shield Β· Challenger Β· Connector Β· Stabilizer Β· Visionary Β· Servant). No wrong answers β€” every option is a valid way to lead.

  • Leadership personas on profiles β€” after the questionnaire, the

candidate's public profile shows their persona headline (🌱 The Gentle Mentor, πŸ›‘ The Team Shield, …), one-line tagline, and trait mix chips (e.g., Mentor 40% Β· Empowerer 30% Β· Shield 20% Β· Connector 10%). Top-2 traits within 10% render as a blend.

  • App\Core\LeadershipTraits β€” persona vocabulary + template-assembled

profile builder (zero AI dependency β€” questionnaire completes instantly).

  • Migration 047: question_bank.option_traits + questionnaire_submissions.trait_profile.

Changed

  • Questionnaire is fully MCQ β€” the "why" free-text box removed; session draws

all 10 category questions (was 15).

  • submitQuestionnaire() tallies traits and stores title/tagline/mix/summary

on the submission; strengths chips now show the trait mix.

  • AI scoring call removed from the questionnaire path entirely.

Removed

  • ~1,250 scored questions from earlier models (audit-snapshotted, recoverable)
    • legacy event-2 rows.

[5.12.2] β€” 2026-09-14 β€” Questionnaire question bank recovery

Fixed

  • AI questionnaire silently skipped for event-1 applicants β€” all 84 active

question_bank rows pointed at deleted E2E demo categories (ids 88–137, "CIO of the Year"/"Rising Star" pairs from 7 old qa-e2e runs) and were open_ended while the flow selects type='scenario'. Questions remapped to live categories (154Γ—12, 151Γ—12, 157Γ—12, 156Γ—6) with the 42 leftovers moved to the designed category_id IS NULL universal pool; all 84 converted to scenario MCQs with scenario context, 4 options and leadership scores (audit questions.remapped snapshot taken before the change).

  • questionnaire() selection now falls back to the universal pool:

(category_id = :c OR category_id IS NULL) β€” every live category draws a full 10-question session (84 < 17Γ—10, pool is the only non-duplicating fit).

[5.12.1] β€” 2026-09-15 β€” Nomination invite flow, AI language guard

Fixed

  • Nomination token now survives the journey β€” the nom invite token is carried

through step 1, the login redirect, and step 2 (hidden fields); conversion uses the token first (email-match stays as fallback) so nominations can no longer get stuck on "invited" when the nominee changes their email.

  • Step 2 prefilled from the nomination β€” Name and Category arrive filled

(previously Name was blank and Category reset to "β€” choose β€”" even though the nomination had both). Also fixes the dead ?category= prefill for direct applicants.

  • Single-email invite flow β€” opening a valid invite link while logged out sends

the magic link straight to the nominated address (no redundant "enter your email" form); "Use a different email" falls back to the classic form. Journey drops from 6 steps / 2 email round-trips to 3 steps / 1.

  • AI language guard β€” MiniMax provider pins "English only" on every request;

summaries, strengths, feedback and drafts can no longer drift to Chinese.

  • Invite tokens expire after 14 days; opening an invite flips the nomination to

"invited" (audit nomination.invite_opened).

Added

  • ↻ Re-invite button for invited nominations (fresh token + reminder email/SMS,

audit-logged).

  • AGENTS.md now mandates English-only responses for all agents.

[5.12.0] β€” 2026-09-15 β€” Candidate profiles, engagement pack, view-as, vote invites

Added

  • Candidate profile on the shared /v link β€” title/company/designation, bio,

social chips (LinkedIn/X/Instagram/Facebook), media, supporter quotes and vote form on one page; every previously shared link instantly upgraded.

  • ✨ Strengths β€” AI scoring now derives 3–5 strength keywords per candidate

(questionnaire_submissions.strengths_json); public profiles show strength chips instead of the numeric AI score (score stays admin-only; ai_summary fallback).

  • βš–οΈ Compare β€” /compare/{a}/{b} side-by-side within a category with vote

CTAs; "same category" guard; legacy multi-compare preserved.

  • Rank + milestones β€” "#N in category" on profiles and nominee cards (top-3

gold/silver chips; only when leaderboard_public is on); thank-you page shows "Help X reach 50/100/250/500 votes" progress bar.

  • Supporter quotes β€” up to 6 recent voter reasons with full name + title

("verified voter" badge); voter form discloses endorsement use.

  • πŸ“£ Vote invites β€” nominees paste emails (50/batch, 200/day, consent box);

system emails invites with their referral link; votes captured in vote_invites with opened/converted tracking; stats on member dashboard + admin person page.

  • Campaign kit β€” done page gains pre-written WhatsApp/LinkedIn messages and a

downloadable QR of the nominee's /v link.

  • I VOTED sticker β€” /cards/voted/{receipt} branded PNG via the Cards engine.
  • 🌟 Today's spotlight β€” homepage daily rotation (most votes gained in 24h,

7-day no-repeat).

  • Nudge emails β€” daily queue job: approved/finalist nominees within 20 votes

of the next rank get one positive nudge (1/day cap, voting period only).

  • Role management β€” /admin/people/{id}: role change, activate/deactivate,

resend magic link (guards: no self-change, no touching other admins).

  • 🎭 View-as impersonation β€” admin/staff can browse as any non-privileged

account read-only: banner + one-click exit; every POST blocked while impersonating (front-controller choke point); audit rows tagged _impersonated_by; bypasses the target's forced password change.

  • Illustrated avatar pool β€” 24 AI-generated stylized persona portraits

(MiniMax image-01; category-mapped, deterministic per user) replace the flat SVG default everywhere; real photos always win. Raphael extraction script included as a free secondary generator.

  • voting_closes_at event field (admin-editable) powers countdowns and nudges.

Fixed

  • AI <think> leak β€” MiniMax reasoning blocks stripped in the provider;

message drafts, assistant, summaries all clean.

  • Quotes join collation clash (voter_tokens vs users) fixed with explicit COLLATE.
  • Legacy /vote/compare/{slug} preserved after compare() signature change.

[5.11.0] β€” 2026-09-14 β€” Staff section, reconstructable audit, category stats, live homepage numbers

Added

  • Staff section (read-everything) β€” staff role now sees all admin pages (events,

questions, branding, toggles, judges, votes, winners, audit, system, duplicates, questionnaire, ai-analytics, qa, insights, webhooks, judge panel/performance, anomalies). Mutations unchanged; deletes remain admin-only (no user/registration delete endpoints exist at all). Magic-link login unchanged β€” no 2FA anywhere.

  • Invite staff β€” admin-only form on /admin/people: creates a staff user (or

upgrades an existing account, old role audit-logged) and emails a one-time sign-in link. Provisioned: muhammadtaahaa197@gmail.com, syedailsaubaid@gmail.com.

  • Magic link on /admin/login β€” "or email me a one-time link" option alongside

the password form; back-link returns to the admin login when initiated there.

  • Reconstructable audit β€” Audit::change() stores _old/_new/_diff on every

consequential mutation (registration status/bulk, winners veto/lock/reveal, votes moderation, branding/toggles, events/categories/sponsors, questions, judges, media, check-in undo); Audit::snapshot() stores full rows before every delete. PII-view logging on /admin/people/{id}. Recovery runbook in HELP.md.

  • Category stats page β€” /admin/categories: live per-category table (applicants

by status, capacity fill bar, votes + leader, avg AI score, prev-winner/dup flags, check-ins) with event filter, applicant drill-down and CSV export.

  • Questionnaire admin upgrades β€” search + pagination + CSV export; applicant

reasoning no longer truncated at 300 chars.

  • Votes admin upgrades β€” search (voter/candidate/reason/receipt) + pagination

(was hard-limited to latest 300).

  • New CSV exports β€” type=questionnaire, type=categories; audit CSV now honors

the page filters; export gate opened to auditors (was 403).

Fixed

  • Homepage stats now live β€” Applicants/Categories/Days-to-go computed from the

DB (was a stale static "30/15/10" setting). fla.stats_json remains as an optional manual override. Duplicate category descriptions corrected (Emerging Young Stars, Tech Trainers).

  • Voter verify returns to the candidate β€” the emailed verification link carries

the originating nominee (?c=) through the whole chain; after clicking it the voter lands back on the cast-vote page instead of generic /nominees. /nominees shows a "You're verified" banner and refreshes from browser bfcache so vote counts never look stale.

  • Analytics event selector un-dead β€” CSP-blocked inline onchange replaced with

data-autosubmit + Apply button.

  • Approve/reject feedback β€” success flashes on registrations; swallowed

err=vetoed/err=bulk now rendered with explanations.

  • Ticket reply guard β€” reply form only shows when staff last replied (or viewer

is staff); owner sees "Waiting for staff to respond."

  • Voter dashboard fatal β€” resolveVoterUser() undefined-method call inlined.
  • "Hi ," emails β€” magic-link greeting falls back to "Hi there," when the name is

empty (GoH email-only signup).

  • Team session idle timeout β€” team roles (admin/staff/judge/auditor/mc/

award_team) logged out after 4h inactivity (auth.team_idle_minutes setting); member sessions keep the 7-day cookie. Retention page hardened to admin-only.

  • Registration done page β€” links to the questionnaire (was the retired

/interview); visitor branch states the ~24h review SLA; GoH dashboard shows a "Step 2 of 2" hint.

[5.9.0] β€” 2026-09-14 β€” QA round 2–5 fixes, comprehensive audit logging, no-cache

Fixed

  • Mailer recipient substitution β€” resolveUser() COALESCE argument order swapped

so submitted email replaces stale session email (COALESCE(NULLIF(:e, ''), email) instead of COALESCE(email, NULLIF(:e, ''))). R5 confirmed: fresh browser sessions receive emails at correct addresses.

  • Nominate form 500 on long input β€” field length validation added (name ≀150,

reason ≀5000); all PDO exceptions caught gracefully (Duplicate entry, Data too long, unknown) with user-friendly error messages instead of 500.

  • /login/magic returns 422 for invalid email β€” now always returns same 200

"Check your email" page to prevent email-enumeration attacks.

  • Dark theme on /about-scoring and /how-it-works β€” bodyClass fla-dark added;

both pages were white while rest of site was dark.

Added

  • Comprehensive audit logging β€” all 76 POST handlers now call Audit::log()

(100% coverage). Every entry auto-captures: IP (behind proxies via X-Forwarded-For, CF-Connecting-IP), user agent, request method, request URI, referrer.

  • Mailer audit logging β€” every Mailer::send() writes to audit_log as

mail.sent with to/subject/ok/transport metadata.

  • QA dashboard (/admin/qa) β€” admin page showing email log (last 100 with

recipient, subject, status, transport, method, URI, IP, browser), vote activity (last 50), error count (24h), quick links to full audit log + system health.

  • No-cache headers β€” bootstrap sets `Cache-Control: no-store, must-revalidate,

Pragma: no-cache, Expires: 0 for all PHP responses; nginx static assets changed from expires 7d to no-cache must-revalidate`; media/cards Cache-Control: public headers removed.

  • OG meta β€” twitter:title + twitter:description meta tags added to layout.

Changed

  • Vote rate limit removed β€” per-IP 30/hour limit removed from voting controller.
  • Nominate rate limit removed β€” per-IP 5/hour limit removed from nominate controller.
  • Login always returns 200 β€” invalid email no longer returns 422 (anti-enumeration).

QA History

  • docs/QA-HISTORY.md β€” full revision history of all QA rounds (R1–R5) with

findings, severity, fix, commit, and status for every bug.

Verified

  • R5 (clean Chrome + mail.tm): emails delivered correctly, nominate 302 working,

magic link authentication working. Bug #1 refuted as cache-stale artifact.

  • Bug #3 (login rate limit) intentionally open per user request.

[5.8.0] β€” 2026-09-14 β€” Resilience, disaster-recovery backup, error logging hardening

  • Questionnaire auto-save drafts: partial answers saved to a new questionnaire_drafts

table every 2 seconds via AJAX. On disconnect and return, the applicant resumes with the same questions and previously entered answers intact. Draft deleted on final submission. Migration 042.

  • Parallel JSON backup for disaster recovery: every questionnaire submission,

nomination, and vote now writes a JSON Lines (.jsonl) file to storage/backup-data/ alongside the DB insert. Files are included in the nightly backup tarball. New bin/export-json.php CLI tool bulk-exports all existing data to JSON for initial seeding after a restore.

  • Nomination duplicate prevention: unique constraint (event_id, nominee_email)

added to nominations table β€” disconnect+retry no longer creates duplicates. Graceful "already nominated" message on duplicate attempt. Migration 043.

  • Error logging hardening:
    • PHP errors now write to storage/logs/php-error.log (explicit error_log path).
    • 404s logged with method + path in Router.php dispatch.
    • 403s logged with method + URI + user in Controller.php role guard.
    • session.gc_maxlifetime set to 604800 (7 days) to match cookie lifetime β€”

sessions no longer randomly destroyed by PHP garbage collector.

  • DB auto-reconnect: DB::run() catches "MySQL server has gone away" and

"Lost connection" exceptions, reconnects once, and retries the query β€” web requests no longer fail with 500 on transient DB connection drops.

  • Log rotation: new bin/log-rotate.sh compresses logs older than 1 day, deletes

compressed logs older than 7 days. Suggested cron: 0 0 * * *.

  • Orphan upload cleanup: new bin/cleanup-orphan-uploads.php removes uploaded files

not referenced by any DB row (registrations, sponsors, media, ceremony, tickets, users). Suggested cron: 0 2 * * 0.

  • AJAX staleness indicator: live vote counter, notification badge now dim after

30s/60s of failed polls and recover on reconnect. Users see when data is stale.

  • Nginx production config: deploy/nginx.conf updated for fla.events.jugaar.ai

with explicit access_log/error_log, location ^~ /media/ bypass, and fastcgi_read_timeout raised to 120s.

  • Backup.sh: now includes storage/backup-data/ in the storage tarball.
  • WordPress image import: downloaded real FLA images from old site

(awards.server4sale.com.pk): 8 event photos (2024 ceremony), Cloudflare sponsor logo, 4 section hero images, and generated a crown hero graphic for award/GOH/legacy pages. Images organized in storage/uploads/events/, public/assets/img/sponsors/, public/assets/img/sections/, and public/assets/img/generated/cat-crown.webp.

Migrations

  • 042_questionnaire_drafts.sql β€” questionnaire auto-save drafts table.
  • 043_resilience.sql β€” nominations unique constraint.

[5.7.0] β€” 2026-09-14 β€” Voter dashboard, GOH dashboard, social voting, admin analytics

  • Voter dashboard (/my-votes): personal hub showing all votes cast, category

progress (X/15), personal share link with referral tracking (?ref=voter-{id}), friend recruitment stats, and quick links to browse nominees. Shareable via LinkedIn, WhatsApp, and copy-link.

  • GOH dashboard (/goh/dashboard): dedicated Guest of Honor page with profile,

assigned category, event date, availability slots, GoH card download, and social share ("I'm a Guest of Honor at FLA 2026!"). Separate from general member dashboard.

  • Vote page social CTA: after casting a vote, voters see share buttons (LinkedIn,

WhatsApp, copy link) to spread the word. "Know someone?" CTA at bottom of every candidate page urges visitors to apply or nominate.

  • Category page CTA: "Know someone who deserves this?" section at bottom of every

category page with apply/nominate buttons and social share links.

  • Recruiter leaderboard (/recruiters/{slug}): public leaderboard showing top

voters who recruited friends via share links. Top 20 ranked by recruited count.

  • "I voted" card: new GD card type (voted) with "I voted for {name}!" headline

β€” voters can download and share on social media.

  • Admin analytics (/admin/analytics): comprehensive analytics page with most

viewed candidates (top 20, 30d), hottest categories (views/votes/conversion), click-to-vote funnel (link viewers β†’ vote page β†’ actual votes), daily traffic trend, and top referrers. Event filter, sidebar link added.

  • Expanded dashboard KPIs: clicks_30d and visitors_7d already in KPI grid;

analytics page provides deeper drill-down.

  • Winners carousel on FLA homepage: auto-rotating (5s) display of last year's

winners across all 10 categories β€” photo, name, category badge, cert number. CSS scroll-snap + vanilla JS; pauses on hover, touch-swipe support, dots/arrows.

  • Form-based voting (replaces LinkedIn-only for FLA): voter fills

name + email (required) + company/designation/LinkedIn (optional) β†’ receives email verification link (SMTP with on-screen fallback) β†’ click β†’ session + cookie set β†’ can vote. Token expires 24h, rate-limited 10/hour/IP, unique per event+email. New voter_tokens table, Voter module, 3 routes (/voter-form/{slug}, /voter-form/{slug}/submit, /voter/verify).

  • Applicant questionnaire (replaces old interactive AI interview): during

registration, applicant answers 10 random questions from their category's question bank β€” mix of MCQ, true/false, open-ended, behavioral. MCQ/true_false auto-scored; open-ended/behavioral scored by AI in batch (with fallback on AI failure). New questionnaire_submissions table.

  • Question bank extensions: question_bank now has type (mcq/true_false/

open_ended/behavioral), difficulty (easy/medium/hard), options (JSON for MCQ choices), correct_answer (for auto-scoring). 50 seed questions across all 10 FLA categories. Admin UI updated with type/difficulty/options fields.

  • Admin questionnaire rankings at /admin/questionnaire: per-category table

sorted by AI score, expandable rows with individual Q&A + AI feedback.

  • Applicant dashboard (/dashboard): live vote counter (10s polling), questionnaire

score + answers display, replaced old AI interview section.

  • Audit hardening: user_agent column on audit_log, voter form events

(voter.form_submitted, voter.email_verified, voter.email_verify_failed) logged with IP + user agent.

  • Schema: migration 037 (voter_tokens, questionnaire_submissions,

question_bank ALTER, audit_log user_agent), migration 038 (50 seed questions). All idempotent.

[5.4.1] β€” 2026-09-10 β€” Held-vote management loop + formal gap analysis

  • /admin/votes held-vote review queue (closes the last vote-abuse loop gap):

held votes (burst auto-hold, FR-1310) now surface first with an amber held badge + reason, per-row Release (back into every tally, voter notified, vote.hold_released audited) and Reject (soft-delete with vote.hold_rejected audit), plus a confirmed Release all bulk action (vote.hold_released_bulk). Header chip shows the open held count. Routes: POST /admin/votes/unhold|reject-held|unhold-all (admin + CSRF).

  • docs/GAP-ANALYSIS.md β€” living gap analysis (G1 deploy parity, G2 owner

keys, G3 features, G4 structural, G5 event-day), team-gated at /docs.

  • E2E phase 28: burst β†’ auto-hold β†’ release β†’ second burst β†’ reject, with

audit assertions (suite now 290 checks, all green).

  • QA hardening: e2e landing marker decoupled from registration-state copy;

webhook capture listener now multi-worker (PHP_CLI_SERVER_WORKERS=4) so keep-alive sockets can't block deliveries β€” the last marginal check is gone.

[5.4.0] β€” 2026-09-10 β€” FLA multi-page midnight-gold site + generated category art

Owner review round: SIFC removed, one-pager became a real multi-page site with a
bold new look, category art generated via the platform's own MiniMax image-01
pipeline, and a full logged-in admin-panel audit.

Added

  • Multi-page FLA site: /about, /categories, /jury, /agenda, /contact

as real routes (theme-gated), nav switched from anchors to pages; legacy + gallery pages join the same look. Home is now a cinematic landing: generated 16:9 stage hero, glass countdown chips, stats band, category/legacy/gallery teasers, crowd CTA band.

  • Midnight-gold design system (body.fla-dark): navy tokens, gold accents,

glass surfaces, .fla-hero2/.fla-catgrid/.fla-catcard/.fla-stats/.fla-tl/ .fla-jury/.fla-strip/.fla-cta2 components; agenda rendered as a gold timeline parsed from fla.agenda_note.

  • 11 generated images (10 category arts + wide hero) via MiniMax image-01

(bin/gen-real-images.php new jobs) β€” the external images hub was unreachable (its upstream api.brainiall.com is down), so generation runs in-platform. Cards map art by category name via new fla_cat_art() helper.

Changed / removed

  • SIFC sponsor removed from the homepage sponsor strip (DB row deleted with

its click history).

  • Print layout (views/print.php) now emits --brand-ink so print buttons on

gold brands get dark text instead of white-on-gold.

  • qa.sh landing marker no longer depends on registration-state copy.

Fixed (admin-panel audit, logged-in Playwright DOM audit over 34 pages)

  • Missing media files 500'd (realpath-false fell into the 404-view branch,

which then threw) β€” now serve the graceful placeholder at 200 as designed.

  • Admin dashboard horizontal scroll (25px): invisible [data-tip] tooltip

pseudo-element (nowrap 280px) overflowed the right edge β€” tooltips now wrap at 240px border-box.

  • Sidebar footer text used page-muted on the dark sidebar (2.4:1) β€” fixed;

.pill badges use --brand-strong like .badge.

  • 404/403 for /staff/home and /judge as admin were correct role behaviour

(audit probe paths, not bugs).

Verified

  • All 6 new/updated pages clean (no console errors, no 4xx) desktop + 390px

mobile, zero horizontal overflow; qa.sh 37/37 on staging AND main.

  • Backups taken before the redesign: storage/backups/db-20260910-202019.sql.gz
    • storage tarball + fla-repo-20260910.bundle (full git history).

[5.3.2] β€” 2026-09-10 β€” FLA home redesign (owner review)

  • Removed the "Message from the founder" section from the FLA landing page.
  • Explore Categories rebuilt: the old .grid-2 of image-strip cards rendered as

10 cramped slivers (root cause: .grid-2/.grid-3 were repeat(auto-fit, minmax(0,1fr)) β€” auto-fit with a 0-minimum packs unlimited zero-width columns; the tablet !important override did the same). Grids are now fixed 2/3-col (2-col on tablet, 1-col on mobile) and categories use a new showcase card: numbered gold-accent rows (.fla-cats/.fla-cat) with display-font titles, 3-line clamped descriptions, status badge and hover arrow.

  • Side effect of the grid fix: Eligibility (was 5-in-a-row), About, Contact and

every other .grid-2/3 consumer now lay out as designed.

  • Verified by screenshot (desktop + mobile): founder gone, categories clean,

no overflow anywhere; qa.sh 37/37.

[5.3.1] β€” 2026-09-10 β€” Visual/contrast audit round (Playwright DOM audit)

Programmatic UI/UX audit of the live staging site (computed styles, WCAG
contrast ratios, overflow, broken assets, console errors) β€” the code-level
equivalent of a visual pass. Verified against light + dark + mobile.

Fixed

  • CSP silently stripped every style="" attribute site-wide β€” style-src had

both a nonce AND 'unsafe-inline'; modern browsers ignore 'unsafe-inline' once a hash/nonce is present, so all inline styles (spacing, widths, flex spans) were dead markup. style-src is now 'self' 'unsafe-inline' (comment in bootstrap documents why a nonce must never be re-added there).

  • FLA gold theme contrast failures β€” new computed tokens --brand-ink (text

on brand background, picks white/dark by WCAG ratio) and --brand-strong (darkened brand for text on light surfaces) emitted by both layouts via new brand_ink()/brand_strong()/color_luminance() helpers. Applied to: .btn-primary (white-on-gold Search buttons), .site-nav active/hover links, .u-eyebrow labels (2.2:1 gold-on-white β†’ 6+:1), .badge chips; dark mode keeps the bright brand-500 for these.

  • Nav gold CTA rendered white-on-gold β€” .site-nav .btn blanket white rule

now excludes .btn-gold (dark text restored).

  • Light-mode --muted 4.44:1 β†’ 7:1 (#64748b β†’ #475569, WCAG AA body text).

Verified clean

  • Zero horizontal overflow on 390px mobile across 5 key pages; mobile tabs +

burger present everywhere; no broken images; no duplicate ids; skip-link hidden base rule works; Inter applies (bodyFont: Inter); zero console errors on all 9 audited pages Γ— 2 color schemes (CSP block messages gone).

  • qa.sh 37/37 after the round.

[5.3.0] β€” 2026-09-10 β€” Full codebase audit: 41 findings fixed, git restored

Two-agent audit (logic + UI/UX) over the entire platform. All Critical/High/Medium
findings fixed and verified; php -l clean; staging qa.sh 37/37;
qa-e2e.sh 284/284 (full lifecycle incl. nominations, ceremony, reveal β€”
up from 278/281: webhook listener hardened, GoH substitution now asserts a
valid substitution + rejection paths).
Repository re-initialized (prior git history was lost β€” baseline tag v5.2.2
re-created at root commit).

Critical

  • Telegram.php parse error bricked the whole class β€” ops alerts silent,

vote-burst alerting dead, and the queue cron's failure handler could die on the ParseError stranding claimed jobs. One-char fix restores all of it.

  • CSP vs. code: nonce CSP blocked all 78 inline event handlers β€” stage

"TAP TO START" gate, full MC console, every destructive confirm() guard, print buttons, event-switch selects, mobile burger, AI tools. Migrated to data-confirm/data-autosubmit/data-print/data-copy delegates + per-view nonced scripts (delegated events for dynamically-built MC queue).

  • Core/Controller::json() never exited β€” ~13 guarded endpoints (ceremony

goTo/queue, assistant, register/fit, push) fell through their 404/422 JSON guard and kept mutating state; one exit repairs all sites.

  • /judge/compare 500 β€” u.photo_path referenced a nonexistent column.

High

  • Live-vote JSON endpoint bypassed the leaderboard_public gate (BR-006).
  • Hall of Fame + Alumni spoiled ceremony-mode winners (bypassed reveal_mode).
  • /admin/backup shelled to /home/fla/bin/backup.sh (other deployment) with

no CSRF β€” now this repo's script + CSRF.

  • Vote hash chain self-broke after vote changes β€” rechain() re-seals the

global tail sequentially now (deterministic chain_ver 2 intact).

  • Public register forms could overwrite an existing account's whatsapp/CNIC/

socials without mailbox proof β€” fill-only/skip per C1.

  • /register/fit unauthenticated AI call now throttled (10/h per IP).

Medium & Low (31 more)

Audit CSV chunker + formula injection Β· capacity/waitlist transacted + seat counting + FR-0308 photo rule on promotion Β· template voting_mode allowlist Β· screen/embed exclude soft-deleted votes Β· auto-finish counts active categories Β· ceremony RuntimeException Β· safe redirectBack() Β· AI session creation race Β· $_SESSION['uid' dead code β†’ real user id Β· comms >100 via job queue + comms.deliver handler + orphaned-job rescue Β· queue cron Β· logout session regeneration Β· tickets double-notify/internal-note status Β· survey duplicate guard (migration 036) + type-based eligibility Β· push URL allowlist (+sw.js) Β· favicon 16px minimum Β· ICS DTEND/UID Β· webhook EVENTS accuracy Β· sponsor ref bounds Β· schedule counterpart bug.

UI/CSS

  • app.css 1325 β†’ 687 lines (removed duplicated v4.5 block); **Inter now

actually applies**; dark-mode FOUC killed (nonced theme script in <head>).

  • Admin dashboard stray </div>s; registrations nested-form β†’ form= binding.
  • Badge QRs generated locally (were api.qrserver.com β€” CSP-blocked + token leak).
  • Dark-mode light-on-light panels β†’ themed color-mix; skip-link base rule;

field-error contrast token; missing classes added (btn-warn, badge.err/ok/warn, btn-sm-cta, hoverable); guided-tour a11y (focus, Escape); FLA theme launch copy now DB-derived with identical fallbacks; countdown aria-live off.

[5.2.2] β€” 2026-09-09 β€” FLA Karachi: program v2 deployed + QA hardened

Staging https://flakhi.production1.jugaar.ai β€” LIVE

  • 10 categories (program framework) with 4 evidence-based AI questions each (40 total)
  • 70% jury / 30% verified public vote β€” events.weight_admin/public + voting.auth_mode=linkedin_only
  • Event: FLA 2026 Karachi, Sept 23 2–5pm, Expo Centre Karachi β€” registration_opens_at Sept 13, coming-soon gating on all register pages with countdown
  • Full WP absorption: jury (3) + founder + sponsors + 16 legacy winner cards + 27 gallery photos served locally

QA hardening (E2E)

  • bin/qa-e2e.sh now fully target-DB-aware via QA_DB/QADB β€” no hardcoded fla_event writes
  • Webhook delivery: port-wait + poll-retry (15s window) for inline-dispatch timing
  • Bulk-approve: visitor-type only (award without photo correctly stays per FR-0308)
  • Self-healing trap/setup: totp/must_change cleared at start and on EXIT
  • QA: main 37/37 + 281/281, flakhi 37/37 (webhook delivery 1/281 marginal β€” timing with single-threaded php -S in E2E; prod dispatch verified 200/success manually)

[5.2.1] β€” 2026-09-09 β€” FLA Karachi program v2 (10 categories Β· 70/30 jury-public)

FLA Khidmat staging (flakhi.production1.jugaar.ai)

  • 10 competitive categories per the program framework (Rising Tech Vanguard Β·

AI Pioneer Β· Digital Catalyst Β· Cyber Guardian Β· Data Frontier Β· Venture Spark Β· Knowledge Architect Β· Technology for Humanity Β· Product Forge Β· Talent Multiplier), each with shield title + subtitle + 4 evidence-based AI-interview questions (40 total).

  • Scoring flipped to 70% jury / 30% verified public vote (event weights + settings).
  • Event description, agenda (2–5 pm 10-block shield program) and banner updated

(500+ registrations β†’ 200 shortlist β†’ 100 shields model).

  • Admin password aligned (forces change on first login); staging QA tuning:

registration + magic-link IP limits raised, E2E host/DB overridable (QA_BASE/QA_DB).

  • QA: platform 37/37 Β· E2E 281/281 (main) Β· flakhi 37/37 + E2E 278/281 β†’ 281 after fixes

[5.2.0] β€” 2026-09-09 β€” FLA Karachi: WP site absorbed, staged at flakhi.production1.jugaar.ai

Added

  • APP_THEME=fla β€” full marketing one-pager absorbing awards.server4sale.com.pk:

hero (+reg-open countdown) Β· sponsor strip Β· about + ITCN Karachi Β· jury Β· founder message Β· 5 categories Β· eligibility Β· agenda Β· 2024 legacy winners Β· gallery Β· contact Β· CTA. Theme-aware nav/footer, DB-logo brand mark.

  • /legacy-winners + /gallery (16 winner cards + 27 photos, served locally).
  • events.registration_opens_at + coming-soon countdown gating on all register forms.
  • voting.auth_mode=linkedin_only (voters must LinkedIn-verify; applicants keep

email+WhatsApp+CNIC path).

  • Media route: nested storage paths (realpath-confined to uploads//cards/) β€”

traversal still blocked.

  • docs/MIGRATION.md β€” awards.server4sale.com.pk cutover runbook.

Deployed

  • /home/flakhi + fla_flakhi DB + nginx + Let's Encrypt (HTTP/2), crons (queue+backup),

49 media assets harvested, full seed (event Sept 23 2–5pm, reg opens Sept 13, 5 categories Γ—6 questions, 3 jury, founder, sponsors, legacy gallery). QA: platform 37/37 Β· flakhi surface+E2E green (see tag).

[5.1.0] β€” 2026-09-08 β€” everything except credentials

Added

  • Register flow stepper (1 Info β†’ 2 AI Interview β†’ 3 Done) β€” sticky nav + live

scroll highlighting on the award registration page.

  • Sitemap.xml expanded β€” includes /speakers, /verify-vote, /docs/srs (all public pages).

Cleaned

  • Zero test data β€” all 39 e2e demo events + their votes/registrations purged

(FK-safe β€” kept only the flagship + demo content). Fewer than 20 registrations in entire DB; votes/receipts preserved only for the demo users. QA: surface 37/37 Β· E2E 281/281.

[5.0.5] β€” 2026-09-08 β€” responsive parity complete

Fixed

  • Speakers GoH page: GoH cards grid (spk-grid) now uses auto-fit minmax(280px, 1fr)

with a dedicated min-size keystore. Collapses to 1-col ≀ 640px β€” the phantom overflow measured by audit hardware credentialed with a pixel-scan (no pixels past x=470 at 485px). No layout overflow from browser point of view; all existing QA green.

  • All grid-systems (grid-2 grid-3) now use repeat(auto-fit, minmax(0,1fr))

instead of fixed columns with toggle-chained !important rules β€” vastly more responsive. QA: surface 37/37 Β· E2E 281/281.

[5.0.4] β€” 2026-09-08 β€” polish: real sponsor logos + submit-queue zeroed

Added

  • Three real MiniMax-generated sponsor wordmarks for the flagship

(TechNova Systems badge Β· PakData Labs disc Β· SecureLayer shield) wired to the sponsors table; logos served via /media, 3-5KB WebP each.

Changed

  • Purged 36 leftover test registrations (status='rejected' with reason,

not hard-delete β€” FK/audit intact). Submitted-registrations queue is now 0. QA: surface 37/37 Β· E2E 281/281.

[5.0.3] β€” 2026-09-08 β€” FULL FLAGSHIP CONTENT: applicants Γ—4, GoH Γ—1, sponsors Γ—3

Added

  • Four photorealistic AI-generated demo applicants, one per flagship category:
    • Bilal Qureshi (Cybersecurity) Β· Nadia Siddiqui (Infrastructure & Cloud) Β·

Umar Farooq (Data & Analytics) Β· Asma Ahmed (Technology β€” kept from before).

  • One Guest of Honor: Dr. Parvez Malik (former CTO, National Bank of Pakistan).
  • Three flagship sponsors: TechNova Systems (gold), PakData Labs (silver), SecureLayer (silver).
  • Event page now renders hero + 4 category rails + applicant avatars + GoH panel + sponsor strip.

QA: surface 37/37 Β· E2E 281/281.

[5.0.2] β€” 2026-09-08 β€” theme fits + vote page restyle + print CSS

Added

  • Dark-mode fits for v5.0 components (status chips, kpi bars, f-icon shadows).
  • Print stylesheet: certificates/winner pages strip header/footer/tabs/heroes,

neutral hero colors, no shadows β€” clean PDF/paper output.

  • Vote page (public vote receiver) v2: candidate-hero card with brand-ring avatar,

AI-score badge, name/counter β€” replaces the plain photo-in-a-box block. Responsive (collapses to centered single column ≀520px).

QA: surface 37/37 Β· E2E 281/281.

[5.0.1] β€” 2026-09-08 β€” responsive pass (whole site)

Fixed

  • Tablet overflow (768px): hamburger now appears at ≀900px (was 640px); nav menu stacks

vertically with full-width tappable rows and drop shadow.

  • Join-now CTA contrast: site-nav link color was overriding button text color;

buttons in nav now keep their own colors (white text preserved).

  • Mobile ≀560px: hero compacts (260px min-height, stacked full-width CTAs),

stat strip 2Γ—2, featured-event media moves below body, category rails collapse to 90px thumbnail, timeline tightens (36px numerals), breadcrumbs wrap, event-hero margin-gap normalized.

  • Hero chips (status/type) get translucent dark surfaces (blur-backed) so white

text stays readable over bright photo areas.

  • Kicker/eyebrow spacing, foot-bottom wrapping on phones.

Verified (headless, 390/485/768px)

  • No horizontal overflow at any tested width; no content outside viewport.

QA: surface 37/37.

[5.0.0] β€” 2026-09-08 β€” full redesign, realistic AI imagery, LinkedIn live-ready

Full redesign

  • New design system: Inter (body) + Space Grotesk (headings) self-hosted, brand color scale,

spacing/radius/shadow tokens, utility classes replace view inline styles.

  • Homepage v2: photoreal hero (MiniMax), stat strip (events/votes/interviews/winners),

featured-event card, category cards with realistic AI thumbnails, 5-step timeline, CTA panel β€” all u-hero/stat/cat-card/f-card/timeline components.

  • 14 photorealistic images generated (stage, trophy, crowd, judges panel, 4 categories,

speakers stage, hall of fame, login abstract, presenter, applicant shadow, footer texture) β€” 1.1 MB total WebP, cached forever.

  • Events directory/detail: status chips, realistic category rails, breadcrumb, calendar .ics CTA.
  • Admin: sidebar grouped into Overview / Build / Operate / Monitor / Configure, KPI icons,

mail-transport KPI, LinkedIn-mode KPI.

  • Register/auth/error pages gain realistic photo banners; footer becomes 4-column with texture.

LinkedIn OAuth β€” production path

  • Branded .btn-li everywhere (login, visitor, award, GoH, voter verification).
  • Members see li-verified-chip when linked; /auth/linkedin?intent=link attaches LinkedIn

to an existing account.

  • Admin sets client id/secret in /admin/branding (masked, auditable) β€” settings keys override

env, clearing both reverts to mock mode. No redeploy needed.

  • OIDC captures headline/website claims; error detail in OAuth failures is masked and diagnosable.

QA: surface 37/37 Β· E2E 281/281 (mock-mode LinkedIn round-trip green).

LinkedIn OAuth β€” live-ready (2026-09-08)

  • Branded .btn-li buttons on /login, /register/visitor, /register/award, /guest-of-honor, /vote/{token}.
  • Signed-in members see green li-verified-chip ("LinkedIn-verified as …") instead of the button.
  • /auth/linkedin?intent=link lets a logged-in member link LinkedIn to their account (links by verified email).
  • Admin panel /admin/branding "LinkedIn OAuth" card: paste client id/secret β†’ save β†’ platform flips live

(masked display, clear-keys resets to mock, audit-logged). No .env edit needed.

  • OIDC profile now captures headline + website claims (best-effort, no schema change).
  • Onboarding + /admin/system KPI show the real LinkedIn mode (live/mock, env-aware).

[4.4.0] β€” 2026-09-08 β€” real SMTP transport + demo applicant

Added

  • SMTP mail transport (src/Core/Services/Mail/SmtpTransport.php): minimal

raw-SMTP client (no new dep) β€” HELO/EHLO, AUTH PLAIN, MAIL FROM, RCPT TO, DATA, QUIT, STARTTLS. Activated the moment MAIL_HOST is set in .env. Falls back to log transport when unset. Works with Gmail, SendGrid, Mailgun, Postmark, etc. /admin/system shows πŸ“¬ smtp / πŸ“­ log.

  • Demo applicant "Asma Ahmed" on the flagship event β€” registration with

bio, AI interview summary (87.5/100), 4-question transcript, photo, AI-composed share card (1080Γ—1080 PNG, 235KB) β€” visible to voters, judges and admin out of the box.

Fixed

  • Backup script crashed on .env source (MAIL_FROM_NAME=FLA Test unquoted,

bash split it). Properly quoted values + bin/backup.sh now produces both DB dump and storage archive.

Verified

  • SMTP driver compiles, log transport round-trips correctly through

Mailer::send() (returned true on every send β†’ 20/20 recipients marked sent by bin/send-scheduled.php).

  • Demo applicant visible: /events/fla-excellence-awards-2026 shows

1 candidate β†’ on Technology Innovation; /profile/demo-ai-tok-2026 renders name + bio + AI score; /cards/demo-ai-tok-2026/applied serves the AI-composed share card (1080Γ—1080).

  • QA: surface 37/37 Β· E2E 281/281.

[4.3.3] β€” 2026-09-08 β€” Visual QA: CSP fixed (real nonces)

Fixed

  • CSP header was malformed: script-src 'self' 'nonce-' . nonce() . ' was sent

as literal text β€” 'nonce-' . nonce() is PHP source inside a double-quoted string, so browsers received a syntactically invalid directive and silently dropped it. "Done" in v3.4.0 changelog was never actually wired. Now uses real PHP concat.

  • views/layout.php:47: nested <?= nonce() ?> inside an outer <?= ?>

echo (the VAPID-key inline script) rendered the literal <?= nonce() ?> as the attribute value β†’ CSP comparison failed β†’ inline script was blocked on every page that uses the public layout. Now: PHP concat nonce="' . nonce() . '".

Verified

  • All 7 spot-checked public pages: 0 leaked nonce literals in response bodies.
  • CSP header carries a real per-request nonce: 'nonce-gOjf5Rsj/...'.
  • Visual QA pixel-sampled: hero AI bg rgb(13,22,47) behind white text,

AI winner card zones (bg/ring/banner) all present.

  • QA: surface 37/37 Β· E2E 281/281.

[4.3.2] β€” 2026-09-08 β€” visual QA round on production

Fixed

  • Registration forms defaulted to leftover E2E events (e2e-v2-* stuck at

registration_open, newer than the flagship) β€” all e2e events now finish at E2E cleanup; prod flipped. Forms target fla-excellence-awards-2026 again.

  • Admin mobile drawer caused horizontal scroll (right:-240px extended scroll

width) β€” transform-based slide-out now (no overflow).

  • File inputs / consent-checkbox rows get 32-40px touch targets.

Verified (headless-Chrome audit, desktop + mobile + admin)

  • No broken images, no tiny text, no horizontal overflow anywhere.
  • Hero/card white-on-image contrast pixel-verified (dark navy behind text).
  • AI-composed card zones verified (bg / photo ring / banner scrim).

[4.3.1] β€” 2026-09-08 β€” AI share cards LIVE (no more waiting for quota)

Added

  • MiniMax image generation (image-01) is live β€” same provider/account as the

M3 chat AI. AiCardDriver rewritten: dual provider (minimax / openai-style), AI background + GD identity composite (circular photo, campaign banner, brand placeholders), reg-keyed cache (renders once, serves instantly). CARDS_DRIVER=ai.

  • Every card type now renders as an AI-composed 1080Γ—1080 PNG (applied, visiting,

winner, finalist, goh, hero).

Fixed

  • /admin/export?type=audit chunked export crashed (AND glued onto a

WHERE-less SQL) β€” correct WHERE/AND now.

  • /admin/votes empty-list state emitted no CSRF field (E2E 419) β€” always-on token.
  • E2E hardening for pristine databases: dup-alert confirm now targets the alias

registration (not the repeat-applicant, whose veto legitimately blocks approve); hof pagination accepts single-page archives.

QA: surface 37/37 Β· E2E 281/281.

[4.3.0] β€” 2026-09-07 β€” production launch content

Added

  • Flagship event: FLA Excellence Awards 2026 (fla-excellence-awards-2026) β€”

registration open, Karachi Expo Centre, 2026-11-14 Β· 4 categories (Technology Innovation, Cybersecurity Excellence, Infrastructure & Cloud, Data & Analytics) with 6 PK-context interview questions each.

  • Production branding: FLA Awards + tagline + GD logo mark.
  • Category cards now show matching AI-generated background accents (keyword-mapped).

Changed

  • Purged 235 synthetic E2E/QA events + orphaned test users (91 judges, 58 auditors,

visitors/applicants) β€” public site is pristine with one real event.

  • qa.sh brand check is now brand-agnostic (hero-kicker + CSS var, not fixed tagline).
  • Backup cron installed (15 3 * * *).

QA: surface 37/37 (E2E re-seeds its own data per run β€” unaffected by purge).

[4.2.1] β€” 2026-09-07

Added

  • E2E Phase 27 β€” 15 new checks covering every v4 feature: vote remove/restore (audited),

template save→draft→cleanup, speakers directory, .ics export, candidate compare, hall-of-fame pagination, recurring draft + idempotency, judges panel. E2E 281/281.

  • /admin/system: queue KPIs (queued/done/failed, recurring pending).
  • ROADMAP updated through v4.2.0; SRS FR-0213/0812/0620 (recurring, tour, judges panel).

[4.2.0] β€” 2026-09-07

Fixed

  • Vote-change reason was silently dropped (the "flaky" E2E assertion β€” a real bug):

rechain() accepted the new reason but never wrote it in one_per_candidate mode.

  • Event create 500-loop: the fixed-column INSERT missed the new recurrence columns β†’

PDO param mismatch β†’ every new-event save redirected err=slug. E2E now 266/266.

Added

  • Recurring events: recurrence weekly/monthly + recurrence_until + parent_event_id;

bin/recur.php drafts the next occurrence (categories + question bank copied) when a recurring event finishes β€” dispatched by the queue cron, idempotent.

  • Guided tour: 4-step spotlight walk-through on the admin overview, first visit only,

per-user tour_state, skip/next/back.

  • Judges transparency panel: public results page shows the judging board

(names + review counts + LinkedIn) post-lock.

  • DB persistent pooling: opt-in DB_PERSISTENT=1 (off by default; per-worker singleton

already pools).

  • csrf meta tag in both layouts; main landmarks focusable for keyboard skip.

Accessibility

  • Every <img> now carries alt (including JS-built ceremony/stage imagery).

[4.1.0] β€” 2026-09-06

Fixed β€” visual/UX audit round (headless-Chrome measured)

  • All /media/ images were 404 β€” nginx static-asset location intercepted /media/*.png

before PHP. Added location ^~ /media/ bypass β†’ app streams from storage again.

  • Missing uploads now serve a graceful placeholder (no broken-image icons ever);

cards fall back to the medal icon. media_url() encodes per path segment (no %2F).

  • Router: {param} now supports {param:.+} multi-segment patterns (/media/{file:.+}).
  • Hall of fame paginated (24/page + pager): mobile page 36,526px β†’ 8,157px.
  • Admin shell was JS-less: added app.js (toasts, theme, helpers), aria-current nav,

burger + slide-out drawer on mobile (sidebar used to stack full-width above content), dark-mode-safe search input.

  • Touch targets: mobile tab bar 10.9pxβ†’12.5px text + 44px targets; footer/table/side

links min-height; selects/inputs β‰₯40px; checkboxes 18px.

  • Hero overlays strengthened for white-on-image text (pixel-verified contrast).

[4.0.0] β€” 2026-09-06

Added β€” gap-closure release (all 25 audit gaps)

  • Vote management (admin): /admin/votes browse/remove/restore votes β€” soft-delete

(deleted_at/deleted_reason) excluded from every tally; receipts + hash chain preserved; vote.removed/vote.restored audited. Voters may re-vote after removal.

  • Event templates: save any event as a reusable snapshot (categories + question bank +

scoring config) β†’ one-click draft from template. /admin/events/templates.

  • Admin onboarding: getting-started checklist on /admin (event β†’ categories β†’

questions β†’ branding β†’ LinkedIn β†’ email), dismissible, state in settings.

  • Public GoH directory: /speakers β€” approved guests of honor with photo/title/bio/event.
  • Calendar export: /calendar/{slug}.ics per event + "Add to calendar" on event pages.
  • Candidate compare: /vote/compare/{slug}?ids=… side-by-side (max 4) with AI scores;

checkbox picker on public leaderboard.

  • Event package export: /admin/events/export?id=… full JSON (event, categories,

questions, registrations, votes, winners, result snapshots, audit).

  • Hall-of-fame search: ?q= across winners/categories/events.
  • Telegram ops channel: App\Core\Services\Telegram β€” bot alerts (vote-burst holds,

job failures) when telegram.* settings set; disabled by default (mock-safe).

  • Async job queue: jobs table + bin/queue.php worker (cron * * * * *), retries Γ—3

with backoff, handlers: telegram.send, mail.send, card.render.

  • Structured login log: login_attempts table (in addition to audit-based throttling).

Changed

  • All vote tallies/leaderboards/has-voted checks exclude soft-deleted votes (14 queries).
  • AI background images converted to WebP (78–96% smaller: 1.8MB β†’ 108KB hero).
  • HTTP/2 enabled on nginx; static cache 7d (was already), focus-visible outlines,

prefers-reduced-motion support, landscape-phone layout, skip-link polish.

  • Admin sidebar: new entries (Templates, Votes); public nav: Guests of Honor.

Security

  • CSRF on all new admin POSTs; admin-only role guards; votes removal reason mandatory.

All notable changes to the FLA Event & Awards Platform are documented here. Format: Keep a Changelog Β· Versioning: SemVer Β· Tags: annotated git tags per milestone (rule in WORKFLOW.md).

[Unreleased]

[3.6.0] β€” 2026-09-04

UX & judging/ops pack β€” dark-mode UI system, judge comparison + AI briefs, bulk certificates, ticket attachments, waitlist auto-promotion.

Added

  • Ticket attachments + internal staff notes β€” ticket_messages.is_internal

(hidden from the member) and attachment_path on ticket messages.

  • Judge comparison view /judge/compare β€” candidates side-by-side with votes,

AI scores and actions.

  • AI judge brief /judge/brief/{id} β€” contestant summary generated by MiniMax M3.
  • Bulk certificate printing /admin/bulk-certs β€” all winners + finalists on

one printable page.

  • Waitlist auto-promotion helper β€” rejecting a registration promotes the first

waitlisted entry when a spot opens.

  • Live vote counter on candidate pages β€” polls every 5s via /v/{token}/count,

pulse animation.

  • Event countdown timer on the landing page (when a voting_open event has

starts_on).

  • Search autocomplete /admin/search/suggest β€” debounced JSON dropdown.
  • Vote count API /v/{token}/count.
  • Comms retry-failed POST /staff/comms/{id}/retry-failed β€” resend to failed

recipients.

  • Vote anomaly dashboard /admin/anomalies β€” IP clusters, burst detection,

velocity heatmap.

  • Season overview /admin/season β€” multi-event comparison.
  • Event status FSM β€” invalid transitions (e.g. draftβ†’finished) are blocked.
  • Dark mode β€” CSS variables + localStorage toggle (auto-detects system

preference).

  • Toast notification system.
  • Mobile bottom tab bar.
  • Skeleton loading, tooltips, empty states, breadcrumbs.
  • Migration 028_polish2.sql (ticket attachments + internal notes + waitlist

columns).

[3.4.0] β€” 2026-09-02

Added

  • /about-scoring transparency hub β€” public explanation of the 70/30 engine, admin veto, SHA-256 hash-chain verification and winner-lock metadata.
  • /survey post-event feedback β€” 1–5 ratings + comment for attendees of the latest finished event; answers stored in survey_answers.
  • /badges printable name badges β€” admin/MC/award-team view with attendee name, category, certificate number, and personal check-in QR link (/self-checkin?t=TOKEN).
  • /admin/ceremony/setup media upload UI β€” attach anthem video, fanfare audio or custom slide media to ceremony slides (file upload + URL).
  • One-click backup β€” "Run backup now" button on /admin/system (fires bin/backup.sh).
  • Data-retention jobs β€” /admin/retention: roll up old page views, archive audit log >365 days, clean notifications >30 days. Migrations 030_retention.sql, 031_survey.sql.
  • CSP nonces β€” inline <script> / <style> tags receive a per-request nonce; 'unsafe-inline' removed from script-src.

Changed

  • ROADMAP: backlog reordered to event-day readiness, trust & growth, engineering.

[3.3.0] β€” 2026-09-01

Added

  • /how-it-works β€” one-page project overview for anyone (public + team): 30-second

TL;DR, role cards (admin/staff/applicant/voter/judge/MC/award team/auditor/AI), a 10-step visual timeline from event setup to hall of fame, accessible role-perspective tabs (ARIA tablist + arrow keys; all panels visible when printed), a 70/30 scoring breakdown with the trust layer, and an under-the-hood section. Nav link, sitemap, page-view tracking and OG tags included.

  • docs/OVERVIEW.md β€” short markdown companion rendered in the /docs viewer

("Specification" group) that links to the visual page.

  • qa.sh +2 checks (how-it-works page, overview doc) β†’ 35 checks.

Fixed

  • qa-e2e.sh pipefail flake β€” curl | grep -q under set -o pipefail races: grep exits

on first match, curl takes SIGPIPE (141) and randomly fails checks on large responses (the long-flaky "cache pages"). All four piped greps now capture first, grep after.

  • Fatal on account anonymize β€” missing use App\Core\Auth in Dashboard controller

(Auth::logout() after anonymizing 500'd; the DB write had succeeded, so tests passed while the user's session was left broken).

  • PHP warning on event form β€” $event['regs'] undefined when the edit form loads

without the aggregate; delete-button gate now checks isset() first.

[3.2.1] β€” 2026-09-01

Audit close-out: e2e suite back to 266/266 after the UTC alignment, plus the remaining accessibility/SEO polish from the 2026-08-31 audit.

Fixed

  • DB session timezone is now UTC everywhere (SET time_zone='+00:00' on connect,

matching app.timezone=UTC) β€” NOW()/CURRENT_TIMESTAMP no longer drift +5h from PHP-written timestamps. bin/qa-e2e.sh DB assertions and its scheduled_at backdate run in UTC too, so every DATE_SUB(NOW(),…) window check (audit trail, webhook delivery, magic-link expiry, page-view/click tracking, scheduled dispatch) sees the same clock the app writes with. This alone un-failed 17 checks.

  • Page cache writes are atomic (tmp file + rename) β€” a concurrent reader can no

longer unserialize a half-written entry and treat it as a miss; e2e "cache pages" check retries to ride out cold regeneration.

  • qa-e2e.sh no longer resets settings mid-flight when a previous run's EXIT trap

fires late (runs are strictly sequential now).

Added / Changed (audit UI leftovers)

  • aria-live/role="alert" on vote-candidate state banners and assistant status line;

MC queue toggle gets aria-controls + aria-expanded sync.

  • Admin layout uses the DB-driven brand favicon (falls back to /favicon.ico).
  • og:url + canonical link on OG-tagged pages.

[3.2.0] β€” 2026-08-31

Event-Day UX pack β€” stage-screen reliability, form UX, accessibility, and the rest of the 2026-08-31 full audit (audit items 7–24 from the UI/UX report).

Added

  • Stage screen start gate: "TAP TO START SHOW" overlay enables sound +

fullscreen after a user gesture; anthem video is muted playsinline until then (unmuted autoplay is browser-blocked on an unattended projector). Poll-failure heartbeat badge after ~6s of errors.

  • Real sponsor slide: stage + MC mirror render live sponsor logos/tiers from

the sponsors table (was an empty black slide).

  • Real pre-flight checklist on ceremony setup: categories/winners counts,

all-locked, ceremony media present, MC + Award Team accounts exist, sponsor count.

  • Old-input persistence + per-field errors on visitor/award/GoH forms β€”

validation failures no longer wipe the form.

  • Public category roster: /events/{slug}/category/{id} β€” linked from event-page

category cards; vote counts only when leaderboard is public.

  • Accessibility pass: :focus-visible ring, skip-to-content link, nav toggle

aria-expanded, ticket rows are real <a> links, chat UI shared via app.css (fixes the broken /messages inbox layout), button.badge affordance, print CSS.

  • MC keyboard shortcuts (Space/β†’ NEXT, ← back, B blank) on the full-screen

mirror; sponsor rendering there too; pinch-zoom re-enabled (was user-scalable=no).

  • Admin sidebar: AI Insights + Season report entries; global double-submit /

busy-state guard in app.js; GET /api/notifications/unread badge endpoint.

  • Favicon upload in Branding (brand.favicon_path); asset URLs are versioned

(?v=app.version) so browser/SW caches bust on release.

Changed

  • Comms "preview" now carries scheduled_at through to send β€” scheduling for a

future time actually schedules (button says Schedule), plus per-recipient retry-failed action (POST /staff/comms/{id}/retry-failed).

  • Award-team board auto-refreshes every 15s; MC fullscreen mirror no longer does

full location.reload() on keyboard actions (polled diffing already existed).

  • Mock/dev hints (mail log paths) hidden when APP_ENV=production.

Fixed

  • Hall-of-fame + events-directory queries now actually run inside the cache

closure (previously queried every hit and cached only the result split).

  • Member dashboard no longer renders a blank chart canvas when a registration

has no votes; event wall/avatars use loading="lazy".

  • Duplicate, contradictory select-all checkboxes on /admin/registrations.
  • Winner board: ineligible candidates' checkboxes are disabled with a hint

(mirrors the server-side eligibility rule).

[3.1.2] β€” 2026-08-31

Integrity milestone β€” transactions, finalists, deterministic vote chain, cards cache, comms + ceremony correctness (audit items C5/C3/I1/I2/I4/I7 + TODO 8/9/12/15/16).

Added

  • DB::transaction() helper; transactional winner lock/unlock (snapshot +

statuses + cert sequence + event finish under GET_LOCK('fla_cert_seq')), vote cast (mode checks + insert + chain under GET_LOCK('fla_vote_chain') with FOR UPDATE), magic-link consume (atomic single-statement claim), ceremony generate (destructive replace is all-or-nothing), queue reorder (gap/dupe-safe swap with bounds).

  • Finalist promotion on lock: non-selected eligible (approved) candidates in

a locked category become finalist β€” finalist cards/certificates are now real (workflow "runners-up β†’ finalist") and cert numbers are issued to both sets.

  • Deterministic vote hash chain v2: sha256(prev | voter | candidate | receipt)

per vote (column votes.chain_ver); chainVerified() now cryptographically recomputes v2 seals; legacy v1 seals degrade to uniqueness/ordering checks (later v2 recomputations still catch tampering).

  • Comms retry-failed endpoint + list UI; unread-notifications JSON endpoint;

event-status text on save audit meta (old→new status/weights/mode).

Fixed

  • one_per_event double-vote race closed (row lock + unique backstop);

open mode no longer emits phantom receipts, duplicate "New vote!" notifications or misleading vote.cast audit rows on re-cast.

  • Vote changes keep original created_at (votes-over-time KPIs no longer

distort). Hash chain no longer embeds time() (recomputable).

  • Share-card cache: deterministic reg{id}_{type}.png path (same-name

candidates no longer overwrite each other's cards; photo changes invalidate correctly; cache warm-up actually warms); AI driver failure falls back to GD; placeholder initials are multibyte-safe.

  • MC goto can't put another event's slide on stage (slide↔event validated in

both MC and stage state reads).

  • Public profile hides live vote counts unless the leaderboard is public,

the event finished, or the viewer is the candidate/staff (BR-006 consistency).

  • Winners board checkbox eligibility matches the controller exactly (C3 mirror);

nomination invite tokens are event-scoped; /register/done requires the form-issued token (&t=) instead of exposing sequential ids.

  • Health /health reads app.version from config (was hardcoded); footer shows

the same version; production gets a real 500 page via set_exception_handler.

[3.1.1] β€” 2026-08-31

Security hotfix β€” account-takeover closure, veto absoluteness, check-in credential separation, 2FA/login throttling (audit items C1/C2/C4/I3/I6/I8/I9/I12/I13).

Security

  • PRE-AUTH ACCOUNT TAKEOVER CLOSED (C1): public registration forms no longer

auto-log-in an existing account. Team-role emails are rejected with a portal pointer; deactivated accounts refuse; everyone else gets a one-time magic link (LoginLink) and completes sign-in via mailbox proof.

  • BR-005 veto absoluteness: bulk + single approve check vetoed BEFORE any

status UPDATE (was dead code after the UPDATE in bulk; absent in single).

  • FR-0705 enforced: must_change_password blocks ALL app routes until the

password is changed (was a one-time redirect); temp-password reset now covers mc/award_team accounts.

  • Self check-in credential separation (I3): new checkin_token secret

(migration 028 backfill) distinct from the publicly-posted share token; GET previews, POST+CSRF confirms; rejected/withdrawn/vetoed can't check in; admin scanner accepts both token kinds + status filter.

  • LinkedIn account linking (I8): email-match merges the LinkedIn identity

(was a 500 on uq_users_email); team accounts are refused with a friendly conflict page; new LinkedIn users mark their email verified.

  • 2FA brute-force protection (I9): per-IP + per-account failure counters,

pending-2FA session dropped at 5 failures; per-account password-login throttling added (I11); session cookie honors X-Forwarded-Proto for Secure behind the TLS proxy (I13).

  • Judge recusal withdraws the review (I6): decision β†’ needs_info, rubric

scores nulled β€” a conflicted review no longer counts toward the board 30%.

  • Ops docs team-gated (I12): TODO/HELP/WORKFLOW/AGENTS require a team role;

veto-after-lock takes immediate public effect (results page + hall of fame filter live veto state, I5).

Added

  • Auth::isStaff() shared helper; Audit::countRecentByUser(); migration

028 (checkin_token, votes.chain_ver, audit_log(action,ip,created_at) composite index, registration.manual_autoapprove toggle seed).

Changed

  • Staff manual entry / CSV import auto-approval of non-award entries is now

driven by feature toggle registration.manual_autoapprove (default on β€” flip in /admin/toggles to require approval per workflow).

[3.0.0] β€” 2026-08-31

Ceremony Production System β€” MC dashboard, stage screen, award team, self check-in.

Added

  • MC Dashboard (FR-3000): /mc β€” dedicated MC role; controls the entire show:

advance/back/skip/goto slides; live winner queue with shield status; reorder winners (move up with reason); call winner β†’ stage shows them; mark appeared (count++); blank screen; GoH no-show β†’ replace with substitute; stage screen + run-sheet links.

  • Stage Screen (FR-3010): /stage/{slug} β€” polling (1.5s), full-screen, brand colors;

renders all slide types; multi-GoH layout (1-N presenters side by side); anthem video/audio playback; CSS transitions between slides.

  • Award Team (FR-3030): /award-team β€” dedicated role; big buttons: "Shield Ready βœ…"

/ "Not Ready ❌"; gets notified on shield request; MC sees live status.

  • Winner Queue + Shields (FR-3020): calling order; shield lifecycle (not_requested β†’

requested β†’ ready / override_blank); reorder with reason; appearance tracking.

  • Admin Override (FR-3040): admin can force blank shield to unblock queue.
  • GoH Substitution (FR-3050): no-show β†’ replace β†’ substitute notified;

category_presenters auto-updated; goh_substitutions table.

  • Self Check-in (FR-3060): /self-checkin β€” attendees enter code/scan QR β†’

checked in β†’ see zone/seat + queue position.

  • Multi-GoH Presenters (FR-3070): category_presenters join table (1-N per

category); stage screen auto-adapts layout.

  • Ceremony Auto-generate (FR-3080): /admin/ceremony/setup β€” one-click generates

the full run-of-show from event data (slides + queue + stage_state).

  • New roles: mc, award_team; migration 027 (6 tables + role enum + seat columns).

Fixed

  • PDO repeated-parameter bug in ceremony_queue INSERT (:o used twice).
  • u.photo_path β†’ r.photo_path (users table has no photo_path; registrations does).
  • Stage screen layout reference (views/screen.php doesn't exist β†’ null layout).

[2.6.0] β€” 2026-08-30

[2.6.0] β€” 2026-08-30

Growth β€” sponsors + season report (final roadmap release).

Added

  • Sponsor wall (FR-2400): per-event sponsors (Platinum/Gold/Silver/Partner, logo upload,

URL) managed on the event form; logo wall on the public event page (cached).

  • Season report (FR-2500): /admin/report?year= β€” print/PDF-ready: totals, per-event

tables with categories, presenters, winners + certificate numbers. Migration 026.

Fixed

  • Upload import missing in Events controller (sponsor logo upload 500).

[2.5.0] β€” 2026-08-30

Section 3 β€” Day of Event.

Added

  • QR check-in (FR-2000): /admin/checkin β€” scanner-ready input accepts bare tokens or

any certificate/vote/profile URL; duplicate detection with friendly flash; undo; live present/expected counts; audited.

  • Presenters + run-sheet (FR-2100): assign a GoH presenter per category (auto notify);

printable /admin/runsheet with numbered programme, presenters, winners + cert numbers, and confirmed GoH slots.

  • Media gallery (FR-2200): member dashboard upload (photo or video URL + caption) β†’

staff moderation queue /admin/media β†’ approved media shows on the vote page + event photo wall. Migration 025.

  • Ceremony ops (FR-2300): /admin/ceremony β€” per-category reveal buttons, presenter

selects, attendance KPI, venue screen + run-sheet links, one screen.

Fixed

  • Ceremony view SQL referenced unjoined alias (500).

[2.4.0] β€” 2026-08-30

Track & Trust.

Added

  • People tracking (FR-1860): /admin/people β€” every registrant with engagement

score; /admin/people/{id} full activity timeline (audit + votes + page views).

  • Anonymous visitor tracking: page_views (hashed IP+UA+day β€” no raw PII), public

pages only, bot-filtered; never blocks a request.

  • Vote receipts + hash chain (FR-1870): receipt code shown after voting; public

/verify-vote?code=; chained seals; integrity badge on /admin/system.

  • AI category fit (FR-1880): "describe your work" on the award form β†’ AI picks the

category (mock-safe).

  • Click analytics (FR-1890): share-link clicks + unique-visitor KPIs on overview.
  • Migration 024.

Fixed

  • Bootstrap tracking used unqualified DB (silent catch) β€” now FQCN.
  • Dashboard::person typed int param vs Router string params β€” accepts string now.

[2.3.0] β€” 2026-08-30

[2.3.0] β€” 2026-08-30

Engage β€” login, messaging, AI drafting, scheduling.

Added

  • Magic-link login (FR-0180): "email me a login link" for members β€” one-time 15-min

tokens (log email + SMS), throttle setting, no account enumeration; one-time use enforced.

  • Internal messaging (FR-1830): team ↔ member threads; unified inbox /messages

(staff: new-thread + member picker; members reply); unread timestamps; ai_assisted markers; notifications + audit message.sent; thread isolation verified.

  • AI drafting (FR-1840): recipient-aware "✨ Draft with AI" in the composer +

bulk personalized drafts (select GoHs β†’ one intent β†’ per-person drafts β†’ review β†’ send all); audited messages.bulk_drafted.

  • Scheduling (FR-1850): GoH availability slots + staff requests/proposals +

GoH confirm/decline/counter-offer; /admin/schedule board + member dashboard section; notifications + full slot.* audit. Migration 023.

Fixed

  • Messages read-column bug (member_user_id_read_at β†’ member_read_at).
  • Schedule inserts: repeated PDO named placeholders (native prepares).
  • Login + magic throttles are now settings (auth.login_throttle,

auth.magic_limit_per_hour) β€” QA lifts/restores them.

[2.2.0] β€” 2026-08-30

[2.2.0] β€” 2026-08-30

Section 2 core β€” GoH completion + ratings + profiles.

Added

  • Decision messages (FR-0305b): approve/reject β†’ in-app + email + SMS (log transports) for

all types (single + bulk); GoH submit confirmation email + SMS.

  • GoH card type goh (FR-0608, campaign.goh_text) for approved Guests of Honor.
  • "Guests of Honor" section on public event page (approved, with title/company/socials).
  • GoH ratings (FR-0620): Seniority/Influence/Relevance (0–100) β†’ average; global +

per-event storage (goh_ratings), effective = event ?? global; /admin/goh-roster sortable (suggestion seed); rate form; audited goh.rated; auditor read-only.

  • Public profiles /profile/{token} (FR-1701): photo, name, title/company, bio,

LinkedIn + X/Instagram/Facebook, badges + cert no, vote counts + CTA; contact info staff-only. social_x/instagram/facebook user columns; self-editor on member dashboard; collected on GoH form. Migration 022.

[2.1.0] β€” 2026-08-30

[2.1.0] β€” 2026-08-30

Reach β€” Web Push + bulk import.

Added

  • Web Push (FR-1820): pure-PHP stack β€” VAPID ES256 JWT + RFC 8291 aes128gcm encryption

(Core\Push\WebPush), push_subscriptions, member opt-in on dashboard, sw.js push/notificationclick handlers, admin broadcast UI (/admin/push, segments), auto-push to applicants on winner.locked, dead-subscription cleanup, bin/generate-vapid.php

    • bin/push-selftest.php (crypto roundtrip green).
  • CSV bulk import (FR-0311): /admin/registrations/import β€” per-line error report,

duplicates skipped+reported, non-award rows auto-approved, audited.

  • Waitlist filter chip + import link; nomination throttle now a setting

(nominations.rate_limit_per_hour, default 5).

Fixed

  • WebPush/VAPID point extraction (PEM vs DER) ECDH bug.
  • QA: nomination throttle lifted during suite runs; 8 SIGPIPE-prone grep pipes hardened.

[2.0.0] β€” 2026-08-30

Program growth & ops — 13 features in one release (A→M).

Added

  • Nominations (FR-1800): public /nominate β†’ admin queue β†’ invite (signed prefilled

application link, log email/SMS) β†’ auto-convert on registration; webhook + audit; honeypot + per-IP throttle.

  • AI Assistant (FR-1810): /assistant β€” multi-conversation chat for the whole team

(admin/staff/judge/auditor); context-aware (live events/queues injected; PII-free for judges); async send, 30 msgs/h/user; audited by length only; mock replies until MiniMax key.

  • Judge rubric (FR-0706): 0–10 scores (innovation/impact/leadership) feed the 30% board

score (avg/30); legacy reviews fall back to approval ratio; winners board shows rubric.

  • Certificate numbers (FR-0607): FLA-<year>-<seq> assigned to winners+finalists at

lock; printed on certificates, on the QR-verify page, searchable.

  • Ceremony reveal mode (FR-1007): per-event toggle; reveal categories one-by-one

(audited); results page, venue screen and public API gated until revealed.

  • Capacity + waitlist (FR-0309b): optional per-category max_applicants; overflow β†’

waitlisted (notified + audited); admin promotes via approve.

  • Scheduled comms (FR-0907): optional send-later; queued recipients; lazy dispatch +

bin/send-scheduled.php (cron-ready).

  • Embed widget (FR-1503): /embed/leaderboard/{slug} β€” white-label auto-refresh

leaderboard; framable (X-Frame-Options relaxed only for /embed/*); snippet in event admin.

  • Data rights (FR-1700): member JSON export + irreversible anonymization

(PII/photos stripped, votes retained detached, audited).

  • Auditor role (FR-0107): read-only team access to overview/audit/registrations/

duplicates/winners/system/search; every mutation stays admin/staff-only (verified by QA).

  • Event cloning (FR-0205): copies event + categories + question bank as draft.
  • wa.me click-to-chat links on member support, admin registrations, nominations queue.
  • Public caching (NFR-16): file-TTL cache for directory/event/results/hall-of-fame/

sitemap/home; flushed on admin mutations.

  • Migrations 017–020 (nominations, rubric/cert/reveal, ops, assistant).

Fixed

  • Reveal-mode sentinel bug (empty revealed list β‰  reveal-off) β€” gating now via isRevealed().
  • Assistant page exposes a CSRF field for non-JS clients/QA.

[1.8.0] β€” 2026-08-30

Search & verification pack (Pakistan-ready).

Added

  • Global admin search (FR-1606): /admin/search β€” one query across registrations

(name/email/token), events, tickets, users; search box on every admin page; audited; anon-guarded; empty state.

  • SMS channel (FR-0906): mass comms channel=sms (log transport to

storage/logs/sms.log, per-recipient tracking; SMS_PROVIDER/SMS_API_KEY env placeholders β€” Twilio/local gateway plugs in without code changes).

  • QR verification (FR-0606): printable certificates now embed a QR code

(client-side render, print-safe) β†’ public /cards/{token}/verify page shows live winner/finalist status ("Certificate verified" / "Application on record"); unknown tokens 404.

  • Migration 016: communications.channel gains sms.

Fixed

  • Core\Search uses distinct PDO placeholders per occurrence (native prepares reject

repeated named params).

  • QA: duplicates-page assertion is now data-volume-proof (24k+ accumulated demo alerts).

[1.7.0] β€” 2026-08-30

Photo flexibility (Pakistan UX).

Added

  • Photo upload may be deferred to dashboard (POST /dashboard/photo) for award/goh; forms show hint β€œOptional now β€” you can upload later”.
  • Dashboard member page shows upload/change-photo form when photo missing, with β€œPhoto required before approval” CTA.

Changed

  • FR-0304/FR-0305/FR-0308 + UC-04/UC-05: photo optional at signup, required before approved.
  • views/register/award.php:41 + views/register/goh.php:21 no longer required on file input.

Fixed

  • Single approval and bulk approve now block when photo missing (redirect ?err=nophoto / ?skipped=N), audited as registration.approve_blocked_no_photo.

[1.6.0] β€” 2026-08-29

Scale & polish.

Added

  • Maintenance mode (FR-0106): site.maintenance toggle + message in /admin/toggles;

public returns 503 with Retry-After, admin/staff bypass.

  • Bulk ops (FR-1604): approve/reject multiple registrations from /admin/registrations

(checkboxes + bulk action, per-row audit).

  • Audit CSV export (FR-1103): /admin/export?type=audit + button on audit log page.
  • System health (NFR-15): /admin/system β€” DB size, table rows/sizes, disk free/total,

storage size, queue depths (pending regs, open tickets, dup alerts), backup recency; linked in admin nav + overview.

Fixed

  • Toggles no longer clear when saving maintenance-only (preserves feature flags).
  • Vote rate-limit / bulk / 2FA edge cases hardened via QA.

[1.5.0] β€” 2026-08-29

Integrations & experience pack.

Added

  • Webhooks (FR-1601): /admin/webhooks CRUD β€” endpoint URLs, per-event subscriptions

(registration.created, vote.cast, winner.locked, duplicate.suspected, or *), HMAC-SHA256 X-FLA-Signature + X-FLA-Event headers, delivery log with HTTP status + fail counters, one-click test ping.

  • Venue leaderboard screen (FR-1602): /screen/{event-slug} β€” dark big-display theme,

30s auto-refresh, per-category top-5; public when leaderboard is public or event is finished, otherwise staff-only.

  • CSV exports (FR-1603): registrations + votes (event-scoped) from the admin area;

audited as export.csv.

  • Admin 2FA (TOTP) (FR-1604): per-account setup with secret + otpauth:// URI and

code confirmation; password β†’ code challenge at login; disable requires a live code; wrong codes audited (auth.2fa_failed).

  • PWA (NFR-13): branding-driven dynamic manifest (/manifest.webmanifest), generated

brand icons (bin/make-icons.php), service worker (/sw.js) β€” cache-first statics, network-first pages; installable on HTTPS.

  • QA: E2E phase 16 β€” 110 checks green (manifest/icons/SW, webhook create + live

delivery + signature + ping via local receiver, screen privacy→public, CSV + guards, 2FA enable→challenge→login→wrong-code→disable round-trip).

[1.4.0] β€” 2026-08-29

Public discovery & ops pack.

Added

  • Events directory /events (live + archive) and Hall of fame /hall-of-fame

(FR-0204, FR-1006) β€” linked in the main nav.

  • Printable certificates (FR-0605): print/PDF-ready HTML pages for winners & finalists

(/cards/{token}/{type}/print), brand-colored, print CSS, verification link.

  • Public read-only JSON API v1 (FR-1501): /api/v1/events,

/api/v1/events/{slug}, /categories, /winners β€” public data only, JSON 404s.

  • SEO surface (FR-1502): /robots.txt (admin areas disallowed, sitemap reference) +

dynamic /sitemap.xml including events & results pages.

  • Vote rate limiting (NFR-12): per-IP, default 30/hour, tunable via

voting.rate_limit_per_hour setting; audited as vote.rate_limited; friendly voter copy.

  • Automated backups (NFR-10): bin/backup.sh β€” single-transaction DB dump (gz) +

storage tarball, 14-day retention, cron-ready (see HELP.md).

  • QA: E2E phase 15 β€” 91 checks green (directory, hall of fame, robots/sitemap, API

incl. 404 JSON, printable certificate, rate-limit enforcement + restore, backup artifacts).

  • QA infra: suite lifts the per-IP vote limit during runs (single-host testing) and

restores the default on exit (trap).

[1.3.0] β€” 2026-08-29

Polish pack β€” closes the remaining feasible backlog (only owner-provided keys/policies remain).

Added

  • Email verification for basic (non-LinkedIn) signups (FR-0309/0310): token issued at

registration, /verify-email?token=… completes it (one-time), audit + notification; log-mailer keeps it mock-safe.

  • Vote-change policy per event (FR-0509): vote_change_allowed toggle on the event

form; voters can update their reason (one_per_candidate) or move their vote (one_per_event) β€” audited as vote.changed; default remains "votes are final".

  • Referral attribution (FR-0510): ?ref= on share links survives the OAuth round-trip

and lands on the vote row; applicants see a per-channel breakdown on their dashboard.

  • Finalist certificate cards (FR-0604): PNG card type finalist (text from

campaign.finalist_text setting) available on the applicant dashboard after results.

  • Security headers on every response (NFR-11): HSTS on HTTPS, X-Frame-Options

SAMEORIGIN, X-Content-Type-Options nosniff, Referrer-Policy, Permissions-Policy.

  • QA: E2E phase 14 β€” 76 checks green (headers, verify flow, referral record,

vote-change off/on behavior, no-dup-on-change, finalist card PNG).

[1.2.0] β€” 2026-08-29

Release-control completions β€” nothing functional left pending except owner-provided keys.

Added

  • Winner un-lock flow (FR-1005): /admin/winners locked categories can be unlocked β€”

snapshot removed (public results retract), winners revert to finalist, event returns to judging; re-lock replaces snapshot; fully audited (winner.unlocked).

  • Team password reset (FR-0705): admin re-issues temp passwords for judge/staff

accounts from /admin/judges; forced change on next login; old password invalidated; audited (team.password_reset).

  • QA: bin/qa-e2e.sh extended to 63 checks β€” unlock round-trip (snapshot removal,

status revert, public retraction, re-lock) + reset-pass flow (re-issue, forced change, old-password rejection).

[1.1.0] β€” 2026-08-29

Duplicate-candidate detection (BR-011) + integrity fixes.

Added

  • Core\Duplicates matcher: signals = email, LinkedIn, CNIC hash (salted SHA-256 β€”

plain CNIC never stored), name+WhatsApp pair, same-account repeat-application across events; weighted scoring.

  • Scan runs on every registration path: public visitor/award/GoH, staff manual entry,

account claim-merge. Hits create duplicate_alerts rows, in-app notify all admins, audit duplicate.suspected.

  • CNIC field: mandatory on award form, optional on manual entry; format-validated

(13 digits); hash-only storage (users.cnic_hash).

  • Admin review queue /admin/duplicates: side-by-side comparison, **Confirm duplicate β†’

auto-veto the new registration, Dismiss**; duplicate.confirmed/dismissed audited.

  • Visibility: red ⚠ dup badge + "dupes" filter on /admin/registrations; open-alerts

KPI card on admin overview; sidebar link.

  • Docs: SRS FR-1300 + BR-011 + UC-17; new workflow docs/WORKFLOWS/duplicate-candidate.md.
  • QA: bin/qa-e2e.sh phase 12 (CNIC validation, repeat applicant, cross-account CNIC,

review UI, dismiss + confirm-veto) β€” 50/50 green; surface suite 33/33.

Fixed

  • Judges board no longer shows other judges' tallies pre-lock (anchoring, SRS A2).
  • Staff manual entry no longer binds to the staff/admin session account (was

attaching registrations to the operator's user row).

[1.0.0] β€” 2026-08-29

Full platform delivered in one drop (milestones v0.2.0–v1.0.0 merged per owner request), verified by a 41-check end-to-end suite (bin/qa-e2e.sh, 41/41) and the surface suite (bin/qa.sh, 33/33). Demo event content kept on the live site.

Added

  • Admin (FR-0100/UC-01): password login with IP throttling + forced password change,

Branding panel (name/logo/colors/campaign texts), feature-toggles UI, team accounts.

  • Events (FR-0200): CRUD + status lifecycle, categories show/hide/order, per-event

voting config (mode, 70/30 weights, reason toggle), public event pages.

  • Registration (FR-0300): visitor / award / guest-of-honor flows, LinkedIn OAuth

(mock-capable), WhatsApp requirement (BR-007), photo uploads, one-registration rule (BR-001/002), staff manual entry + owner claim-by-email, success pages with share links + "I have applied/visiting" cards.

  • AI interviews (FR-0400/UC-08): admin question bank per category, randomized 5-question

sessions (BR-010), AI marking (score+feedback via gateway; mock deterministic), session totals shown to applicant, judges and admin.

  • Public voting (FR-0500/UC-07): candidate share pages with Open Graph + share-card

previews, LinkedIn-verified voters (mock binds identity to browser session), restriction modes (BR-003), optional voter reason, duplicate protection, own-voters visibility (BR-006), vote notifications.

  • Judges (FR-0700/UC-10): team account creation (temp password + forced change),

event/category assignments, review board, approve/disapprove/needs-info, recusal.

  • Dashboards (FR-0800/UC-16): admin KPIs with 7-day trends, Chart.js graphs

(votes-over-time, funnel, category shares, AI distribution), audit tab with filters (FR-0807/1102), AI insights panel with anomaly flags (FR-0407), staff desk, applicant dashboard (votes chart, voters, AI transcript, cards, tickets, notifications).

  • Tickets (FR-0901): threaded tickets with staff queue, statuses, notifications.
  • Mass comms (FR-0902–0904): audience segments, preview, per-recipient delivery

tracking; email log transport + simulated WhatsApp (keys plug in later).

  • Winners (FR-1000/UC-11): 70/30 blended rankings + judges tallies, veto/unveto

(BR-005), approve-&-lock snapshots (BR-009), auto-finish, public results page, winner badge cards (PNG).

  • Platform core: CSRF on every form, role guards, audit writer on all sensitive

actions, media streaming route, on-demand GD card service, log Mailer, notify queue.

  • QA: bin/qa.sh (33 surface checks) + bin/qa-e2e.sh (41-check full lifecycle).

Fixed

  • move_uploaded_file failures: storage/ must be owned by the php-fpm user

(documented in HELP.md); uploads/cards/logs/cache are www-data-writable.

  • Mock LinkedIn identity is now stable per browser session (true duplicate-vote testing).

Security

  • All POST endpoints enforce CSRF tokens; team logins throttled (5 fails/10 min/IP)

with audit entries; upload validation (type/size/dimensions); forced initial password change for created accounts; winner announces happen only via audit-locked snapshots.

Pre-1.0 QA fixes (delivered as commits after v0.1.0)

Added

  • SRS: admin dashboard spec expanded β€” graphs (FR-0805: votes-over-time, funnel,

category shares, top candidates, AI score distribution), full stats coverage (FR-0806, 7d/30d trend KPIs), audit tab (FR-0807), AI insights (FR-0407: weekly summaries, judge-ready applicant briefs, vote-anomaly flags), new use case UC-16, chart-stack decision D11 (Chart.js CDN + JSON endpoints). Roadmap updated: v0.2.0 gets a dashboard starter, completion in v0.8.0.

  • bin/qa.sh β€” repeatable QA suite (29 checks: route statuses + body markers for every

public page and all docs, security probes .env/.git/path traversal, HTTPS redirect). Usage: ./bin/qa.sh [base-url].

Fixed

  • Docs viewer fatal error (sort($paths ??= []) β€” expression cannot be passed by

reference); /docs, /docs/srs and all workflow pages restored.

  • Doc slugs are canonical: docs/SRS.md β†’ /docs/srs, workflows β†’ /docs/workflows~<name>;

registration placeholder cross-links updated accordingly.

  • QA script: grep marker handling (-- guard) and correct HTTPβ†’HTTPS redirect probe.

Security

  • database/migrate.sh no longer carries a default DB password β€” credentials come from

.env / environment, script refuses to run without them.

  • Production posture: APP_DEBUG=false in live .env β€” stack traces are no longer

rendered to visitors (errors go to logs only).

[0.1.0] β€” 2026-08-29

Docs + scaffold baseline.

Added

  • Plain-PHP modular core: Router, DB (PDO/MariaDB), Settings, View, Config,

Env, Auth session skeleton, helpers (url(), setting(), absolute_url()…).

  • Module skeletons under src/Modules/: Home, Docs, Registration, Branding, Events,

Ai, Voting, Cards, Judges, Tickets, Comms, Winners, Audit, Notifications, Dashboard.

  • Docs viewer (/docs, /docs/{slug}): renders all repo .md files as organized

webpages with TOC + mermaid support (FR-0103).

  • Core services:
    • AI gateway β€” MiniMax platform driver (minimax-m3, OpenAI-compatible) + deterministic

MockProvider; mock active until keys (NFR-08).

    • LinkedIn OIDC client + mock fixture.
    • WhatsApp null driver (logs to storage/logs/whatsapp.log).
    • Share-card generator: GD driver (applied / visiting / winner, 1080Γ—1080 PNG);

AI driver reserved (cards.driver).

  • Landing page (responsive, DB-branded), 404 page, registration placeholder pages.
  • Database migrations 001–009: settings(+seeds), users, events, categories,

registrations, question_bank, ai_sessions, ai_answers, votes, judge_assignments, judge_reviews, tickets, ticket_messages, feature_toggles(+seeds), audit_log, notifications, communications, communication_recipients. Runner: database/migrate.sh.

  • Documentation set: docs/SRS.md (business use-cases UC-01…15, FR/NFR, BR-001…010),

7 workflow docs (docs/WORKFLOWS/), TODO.md (session handoff), ROADMAP.md, WORKFLOW.md, HELP.md, AGENTS.md.

  • .env.example, .gitignore, composer.json (PSR-4 autoload only β€” no framework),

bin/card-demo.php, Apache-inline public .htaccess.

  • Live deployment: nginx vhost (deploy/nginx.conf template) serving

https://event.production1.jugaar.ai on plain 443 — Let's Encrypt certificate issued, HTTP→HTTPS redirect enabled. Verified public responses on /, /health, /docs/*.

Fixed

  • Router treats HEAD as GET (monitoring tools send HEAD).

Security

  • .env git-ignored; PDO prepared statements; session cookie flags

(HttpOnly, SameSite=Lax, Secure on HTTPS).