Sem Panela
Lista de presença, sorteio de times equilibrados e controle de pagamentos da sua pelada.
翻譯準備中
尚未收集儲存庫資訊。
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗v1.5.6
A football-only AI chat app for fans and creators: match and player analysis, research with sources, match-day scripts and club banter, backed by Gemini's live Google Search, our own checked data bank (fixtures, results, tables, facts) and real match/coach data (API-Football), not just whatever the model remembers from training. Runs as a website and as an Android app (see the creator-booth-app project).
GEMINI_API_KEY — your Gemini key (free tier, no card)GEMINI_MODEL — comma-separated, in priority order. Recommended and now the default: gemini-2.5-flash,gemini-2.5-flash-lite. Gemini 2.5 is used because Google Search grounding is free on it (500 searches/day shared); Gemini 3.x search needs a paid account. If a model is gone, busy or out of quota, the next one answers.GROQ_API_KEY — backup AI only (plain openai/gpt-oss-120b, no search) plus small helper tasks (openai/gpt-oss-20b). Groq's search models (groq/compound*) were retired and are no longer used.API_SPORTS_KEY — API-Football key (no spaces or quotes). Free plan: 100 requests/day, today ±1 day only.SUPABASE_URL — https://krmjvfemsufduqdofuqy.supabase.co (same project as the app's database)SUPABASE_SERVICE_KEY — Supabase → Project Settings → API Keys → the secret / service_role key. Server only; never put it in the website. Used by the data bank.ADMIN_KEY — a long password you invent. It opens /admin.html.CRON_SECRET — another long random string. Vercel sends it to the daily update so nobody else can trigger it.Our own database of results so answers come from checked data, not the model's memory. Leagues covered: Premier League, La Liga, Serie A, Bundesliga, Ligue 1, Saudi Pro League, UEFA Champions League, Europa League, Conference League, and Inter Miami (any competition). Seasons are named by the year they start (2025 = 2025/26).
One-time setup
bank.sql (kept outside this repo; already run for this project) → Run. The tables are private: the public key in the website cannot read or change them.https://creator-boot.vercel.app/admin.html, enter ADMIN_KEY.How it stays current
vercel.json cron → /api/cron-sync): API-Football for yesterday/today/tomorrow (fills results, kick-off times, postponements), then the openfootball file for the current season. Overview shows a red banner if the last run failed and an amber one if it is over 36 hours old. "Run the daily update now" is on the Log tab.Admin tabs — Overview (completeness per league and season, gaps, conflicts, sync health) · Gaps (type scores or mark postponed) · Import (paste JSON with a preview of what changes, or load from openfootball; "Copy format" gives you text to paste to ChatGPT/Gemini so it returns JSON in our shape) · Matches (filter, edit, delete, see the computed table) · Clubs (merge two spellings) · Inbox · Facts (hand-checked facts such as honours; shown to the AI only when a question contains one of their keywords) · Log.
How the chat uses it — GET /api/bank?q=…&today=… works out which leagues, clubs and seasons the question is about and returns a block starting === VERIFIED DATA BANK === with the table, champion, recent results, next fixtures or head-to-head. It states how complete the season is (COMPLETE, or "N of 380 stored" plus which results are missing) so the AI says "not stored yet" instead of guessing. If the bank is down or empty the chat works as before.
Known limits — Tables are computed from results: Serie A and the Saudi league tie-break rules are not verified (flagged in code), UEFA competitions show results/finals only, no tables. Only the leagues above exist; other competitions are ignored on import. Hobby-plan cron frequency was not verified against Vercel's docs; daily is the lowest limit I know of.
Version numbers count like an odometer. Each release raises the LAST number by one. When the last number
would pass 9, it goes back to 0 and the MIDDLE number goes up by one; when the middle number would pass 9, it
goes back to 0 and the FIRST number goes up by one. So: 1.3.3 → 1.3.4 → 1.3.5 → 1.3.1 … 1.9.9 → 2.0.0.
(An earlier version of this note said 1.2.9 → 1.2.10; the owner's rule is the odometer rule above.) One release =
one increment, no matter how many changes it contains. The Android app project (package.json + the app README)
uses the same number.
This README must be updated on every change, in the same commit/patch as the code. Not just the version number — the relevant sections above too (features, known gaps, deploy steps) if they changed. A README that describes an old version is worse than no README, because it actively misleads whoever reads it next.
On every update:
APP_VERSION in index.html (single source of truth — it drives
both the header tag and the Settings screen automatically)."version" in package.json to match.Run this once in Supabase → SQL Editor. Until you do, the feedback form shows "Couldn't send it just now".
create table if not exists feedback (
id uuid primary key default gen_random_uuid(),
created_at timestamptz not null default now(),
user_id uuid references auth.users(id) on delete set null,
rating smallint not null check (rating between 1 and 5),
category text check (category in ('bug','idea','confusing','praise')),
message text not null check (char_length(message) between 5 and 2000),
contact text check (contact is null or char_length(contact) <= 200),
app_version text,
platform text,
viewport text
);
alter table feedback enable row level security;
create policy "Anyone can send feedback" on feedback
for insert to anon, authenticated
with check (user_id is null or user_id = auth.uid());
-- Deliberately no SELECT policy: nobody can read feedback through the app.
-- You read it in Supabase → Table Editor → feedback.
v1.4.0 adds a personalization survey and Profile settings backed by a new Supabase table. Run this once in your Supabase project's SQL Editor:
create table if not exists profiles (
user_id uuid primary key references auth.users(id) on delete cascade,
surname text default '',
other_names text default '',
nickname text default '',
referral_source text default '',
referral_other text default '',
use_case text default '',
favorite_club text default '',
notes text default '',
script_preferences text default '',
onboarded boolean default false,
created_at timestamptz default now(),
updated_at timestamptz default now()
);
alter table profiles enable row level security;
create policy "Users manage own profile" on profiles
for all using (auth.uid() = user_id) with check (auth.uid() = user_id);
Until this table exists, profile/onboarding features fail silently (no onboarding prompt, greeting falls back to email prefix) rather than breaking sign-in — but run this before shipping v1.4.0 so personalization actually works.
v1.7.0 makes the thumbs up/down buttons actually persist instead of being local-only UI. Run this once in your Supabase project's SQL Editor:
alter table messages add column if not exists feedback text;
Until this column exists, tapping thumbs up/down still gives visual feedback but the save silently fails (caught and ignored, same fail-open pattern as the rest of this app) — so it's safe to deploy before running this, just won't actually persist until you do.
Attached images are now compressed client-side (max 1024px, JPEG ~70% quality) and saved as base64 directly in the messages.media jsonb column — not Supabase Storage. This is the simpler option and fine for personal/moderate use, but it counts against Supabase's free-tier database size limit (500MB), not the separate Storage limit (1GB), and base64 is ~33% larger than the raw bytes. If image use grows heavy, migrating to Supabase Storage (storing a URL instead of the bytes) would be the next step — flagging now so it's a deliberate choice later, not a surprise.
v1.3.6 — Gemini 2.5 + data bank. Gemini 2.5 Flash / Flash-Lite are the primary models (free Google Search grounding); a model that is gone, busy or out of quota hands over to the next. Groq live research (groq/compound*) is removed — it was retired by Groq. If Gemini's search is refused, the answer is written without search and the orange note says why ("Gemini search limit reached for now"). Groq is now only a plain backup (gpt-oss-120b, no tools); with an attached image and Gemini down the app says so honestly instead of guessing. New: data bank (/api/bank, /api/cron-sync, /api/admin, admin.html, supabase/bank.sql) — see "Data bank". /api/groq now only accepts openai/gpt-oss-120b and openai/gpt-oss-20b and caps request size. The "denial" guard no longer blocks an answer that used verified data. The Verified badge no longer says "API-Football". Tests: bank 36, API handlers 21, chat flow 20, update flow 28, unit 10.
v1.3.5 — The orange "no live web search" note now ends with the reason the research step failed, e.g. [research: too slow] or the Groq error text, so a failure can be diagnosed from a screenshot. No other change.
v1.3.4 — Update notice in the Android app, and a better-researched, more honest chat. (Website code is shared with the app, so the update code is inert in a browser tab.)
version.json from the GitHub release and, when a newer version exists, opens an "Update available" sheet with Update now (downloads the web files and restarts). If that fails, or the release needs a fresh install, the sheet shows Download update (the APK, via /download on this site). Before this, the notice only appeared after a silent background download had already succeeded, so any failure meant no notice at all.isDebatedTopic) always use the multi-search model (groq/compound). Short now only controls how long the answer is, and may use about 200 words on a debated topic. Plain questions in Short still use the fast model, and if it fails the stronger one is tried (never more than about 40 s of trying).This is a rebuild of an earlier project (previously "Match Day Script Booth"). The backend proxy pattern carried over; the frontend was replaced with a new chat-style UI, and the feature set was intentionally slimmed down in the process — see What this version doesn't do yet below before assuming parity with the old app.
MODES in index.html, no architecture changes.meta-llama/llama-4-scout-17b-16e-instruct) instead of the usual
text-only one.localStorage instead of
the account.Speaker:- text turns, either
copied to clipboard or downloaded as a PDF. PDF generation uses jsPDF,
lazy-loaded from a CDN only when Download is actually clicked./terms.html, /privacy.html) — short,
accurate placeholders, not full legal documents.Carried over from the old app's README/code review, for anyone expecting parity — these were cut or simplified during the rebuild, not forgotten:
| Feature | Status |
|---|---|
| Voiceover / TTS (ElevenLabs, Google Cloud TTS) | Removed. Deferred intentionally; no voice UI in this version at all. |
Projects / saved workspaces (createProject, saveWorkspace, etc.) | Not carried over. Current app only has chats — no separate "project" concept. |
Football tool-calling loop (toolGetMatchFacts, toolGetHeadToHead, live function-calling) | Simplified. Replaced with a single one-shot fixture lookup for today's matches only. No head-to-head, no multi-turn tool use. |
| Chat images persisted to history | Not yet. An attached image is used for that one request only; reloading a chat shows a text placeholder, not the image. |
| Custom email sender (vs. default "Supabase Auth") | Not set up. Requires a domain + SMTP provider (Resend, etc.) — see in-app disclaimer instead. |
If you want any of these back, the architecture (see below) is set up to make that additive rather than a rewrite.
Everything lives in one index.html (~55KB) plus a handful of serverless
proxies under api/. It's intentionally still a single file at this size —
see the "splitting it up" note below for when that stops being true.
callAI() centralizes all model calls: tries Gemini first (with
whatever tools/system_instruction were requested, including search
grounding), falls back to Groq on any failure. geminiToolsToOpenAI() /
geminiContentsToOpenAIMessages() / openAIResponseToGeminiShape() handle
the format translation between the two APIs, including image content.MODES is a flat array of {id, icon, label, system} — the entire
"personality" surface of the app. No per-mode branching anywhere else in
the pipeline.gatherFootballContext() does the one-shot API-Football lookup;
runPipeline() is the single request/response flow every mode goes
through.localStorage.At some point — a "Movie Creator" mode, a "Gaming" mode, real tool-calling coming back — this stops being comfortable as one file. The natural split, whenever that's warranted:
js/
chat.js runPipeline, callAI, MODES
football.js gatherFootballContext + any tool-calling that comes back
auth.js Supabase auth screens
ui.js drawer, settings, modals, header state machine
storage.js chats/messages persistence (Supabase + localStorage)
Not needed yet at this size — noted here so it's a deliberate choice later, not a surprise.
Push this folder to a GitHub repo, or replace the contents of an existing one.
Go to vercel.com → New Project → Import that
repo. No build settings needed — Vercel auto-detects api/ as
serverless functions and serves index.html as-is.
Add your keys: Project → Settings → Environment Variables →
GEMINI_API_KEY — from aistudio.google.com/apikeyAPI_SPORTS_KEY — free key from dashboard.api-football.com (Account → My Access)GROQ_API_KEY — (optional) free key from console.groq.com/keys, no card
required. Automatic backup when Gemini's quota runs out. The chat still
works without it, it just has no fallback.Then redeploy (Deployments tab → ⋯ → Redeploy) — Vercel doesn't hot-reload env vars into an existing deployment.
Configure Supabase auth redirect URLs (separate from Vercel): in your
Supabase project → Authentication → URL Configuration, set Site URL
to your deployed domain (e.g. https://creator-boot.vercel.app) and add
it to Redirect URLs as https://your-domain/**. If this is still set
to localhost, both Google and email sign-in will redirect to a dead
localhost URL after auth — this bit us once already.
Open the deployed URL and click through: sign-in (both methods), each mode, a football request, an image attach, and incognito — before trusting it's fully working. None of this has had a real end-to-end test pass yet as of v1.0.0.
Supabase pauses free-tier projects after 7 days with no database
activity — the project goes offline (DNS_PROBE_FINISHED_NXDOMAIN when
you hit its URL directly) until manually restored from the dashboard. This
happened once already, after a 2-month pause between working sessions.
Restoring is free and keeps all data, but it's an avoidable interruption —
the project resolves this the same day it happens, but only if someone
notices and restores it.
.github/workflows/supabase-keepalive.yml runs a real SELECT against the
chats table every 3 days (safe margin under the 7-day limit), so the
project never goes quiet long enough to trigger a pause. It needs no setup
— it's already wired to this project's URL/anon key (same ones hardcoded in
index.html; the anon key is public by design, so there's no security
reason to hide it as a GitHub secret here too). Confirm it's running: repo
→ Actions tab → "Supabase Keep-Alive" should show green runs every 3
days. You can also trigger it manually from there (Run workflow button)
to test it immediately.
If the project ever does get paused again anyway (workflow got disabled, a run failed silently, etc.) — restoring from the dashboard is safe up to 90 days paused; past that, the "Restore" button is disabled entirely and you'd need to download the backup and create a fresh project instead. Don't let it sit paused for months on the assumption it's fine.
npm i -g vercel
vercel dev
Runs the same api/ functions locally on http://localhost:3000. Put your
keys in a local .env file — vercel dev loads it automatically. Don't
commit .env. Note: if you test locally, your Supabase redirect URL config
needs http://localhost:3000 in it too (in addition to, not instead of,
your production domain) or auth will break in one environment or the other.
index.html the app — chat UI, all client-side logic
terms.html Terms of Service (short placeholder)
privacy.html Privacy Policy (short placeholder)
api/gemini.js proxies Gemini generateContent (grounding + vision + system_instruction passthrough)
api/groq.js proxies Groq chat completions (plain backup only; two allowed models)
api/download.js /download: newest APK (Supabase copy first, GitHub fallback)
api/bank.js public read: verified-data block for a question
api/admin.js admin actions (needs ADMIN_KEY)
api/cron-sync.js daily update (Vercel cron, needs CRON_SECRET)
lib/ bank logic: bank-core, bank-sync, bank-context, bank-store, sync-run, apisports
(SQL files are run in Supabase and not stored in this repo)
admin.html the admin page (website only; not bundled in the Android app)
api/football/[...path].js proxies any API-Football endpoint, e.g.
/api/football/fixtures?date=2026-07-27
api/google-tts.js unused by the current frontend — leftover from the old voiceover pipeline
api/elevenlabs.js unused by the current frontend — leftover from the old voiceover pipeline
vercel.json function config
package.json name/version/description only — no build step
api/google-tts.js and api/elevenlabs.js are dead code from the pre-rebuild
version — safe to delete if you're not planning to bring voiceover back, kept
for now since removing voiceover was a UI decision, not evidence the backend
routes are broken.
Each club's current squad (players/squads?team=, 1 request per club) is fetched on spare requests only: quiet hours 02:00-07:00 UTC, the same busy-day limits as the transfers job, and squads and transfers share one daily allowance and the 3-requests-per-run cap (squads go first). Refreshed weekly.
bank_api_squads (applied). Tests: tests/squads.test.mjs. Not filled by the API: coaches, joined dates, club status beyond "active".Admin → Clubs now lists names in the chosen league that are probably one club spelled two ways: one name's words are inside the other's ("Celta" / "Celta Vigo", "Bologna" / "Bologna FC 1909") or the names differ by 1-2 letters ("Al Taawoun" / "Al Taawon"). Two names that have played each other in that league are different clubs and never appear. Each pair has Merge A into B, Merge the other way and Different clubs (remembered in the new table bank_not_dupes, already applied). Merging uses the same move as before (matches moved, double matches dropped, spelling learned). Spellings that look nothing alike ("Olympiakos Piraeus" / "Olympiacos") are still found by hand in the full list. Tests: tests/suspects.test.mjs.
For matches API-Football does not cover. Admin → Import → Match details by hand: paste one match or {"matches": [...]} (up to 40), press Check, then Save. "Copy format for ChatGPT / Gemini" gives the exact shape and rules to give an AI with the match report.
score (sets the result, marks it verified), venue, referee, events (goal, own goal, penalty, yellow, red, sub, each with minute and optional stoppage extra), stats as [home, away] pairs (possession, xg, shots, shots_on_target, shots_off_target, blocked, corners, offsides, fouls, yellow_cards, red_cards, saves, passes, passes_accurate), lineups (formation, coach, 11 starters with optional grid like "2:3" for the pitch picture, subs, optional ratings).bank_match_details.api_fixture_id is now optional (already applied). Tests: tests/manualdetail.test.mjs.The few team ids learned free from fixtures made the job think a league was already mapped, so the one-off teams request never ran for La Liga and Ligue 1 (only 2 clubs each). It now runs that request once per league regardless. Test added.
Every row in Admin → Gaps has a Change date button: pick the new date (and optionally a UTC kick-off time) and press Move. The match stays scheduled with its score cleared, is marked verified, and the daily result check then looks for it on the new day. New admin action setDate; cup match ids that include the date are re-keyed. Tests added.
Every club in the leagues that API-Football covers (EPL, La Liga, Serie A, Bundesliga, Ligue 1, Saudi Pro League, Inter Miami) gets its recent transfers (since 1 July 2025) with date and type (Loan / Free / fee text), 1 request per club.
teams request per league that has none (season 2024, the newest the free plan allows). Clubs that could not be matched are listed in Admin → Profiles.bank_team_ids, bank_club_transfers (already applied). Tests: tests/transfers.test.mjs (8).Found while checking the gaps list against independent match pages:
API_DATE_WINDOW_DAYS (default 1) widens this on a paid plan.Each squad player may now have joined (2026-08 or 2026-08-30) and joined_from (club). Both are optional; leave out what is not known. Stored in bank_players.joined / joined_from (already applied).
mergeHistory), and the "may have left" check no longer flags him (our data is newer, so there is nothing to confirm).as_of, before the birth date, or not in YYYY-MM[-DD] form is rejected.In Admin → Profiles, tapping He left or Our data is right now only marks your choice (highlighted, tap again to undo); the page no longer reloads each time. A bar at the bottom shows the count and Confirm N answers saves them all at once (resolveFlags). Everything unanswered: our data is right fills the rest in one tap. Players marked He left are removed from their squads only when you confirm.
Admin → Profiles (new tab). Nothing changes in the chat yet: this release only stores the data and checks our squads against it.
bank_player_profiles (8-player-profiles.sql, already applied).importProfiles, profileStats, checkFlags, flags, resolveFlag. Tests: tests/profiles.test.mjs.vercel.json cron changed from 0 5 * * * to 0 1 * * * (Vercel cron times are UTC; Nigeria is UTC+1 all year). Vercel's free plan may start a daily job at any point within that hour, so expect it between 02:00 and 03:00 Nigerian time. The 20-minute result check (GitHub Actions) is unchanged.
app-updates was private, so every Supabase address answered HTTP 400. The app's quick update and the /download link therefore always fell back to GitHub. The bucket is now public (it only holds the public update files: version.json, bundle.zip, creator-booth.apk). If the update screen ever shows "Supabase 400" again, check Supabase → Storage → app-updates → Public bucket.Tapping the fixture card under a chat answer opens a full-screen match screen with Facts, Lineup, Table, Stats and (for the European cups) Play-offs. It reads our own data bank through /api/match; it never calls the football API itself.
ZONES in lib/match-view.js (one place to edit). Extra European places depend on UEFA rankings and cup winners, so check them each season.Where line-ups and stats come from. When the due-match check fills a result it also records API-Football's fixture id. The same 20-minute run (/api/cron-sync?mode=due) then fetches fixtures?id= once per finished match: 1 API credit, saved in bank_match_details, free for every later view. At most 3 per run, only for matches finished in the last 3 days, and an API error pauses detail fetching for an hour so it cannot use up the 100 daily requests. Matches before this release, and matches the 02:00 (Nigerian time) daily sync filled first, have no details and show "No data available".
Setup (once): run 7-match-details.sql in Supabase (already applied to the project). No new environment variables. No new APK needed.
Tests: tests/matchview.test.mjs (13).
.github/workflows/due-sync.yml, every 20 minutes, calls /api/cron-sync?mode=due) and every chat question (/api/bank, 4-second limit). Nothing due means no API request. The daily Vercel sync stays as it was.?mode=due address needs no key: it only reads stored matches and cannot spend more than the limits above. No SQL, no new env vars or secrets.sync-due appear on the admin Log tab.clubBadgeHtml) without touching anything else./api/bank now also returns cards. No SQL, no new env vars.FIREBASE_SERVICE_ACCOUNT (the whole service-account JSON), and 6-device-tokens.sql run once in Supabase (paste the contents).api/device-token.js links a phone to the signed-in person (also removed on sign-out). Code: lib/push.js (no dependencies; signs the Google token with Node's crypto)./api/script-job (up to 300 s, runs server-side). The chat says "I'm on it", you can leave, and the finished script is saved into the chat. The app shows a notice when it is ready (and when you next open it). A failure saves a visible message with a Regenerate hint.5-script-jobs.sql once in the Supabase SQL Editor (paste the contents).vercel.json now sets 300 s only for api/script-job.js (60 s for the others). On Vercel Hobby, 300 s needs Fluid compute (Settings → Functions) on older projects.league, season, club, as_of (date the list is true), coach {name, nationality} and players with name, shirt, dob, age, position, nationality, club_status ("Active" or "Retired (2026)") and national_status ("Active", "Retired (2022)" or "Not capped"). "NOT FOUND", blanks, N/A and "-" are stored as empty, never as text. A player with an age but no date of birth keeps the age and the as_of date it was true on.on_loan and loan_from.4-chats-last-used.sql (adds chats.updated_at); until it is run, the order still works on the device you use but is not shared between devices.nationality is the country he represents at senior level (or is eligible for if uncapped); other_nationalities lists any other citizenships. A national team is built later by grouping on this, so players are never entered twice.tournaments: registered squads only. Each entry is tournament, year, nation, status (registered or withdrew_injured) and optional stats. Allowed tournaments: FIFA World Cup, UEFA European Championship, Copa América, Africa Cup of Nations, AFC Asian Cup, CONCACAF Gold Cup, Olympic Games (common short names such as "World Cup" or "Euros" are accepted). A player who was not in the final squad simply has no entry, so each tournament's squad can be rebuilt from the profiles.totals now means club career only. National-team figures live in the new international list: one row per nation and level (senior, Olympic, U23 … U16), so a player who switched nations has one row for each.by_club: one row per spell at a club (from, to, and that spell's stats). Dates alone are fine; stats are optional. The rows can never add up to more than totals.kind: individual or team.as_of rule is unchanged and covers club, season and national-team stats alike.admin.html, lib/bank-career.js, tests/career.test.mjs, README.md and package.json changed.totals, per-season stats (seasons), awards, market_values (each with a date) and transfers. Every section is optional; only name is required, plus as_of whenever totals or seasons are given.as_of is the guard. It is the date the figures are correct up to. Matches recorded in the bank count from that date on, so recorded matches and imported career figures are never added together. A newer import replaces older stats; an import dated older than what is stored leaves the stored totals and seasons alone (awards, transfers and values are still added).as_of; money must be a plain number with a date; duplicate season rows are refused. A stat name that isn't a standard one is kept but warned about (catches assits).2-career-profiles.sql once in Supabase (creates the private bank_career table). Until then the card shows a "not ready" notice and nothing else on the page breaks.node tests/career.test.mjs.ADMIN_VERSION in admin.html. Bump it with package.json on every release./download now saves the file as creator-booth-v<version>.apk (read from the published version.json), so downloads are recognisable. The browser still adds (1), (2) if you download the same version twice; delete old copies. Once the app is on v1.3.8 or later, updates arrive inside the app and the APK is not needed again. If the Supabase copy is missing, GitHub is used and keeps the plain name creator-booth.apk.admin.html, api/download.js, README.md, package.json. No SQL, no environment variables, no app update.bundle.zip, version.json and the APK to a public Supabase Storage bucket named app-updates (the same host the app already uses for chats and sign-in). The app checks both Supabase and GitHub, takes the newest version, downloads from that host and automatically tries the other if it fails. The update is the web files only (expected to be well under 1 MB; not measured here because the real build needs the app repo's CI), not the 5 MB app file./download (the APK link) now goes to the Supabase copy when it exists (api/download.js), otherwise GitHub as before./admin.html in Chrome on the phone → ⋮ → Install app (or tap Install app in the page header). It gets its own "ADMIN" icon. It caches nothing, so it is always the newest admin page; deploying the website IS the admin update. admin.webmanifest, admin-sw.js, admin-*.png.index.html (unchanged flow: copy it into the app repo's www/). admin.html is never part of the app.app-updates (public) in Supabase and add two GitHub secrets to the APP repo, SUPABASE_URL and SUPABASE_SERVICE_KEY. Without them the build still works and updates come from GitHub as before.{"league":"EPL","season":2026,"club":"Arsenal","players":[{"name":"…","shirt":7,"dob":"2000-01-31","position":"Forward","nationality":"England"}]} (or an array of groups). Preview warns about a repeated shirt number in a club/season and about the same player at two clubs. Date of birth is stored; age is calculated when shown. Do not import unverified players.CLUBFRIENDLY, NATIONFRIENDLY, WORLD_CUP, EURO, AFCON, COPA_AMERICA, WC_QUALIFIERS, EURO_QUALIFIERS, AFCON_QUALIFIERS. They are competition categories; no results are included.1-competitions-and-players.sql once in Supabase (paste its contents into SQL Editor). It widens the allowed competition list and creates the private bank_players table. The SQL is kept out of this repo on purpose.node tests/matcher.test.mjs.index.html (still v1.3.6), so the Android app needs no update.coachs?team=, +1 request per club); the current coach is the one whose career entry at that club has no end date.lib/link-clubs.js): 1) a league with no club list yet costs 1 request (La Liga and Ligue 1 had never been read); 2) free re-match of API teams we already know but could not name-match, with a looser rule ("Leverkusen" = "Bayer 04 Leverkusen"); 3) optional search by name, 1 request per club, accepted only when exactly one plausible club of the right country comes back. Unclear ones stay unlinked, never guessed. 5 requests per batch, 40-second pause.status call, which per its documentation does not use a request. If the answer has an unexpected shape the line says "unavailable" instead of guessing.DETAIL_LEAGUES (e.g. EPL,LALIGA,SERIEA or *) to change this.v1.2.4 — About the author filled in (About page → "About the author"): name, short bio, a round avatar made from the pPrince logo, and a link to the freeCodeCamp Responsive Web Design certificate. Personal email and phone number are deliberately NOT shown on the public page. Edit the ABOUT_AUTHOR object in index.html to change any of it; leave bio empty to hide the section again. Inside the app, links open in the phone's browser.
v1.2.3 — Everything since v1.2.2, in one release. (An earlier build of this work was labelled v1.3.0; the correct number is v1.2.3 because only the last number is bumped.)
ABOUT_AUTHOR in index.html (hidden while bio is empty).feedback table above. Terms and Privacy updated to say what is stored./download, a redirect defined in vercel.json, so the GitHub address isn't printed on the page. Also available from Settings → Get the Android app. Not shown inside the app.v1.2.2 — New logo everywhere; version label reset to v1.2.2 (entries below used a higher internal numbering and are kept for history).
favicon.svg, favicon.ico), an iOS home-screen icon (apple-touch-icon.png), and a link-preview image (icon-512.png).#FF6A1A (CSS variable --brand); the interface accent colour is unchanged.v2.2.0 — Desktop layout. Phones are pixel-identical to v2.1.0.
v2.1.0 — Groundwork for the Android app; the website itself behaves the same.
com.creatorbooth.app://login-callback link; email sign-in links use the same return link. On the website nothing changes. Supabase → Authentication → URL Configuration → Redirect URLs must also contain com.creatorbooth.app://**.NATIVE in index.html; the build script in the app project injects the pieces it needs. The placeholder '__GITHUB_REPO__' is replaced at app-build time and is harmless on the website.v2.0.0 — Two decisions actually built this time, not just discussed:
MODES is now a single football entry, the mode-picker dropdown is removed entirely (the pill is now a static badge, nothing left to pick), and the header now carries a permanent "FOOTBALL AI" tag under the Creator Booth name. The football system prompt itself was rewritten to explicitly cover two registers instead of being a narrow video-scriptwriter prompt: an OFFICIAL register (research, analysis, scripts — accurate, respects verified data) and a BANTER register (jokes, hot takes, rivalry roasts — only when actually asked for, grounded in real well-known context, never a fabricated specific incident stated as fact). Added a 5th Home quick-start card, Banter, styled distinctly (dashed border) from the 4 "official" cards to make that duality visible rather than hidden behind a system-prompt detail. A caught-by-testing bug from this pass: the removed mode-switcher was calling a function immediately at page-load that read a let-declared variable before its declaration had run (a temporal dead zone crash) — would have broken the entire app on load; found by actually loading the page in a real browser, not by reading the diff.localStorage — realistically hundreds of MB+ of quota instead of ~5-10MB, available identically on the website and inside the Android app) every time they're fetched from or written to Supabase. refreshChatList() and openChat() try Supabase first; if that fails (offline, or Supabase unreachable), they fall back to the local cache instead of showing nothing or throwing — verified in a real headless-Chrome test where the fake backend was made to genuinely throw when offline, confirming the fallback path (not just lucky mock behavior) returns the correct chat list and message content. Supabase remains the only source of truth; this is purely a read cache, so signing in on a second phone still works exactly as before. Scoped per account on purpose: the entire cache is wiped the moment a different user signs in on the same device (verified: after switching from one test account to another, the first account's cached chats are completely gone, never visible to the second) — directly addresses the "someone logs in on another person's phone" concern, by never trying to filter a shared cache and instead just not sharing it at all. Also wired into chat delete and account delete, so removed chats don't linger in the cache indefinitely. Found and fixed two real gaps while building this: refreshChatList/openChat had no error handling around their Supabase calls at all before this (a network failure there would have thrown uncaught), and createChat now fails with a clear toast instead of an uncaught exception when attempted while offline.v1.9.0 — An audit pass plus three concrete fixes from the offline-experience discussion:
addChips() and its .chips/.chip CSS — leftover from the old numbered-chip feature that was removed from the UI a while back, but the function itself was never deleted; summarizeFixtureList() — an orphaned helper, never wired to anything; api/google-tts.js and api/elevenlabs.js — unused backend routes from before the chat-UI rebuild, already flagged as dead in this README but never actually deleted until now. Not removed, flagged as a decision for you instead: 5 of the 6 chat modes (Story, Secret Web Tools, Hidden AI/Prompting Guides, Conspiracy/History, AI News Roundup) have had zero feature investment for a long time — no quickstart card, no onboarding tie-in, no verified-data badge, nothing — while Football has gotten every enhancement. That's not a bug, but it is a product question worth deciding on purpose: keep maintaining all 6 as general-purpose modes, or lean fully into football and simplify/remove the rest. (The CSS coverage run also flagged ~178 "unused" selectors, but almost all of those are hover/active/modal/error states a quick synthetic script doesn't trigger — not a reliable dead-code signal, so nothing was removed on that basis alone.)online event), with no button to press. Applies to both the AI reply and the message-save step. Found and fixed a related gap while building this: the signed-in (Supabase) save path had no try/catch around its network calls, so a genuine network failure there could throw unhandled and hang silently with no warning and no queued retry at all — guest/local saves were already resilient to this since localStorage doesn't care about network.v1.8.0 — Four real regressions from v1.7.x, all root-caused and fixed — see the chat for how each was actually verified (a headless-Chrome test with a mocked backend, inspecting the real network requests and DOM, not just reading the code):
buildHistoryContents() now reconstructs the earlier turns from the thread itself (works correctly for a fresh send, a regenerate, an edit, or a chat reloaded from history) and includes them in the request, with an explicit instruction telling the model to treat follow-ups like "what of France and Mbappé" as continuations of the topic rather than standalone questions. History is capped (~16 turns / ~14k characters) so a very long chat doesn't blow up the request..appendChild(), which — since the spacer was already the last child — pushed each new message in after it, behind a few hundred pixels of blank space. Fixed by routing every message insertion through one appendToThread() helper that always inserts before the spacer, so the spacer stays a trailing buffer instead of a wall in the middle of the conversation.v1.7.1 — Backend groundwork for the Android app (see the new app/ folder):
/api/gemini, /api/groq, and /api/football/... now goes through an absolute API_ORIGIN (https://creator-boot.vercel.app) constant instead of a same-origin relative path. Behaves identically on the website (still same-origin, no functional change) but is what makes the app work at all — the app bundles this same file locally at a different origin (https://localhost), and a relative /api/... path there would have resolved against the app's own local origin and failed outright.Access-Control-Allow-Origin: * + OPTIONS preflight handling) to api/gemini.js, api/groq.js, and api/football/[...path].js, since the app now calls them cross-origin. No change in behavior for same-origin website calls.v1.7.0 — Decided against a native app for now (see chat discussion — Capacitor wrapper is real future work, not this pass) and built the three flagged ideas instead:
content and media together, so the fix now re-reads the existing images from the DOM before saving an edit).feedback column on messages (SQL above) via a msgFeedbackRef map from DOM element → storage row, set right after each message saves. Tapping an already-active thumb clears it rather than getting stuck on. Reloading a chat restores whichever thumb was previously pressed on the last message.v1.6.0 — A big batch from one review session:
===SUGGESTIONS=== marker, never shown as raw text); tapping one sends it immediately, same as the ChatGPT pattern that prompted this.isGenerating guard rather than leaving that unprotected.v1.5.0 — Built the two lowest-risk pieces of the football-first redesign discussion (see README history / chat for the full reasoning on what was deferred and why):
v1.4.1 — Fixed the onboarding trigger: it only fired for accounts that already had a profiles row with onboarded: false, so every account that existed before v1.4.0 (which has no profiles row at all) was silently skipped and never asked. Now it fires whenever there's no profile row yet, not just when there's an unfinished one — every pre-existing account gets asked exactly once, next time they sign in.
v1.4.0 — Three things:
profiles table (SQL above), synced across devices for signed-in users; guests get the same form saved to local storage instead. The chat greeting now prefers nickname → other names → surname → email prefix, in that order, instead of always using the email prefix. Terms and Privacy rewritten to disclose this data collection plainly (Section 4/2 respectively) — personalization only, never sold, deletable anytime.:streamGenerateContent) is possible but is a real backend change that complicates the Gemini→Groq automatic fallback and the timing of grounding source links — worth doing later if the typewriter effect isn't enough on its own.v1.3.0 — Four things:
teams + coachs endpoints for
its actual current manager, and the result is injected as a
=== VERIFIED CLUB/COACH DATA === block the model is told to treat
as ground truth over its own memory — this is what actually catches
cases like "Maresca manages Man City" (it doesn't; Guardiola does).
Why not a real tool-calling loop: combining Gemini's built-in
google_search grounding with custom function declarations in one
request is a Gemini 3-only preview feature — this app runs on
gemini-2.5-flash, so the model can't be handed a "look up the
coach" tool to call itself. This is a fixed extract → verify →
inject pipeline instead, not the model deciding when to check.
Known limitation: only catches club/coach facts, only for up to
3 clubs per request, and only when API-Football's team-name search
resolves a match — anything else still relies on Gemini's own
search grounding (and the v1.2.2 date/time nudge) as before.v1.2.2 — Every request now gets the visitor's current date/time
injected into the system prompt, plus an explicit instruction to verify
role/roster-type facts (managers, staff, club affiliations) via search
rather than trusting memorized training data. Known limitation:
Gemini's google_search tool has no API setting to force a search on
every request (that existed for older Gemini 1.5 models via
dynamicRetrievalConfig, removed for the tool used here) — this is a
prompt-level nudge that improves the odds, not a guarantee. If stale
facts keep slipping through, the real fix is a deterministic pre-search
step (like gatherFootballContext already does for fixtures) feeding
verified results in as required context — bigger lift, needs its own
search API key, not yet built.
v1.2.1 — Regenerate now snapshots the previous response before clearing the message; if the regeneration attempt fails (both Gemini and Groq down), the old response is restored with a brief "couldn't regenerate" notice instead of being permanently lost behind an error box. Closes the tradeoff noted in v1.2.0.
v1.2.0 — Redesigned message actions to match a "reference layout":
every assistant reply gets copy + thumbs up/down; only the current last
reply also gets regenerate, plus a brand-mark/disclaimer footer row.
Regenerate now actually replaces the message in place (same DOM element,
same DB row updated via saveMessage(..., {replace:true})) instead of
appending a second reply below the old one. Thumbs up/down are local-only
UI feedback for now — no backend collects them yet. Per-message Share was
removed (the header-level "Export chat" feature covers that need).
v1.1.1 — Added a GitHub Actions keep-alive workflow
(.github/workflows/supabase-keepalive.yml) that pings Supabase every
3 days so the free-tier project doesn't auto-pause after 7 days of
inactivity. No app code changed.
v1.1.0 — Export chat as a "Speaker:- text" transcript, either copied to clipboard or downloaded as a PDF (jsPDF, lazy-loaded). Available from a header icon in both normal and incognito chats.
v1.0.0 — Merged the prototype chat UI into the production repo. 6 content modes, Gemini + Google Search grounding, Groq automatic fallback (text + vision), best-effort football fixture context, Google/email auth via Supabase, incognito chats, image attachments (up to 4/message), message actions (copy/regenerate/share), Terms & Privacy pages, visible version tag. Voiceover, project/workspace management, and the old football tool-calling loop were not carried over — see "What this version doesn't do yet" below.
node tests/context.test.mjs.deleteClubSquad; players now returns per-club totals.3-squads-status-coaches.sql once in Supabase -> SQL Editor (after 1-competitions-and-players.sql). It is NOT inside the zip. Paste its contents, not the file name.node tests/squad.test.mjs, node tests/retry.test.mjs, plus the older career and matcher tests.