MyGo 云音乐
用 Go 与 MyGo 写成的第三方网易云音乐桌面客户端。原生界面,没有 WebView;边下边播,无损与 Hi-Res,逐字歌词与「律动」,刘海播放器。
Translation pending
Rate each performance live, one at a time.
Translation pending
MediaTypeScript · PLpgSQLGitHub @ZeeshanMakhtoorOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
Audience rates one performer at a time from their phone. An admin drives the event
from a separate subdomain and watches rankings build live.
A club event has a running order of 40–50 performers. Each performs in turn; while
they're on stage the admin opens voting, and everyone in the room rates them 1–5 on
their own phone. No login, no app install, no account.Read the full README ↗Audience rates one performer at a time from their phone. An admin drives the event from a separate subdomain and watches rankings build live.
A club event has a running order of 40–50 performers. Each performs in turn; while they're on stage the admin opens voting, and everyone in the room rates them 1–5 on their own phone. No login, no app install, no account.
yourdomain.com — the audience app. Shows whoever is performing right now and
a 1–5 scale. One rating per person, per performer.admin.yourdomain.com — the admin app, behind an email/password login. Build
the running order, advance performers, open and close voting, watch live stats,
finish the event and export results.Results are never shown to the audience — not while voting is open, not once the event finishes. Standings are visible only on the admin dashboard, with a CSV export. When the event finishes, voters see a closing message and nothing more.
Next.js 14 (App Router) on Vercel + Supabase (Postgres, Auth, Realtime). One
deployment serves both apps; middleware routes by Host header.
The database is the authority. Every rule that matters is enforced in Postgres, not in the browser and not in a server action. The audience client talks to Supabase directly, so anything enforced only in JavaScript would be enforced by nobody.
| Concern | Where it lives |
|---|---|
| Casting a vote | cast_vote() — re-reads event and participant state under a row lock, then inserts |
| One vote per person per performer | Unique index on (event_id, participant_id, voter_id) |
| Event control | admin_*() functions, callable only by an allowlisted admin |
| Ranking | get_leaderboard() — computed in SQL; raw votes never reach a browser |
| Who may see what | Row Level Security on every table |
Realtime is a synchronisation signal, not a source of truth. Nothing that changed while a client was disconnected is replayed, so both apps re-fetch state on every (re)connect and show a "Reconnecting" indicator while they're out of touch.
Audience phone ─┐
├─→ Vercel (SSR, dynamic) ─→ Supabase Postgres ← RLS + RPCs
Admin laptop ──┘ ↑ │
└──── Realtime ──────────┘ (refetch trigger only)
Anonymous voters are identified by a random ID kept in both localStorage and a
cookie, because browsers evict the two independently. Whichever survives restores the
other. On load the app also asks the database (has_voted) whether this voter already
voted for the performer on stage, so the "already voted" screen does not depend on
local storage surviving.
That ID is a convenience, never a trust boundary — the unique index is what actually stops double voting. A voter who clears both stores, or opens the site in a different browser, is a new person as far as anything can tell. That is inherent to anonymous voting; see Deliberate limits.
Requires Node 20+.
git clone <your-repo-url>
cd live-voting
npm install
cp .env.example .env.local # fill in the two NEXT_PUBLIC_ values
anon public key. Those are
the two values .env.local needs.events, participants and votes to the
supabase_realtime publication. Confirm under Database → Replication.events — anon may read only live and finished events; admins may do anything.participants — anon may read participants of a live event; admins may do anything.votes — no anon policy at all, and admins get SELECT only. Votes are
append-only, insertable solely through cast_vote().participant_scores — no anon grant, and security_invoker is on, so it
cannot be used to read standings.admin_users — RLS on with no policies, so it is unreachable through the API.| Variable | Required | Where | Notes |
|---|---|---|---|
NEXT_PUBLIC_SUPABASE_URL | yes | local + Vercel | Supabase → Settings → API |
NEXT_PUBLIC_SUPABASE_ANON_KEY | yes | local + Vercel | Supabase → Settings → API. Public by design; RLS is what protects the data. |
NEXT_PUBLIC_ADMIN_HOST | yes in production | local + Vercel | Hostname only — admin.yourdomain.com. No protocol, no trailing slash, no path. |
SUPABASE_SERVICE_ROLE_KEY | no | never on Vercel | Bypasses RLS entirely. Nothing in the request path uses it. Only for one-off local admin scripts. |
There are no other environment variables, and no hardcoded URLs anywhere — the admin host is read from configuration in every place it matters.
Apply everything in supabase/migrations/ in filename order. Either paste each
file into the Supabase SQL editor, or use the CLI:
supabase link --project-ref <your-project-ref>
supabase db push
| Migration | What it does |
|---|---|
0001 | Core schema: events, participants, votes; RLS; indexes |
0002 | Event lifecycle, voting states, scoring view, get_leaderboard |
0003 | Admin event-control functions |
0004 | Revokes the execute grants Supabase auto-adds to anon |
0005 | Lets the audience see that an event has finished (superseded by 0015) |
0006–0007 | Realtime publication; reorder / skip / remove controls |
0008 | Closed voting cannot be reopened |
0009 | Removed participants excluded from rankings |
0010 | Input validation as CHECK constraints |
0011 | Consistent lock ordering between voting and admin control |
0012 | Admin allowlist (admin_users + is_admin()) |
0013 | Tightens function grants on the anon-callable surface |
0014 | admin_delete_event, which refuses to delete a live event |
0015 | Removes the public results function |
0016 | has_voted, so the audience app asks the database rather than trusting local storage |
0017 | Year becomes a year of study picked from a list, not a typed graduation year |
To verify a fresh database, run the business-logic suite (46 assertions; it creates a live event, so use a scratch database, not production):
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f supabase/tests/business-logic.test.sql
Two steps — both are required. A signed-in account that isn't on the allowlist is turned away at the dashboard.
insert into public.admin_users (user_id, email)
select id, email from auth.users where email = '[email protected]';
To check who currently has admin rights: select email from public.admin_users;
npm run dev # http://localhost:3000
http://localhost:3000http://localhost:3000/adminTo exercise the real subdomain split locally, add admin.localhost to your hosts file
and set NEXT_PUBLIC_ADMIN_HOST=admin.localhost:3000. Otherwise middleware falls back
to path-based routing at /admin, which is fine for development.
Other commands:
npm test # unit tests: validation, error mapping, voter identity
npm run typecheck
npm run lint
npm run build
NEXT_PUBLIC_* variables from section 5 to Production (and
Preview, if you use preview deployments).SUPABASE_SERVICE_ROLE_KEY.Every route that reflects live event state is force-dynamic, so no page of it is
ever served from a static or edge cache. That is deliberate: a cached page would show
a stale performer to the room.
vercel.json pins Serverless Functions to bom1 (Mumbai) to sit next to the
Supabase project in ap-south-1. This matters more than it looks: rendering the
audience page makes two sequential Supabase queries, so a mismatched region pays
that round trip twice per page load. The project defaulted to iad1 (Washington
DC), which meant every load crossed Virginia↔Mumbai and back, twice, for queries
that execute in under 5ms.
If you move the Supabase project, change this to match it. Co-locating the function with the database beats co-locating it with the audience, because the database round trips are multiplied while the user's hop to the function is paid once. Confirm under Vercel → Settings → Functions after deploying; on a plan that restricts region selection, set it there instead and remove the field here.
In Vercel → Project → Settings → Domains, add yourdomain.com (and www if you
want it, redirecting to the apex). Follow Vercel's DNS instructions at your registrar
and wait for the certificate to issue.
admin.yourdomain.com as a second domain on the same Vercel project — not a
separate project. One deployment serves both.CNAME to cname.vercel-dns.com).NEXT_PUBLIC_ADMIN_HOST=admin.yourdomain.com in Vercel and redeploy.Middleware reads the Host header: on the admin host, /login and /dashboard are
served from the admin app; on the public host, the audience app is served and the admin
app remains reachable at /admin.
If NEXT_PUBLIC_ADMIN_HOST does not exactly match the real subdomain, the admin app
silently falls back to yourdomain.com/admin. It still works and is still protected —
but it isn't on the subdomain you intended, so check this after deploying.
See OPERATIONS.md for the event-day runbook.
The loop, in one line: Start performer → Open voting → watch the count → Close voting → next performer. Finish the event at the end to close voting for good.
| Symptom | Cause and fix |
|---|---|
| "That account isn't an event administrator" | The user exists in Auth but not in admin_users. Run the insert in section 7. |
Admin app loads at /admin instead of the subdomain | NEXT_PUBLIC_ADMIN_HOST doesn't match the real host, or wasn't redeployed after being set. |
| Audience stuck on "Voting hasn't started yet" | No event is live. Start a performer from the dashboard. |
| "Reconnecting" persists on a phone | That phone lost its Realtime connection. The app re-fetches on reconnect and the audience can still vote. Usually venue WiFi. |
| A voter says Submit does nothing | They've already rated this performer — the screen will say so. Ratings are one per performer, per device. |
| "Cannot start event" / duplicate live event | Another event is still live. Only one may be live at a time; finish the old one first. |
| Removing a participant is disabled | They already have ratings. Skip them instead — removing would discard real votes. |
| Voting won't reopen after closing | Closing is deliberately final for that performer. Advance to the next one. |
| Dashboard shows an error banner | The action failed and did not apply. The message says which one. Retry; the event is unaffected. |
| Vote counts look frozen for a second | Vote-driven refreshes are coalesced into roughly one per second so a burst of 100 ratings doesn't hammer the database. This is normal. |
-- What is live right now?
select name, status, voting_state, active_participant_id from public.events;
-- Ratings for the current performer
select count(*), round(avg(rating),2) from public.votes
where participant_id = (select active_participant_id from public.events where status='live');
-- Final results, straight from the votes
select * from public.get_leaderboard('<event-id>');
node scripts/qr/make-qr.mjs https://your-address
Writes qr-out/vote-qr.svg and qr-out/vote-poster.html — open the poster and print
it, or screenshot it for a slide. Error correction is level H, so it still scans when
projected, photographed at an angle, or partly obscured.
Regenerate it if the address ever changes, and circulate exactly one address. A
voter's identity is stored per domain, so someone who arrives on a different hostname
counts as a new person and is offered a vote they already cast. A Vercel project has
several .vercel.app hostnames by default; pick one and use it everywhere.
Verified in Chromium. Firefox and WebKit could not be run in the environment this was built in, so their behaviour is reasoned from feature support rather than observed — worth a two-minute check on a real iPhone and a real Android phone before the event.
The floor is roughly iOS Safari 15.4+, Chrome 93+, Firefox 92+ — anything from early 2022 onward. iOS 15.4 runs on the iPhone 6s and newer, so in practice every phone in the room qualifies.
Deliberate choices that keep that floor low:
format("woff2"), not the non-standard
format("woff2-variations"). Browsers that don't recognise a format string
skip that source entirely, which would drop the whole page to fallback
fonts.:has(), which Firefox only shipped in December 2023. The radio inputs are
visually hidden, so on a browser without :has() a keyboard voter would
have had no focus indicator at all.100dvh is paired with a 100vh fallback. An unsupported declaration is
dropped, and without the fallback those browsers get no min-height.getVoterId() degrades through crypto.randomUUID → getRandomValues →
Math.random, and survives localStorage being blocked entirely.Below that floor the app degrades rather than breaking: focus rings stop
showing (:focus-visible), but voting still works.
Ballot stuffing is not fully preventable while voting is anonymous. The defences are voter ID + database uniqueness + event/participant validation. Someone who clears site data repeatedly, or scripts against the public anon key, can add votes under fresh IDs.
IP-based rate limiting is not the answer here and is not implemented: a venue full of people shares one NAT'd IP, so any threshold low enough to matter would block the real audience. CAPTCHA was rejected for the same reason — it taxes every honest voter to slow an attacker down.
What bounds the risk in practice is that voting is open only for a minute or two per performer, and the admin can see live vote counts. A performer whose count is wildly out of line with the rest is visible while it's happening.