smooth-app
전 세계 식품의 성분·영양·정보를 자유롭게 협업하고 공개하는 데이터베이스
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A single-file web app for running a team steps challenge — daily step logging, weekly proof photos, teams, a leaderboard, bonus challenges, and an admin console. There's no self-service sign-up: every account (username, password, profile, and team) is provisioned ahead of time by an admin, either from the Admin tab or by running the scripts in /scripts, and admins can promote/demote other users from the Admin tab. Admins can also randomly audit a participant's proof photos with AI and set each week's review status.README 전문 보기 ↗A single-file web app for running a team steps challenge — daily step logging, weekly proof photos, teams, a leaderboard, bonus challenges, and an admin console. There's no self-service sign-up: every account (username, password, profile, and team) is provisioned ahead of time by an admin, either from the Admin tab or by running the scripts in /scripts, and admins can promote/demote other users from the Admin tab. Admins can also randomly audit a participant's proof photos with AI and set each week's review status. The whole thing runs with no backend and no billing account anywhere — see How it's built and Security model for how that works.
The Leaderboard tab defaults to team rankings — top 3 teams on a podium (varying heights), the rest in a compact expandable list — with a toggle to switch the same layout to individual rankings. Inside "My team," members race each other for the current week (Start → Finish), each otter's position set by their own share of that week's top walker.
Live sites:
index.html — the entire app (markup, styles, and JS in one file, no build step, no server).assets/otters/ — the otter mascot artwork. One otter design is the "individual" racer, another the "team" racer; both appear as the header logo, the login screen, and — on the Leaderboard tab — a "🏁 The Race" section where each person/team gets its own lane and their otter's position is driven live by their current step total relative to the group leader. Also rides the current week's dot on the "My trail" progress line.trail-blazers_alice-tan is really the local part of a synthetic email ([email protected]); accounts only ever get created by an admin, either from the Admin tab or by /scripts run locally.config/main — challenge name, start date, number of weeks, optional weekly step limit.users/{uid} — one profile per account, keyed by Firebase Auth uid (name, email — the synthetic one, country, provider: 'password', role: 'admin'|'user', teamId, mustChangePassword, total, byWeek: [n, n, ...]). total/byWeek are a participant's own running totals, kept in sync with their steps doc on every save — see Scaling to hundreds of participants.teams/{teamId} — name, captainUid, members: [uid, ...].bonus/{bonusId} — admin-authored bonus challenges and who/which team has been awarded them. In the Admin tab, each challenge's award list gets a name filter once there are more than 8 people/teams to scroll through.steps/{uid} — each user's daily step counts and hasProof: { [weekIndex]: true } (just a boolean per week, not the photo itself), plus verified: { [weekIndex]: { status: 'verified'|'rejected'|'needs_review', verifiedBy, verifiedByName, verifiedAt } } (a whole-week verdict) and dayReviews: { [date]: { status: 'correct'|'incorrect', verifiedBy, verifiedByName, verifiedAt } } (a per-day right/wrong, from the proof-review modal). No entry for a week/day means it hasn't been reviewed yet. Both are set only by an admin, never the participant themselves.proofs/{uid} — the actual weekly proof photos (compressed, stored inline as JPEG data URLs), kept separate from steps/{uid} on purpose — see Scaling to hundreds of participants.firestore.rules — access rules (see Security model below). This is what actually gates who can create accounts/teams, not any server./scripts (deleting a participant — see Security model for why that one specifically can't be done from a webpage).There's no self-service sign-up, and the whole thing runs on a simple two-phase model:
From the Admin tab, prestart only:
ensureUserClient() in index.html), then writes their profile/team doc while you're still signed in as the admin. firestore.rules' isAdmin() check is what actually stops a non-admin from doing this, not anything about where the code runs.Team Name, Captain, Member 2, Member 3, Member 4, Member 5 (see scripts/roster.example.csv). Every submission fully replaces the roster: since prestart means there's no real data yet, re-keying wipes every existing participant's profile, step data, and team, and regenerates fresh accounts (new usernames, new passwords) from exactly what's in the list — nothing is merged or carried over between keyings. Gives you a downloadable CSV of every username/password afterward.firestore.rules — entirely) while the users collection is still intact: a "⚠ Recovery available" panel appears automatically, showing how many participants have a teamId pointing at a team that no longer exists, with a Rebuild teams from profiles button. It reconstructs each team from the surviving profiles grouped by teamId — but the team's exact name and who was captain were only ever stored on the deleted team doc, so both are lost: the name is guessed from the team ID and the captain is picked arbitrarily. Check and fix both afterward. Never overwrites a team that already has a doc, so it's safe to click more than once.Once the start date arrives, all of the above disappears from the UI and refuses if called directly — the Participants table's Team column becomes plain text, and "My team" no longer offers Join/Leave/Remove. The Participants table itself (and everything else in the Admin tab) is only ever visible to accounts with role: admin in the first place — the tab doesn't even render for anyone else.
There's no "delete a participant" button, prestart or otherwise — Firebase deliberately has no client-side way to delete another user's Auth account (only the Admin SDK can, or a user deleting themselves), so that can't run from a webpage no matter how it's hosted. In practice you shouldn't need it: fix the master list and re-key instead, which wipes and regenerates everyone anyway. A re-keyed or cleared person's old Auth login is left dangling in Firebase (harmless — it has no profile, so handleAuthChange() treats it as "not registered" and refuses to load the app for it) rather than actually deleted. If you genuinely need to delete a login outright, scripts/delete-participant.js is still there for that.
From your own machine (the one thing that has to start here) — creating your first admin account is a chicken-and-egg problem (the in-app tools above need you to already be signed in as an admin), so that one step still runs locally:
cd scripts
npm install
npm run setup-admin # creates username admin_emily, role admin
Full details, including the CLI equivalents of add/import (useful as a
fallback, or for scripting) and delete-participant.js (for the rare case
you need to actually remove a login, not just unassign it), are in
scripts/README.md. Every created account's initial password is the
same string as its username; the app shows a reminder to change it, but
doesn't require it before signing in. setup-admin is also how you add more admins later — "the admin
group" is just everyone it's been run for, not a separate Firestore concept.
A few things only you can do from the Firebase dashboard — none of them need a billing account or credit card:
firestore.rules (they are not live until you deploy them):
npm install -g firebase-tools
firebase login
firebase deploy --only firestore:rules
Run this from inside this project folder. Do this again any time firestore.rules changes.Until steps 1 and 3 are done, sign-in will fail (step 1) or every read/write will be rejected (step 3) even for correctly-provisioned accounts.
This is a static site — no server, no build output, nothing to compile — so Render needs almost nothing from you.
Option A — connect the repo in the Render dashboard:
oh-apac-steps-challenge GitHub repo.echo "no build").. (repo root — that's where index.html lives).https://oh-apac-steps-challenge.onrender.com.Option B — Blueprint (Infrastructure as Code): this repo already includes render.yaml. In the Render dashboard, New → Blueprint → select this repo, and Render reads render.yaml and creates the static site automatically with the settings above pre-filled.
None are required. This surprises people coming from apps with a backend, but it's correct here:
apiKey, authDomain, projectId, etc.) hardcoded in index.html is not a secret — Firebase's own docs are explicit about this. It's a public client identifier, safe to ship in source, the same way it's safe to view in any browser's dev tools on any Firebase web app. Actual access control lives in firestore.rules (who can read/write what) — not in hiding this config.process.env for a runtime environment variable to land in anyway. Render's env vars on a Static Site only affect the build step, and this site has no build step.Nothing Render-specific is needed for sign-in itself: Firebase's "authorized domains" restriction only applies to OAuth-style flows (Google, redirect/popup sign-in), not plain Email/Password sign-in, so there's no domain allowlist to maintain as you add GitHub Pages, Render, and any future custom domain.
There used to be a second Render service (audit-proxy/) holding a Gemini key and Firebase Admin SDK access, for an AI photo-audit feature. Both the proxy and the AI audit feature itself are gone — see Provisioning accounts for how participant/admin accounts work now with no server at all.
In the Admin tab, "🎲 Randomly select someone to audit" expands a participant's row inline. Clicking a proof photo there opens it next to that week's day-by-day steps, with a ✓ Right / ✗ Wrong toggle per day, so an admin can check the photo against each claimed day. Separately, an admin sets that week's overall status to Verified, Needs further verification, or Rejected (or Clear to reset it back to pending) — the resulting colored pill (Excel's classic Good/Bad/Neutral cell colors, plus grey for pending) shows up next to that week everywhere it's displayed: the participant table, the detail row, and on the participant's own "My trail" page once that week is locked.
The Admin tab also has a Proof photos panel listing every uploaded proof photo across all participants and weeks, with an Export all proof (ZIP) button that bundles them all into one download (name_weekN.jpg).
No build step — just open index.html in a browser, or serve the folder locally (e.g. npx serve .). Firestore reads/writes and sign-in all work against the same live Firebase project, so treat local runs as touching real data — sign in with an account created via /scripts.
Firestore's free (Spark) tier — the only tier this app uses, by design — caps out at 50,000 document reads/day and a modest daily network-egress allowance. Two things in an earlier version of this app would have blown through both at real event scale (hundreds of participants), and both are fixed now:
steps/{uid}. Every visit to the Leaderboard or My Team tab bulk-fetched the entire steps collection — everyone's daily counts and everyone's compressed JPEGs — just to add up totals. At a few hundred participants and a few weeks of photos each, that's tens to hundreds of megabytes moving on a single person's visit to a leaderboard. Fixed by splitting proof photos into their own proofs/{uid} collection, fetched only one document at a time (a specific participant's detail view, their own trail) or in bulk deliberately (the Admin tab's proof gallery and ZIP export are both behind a "Load proof photos" click, never automatic). steps/{uid} keeps a hasProof: { [weekIndex]: true } boolean instead of the photo itself, which is all the Admin table's proof checkmark ever needed.steps collection just to sum totals — hundreds of document reads on every visit, before counting the (now-removed) photo bandwidth. Fixed by having each participant maintain their own running total/byWeek fields directly on their users/{uid} doc, written in the same batch as their steps/{uid} save (see saveMySteps()/computeTotals() in index.html). Leaderboard and My Team now read totals straight off the users collection, which the app already fetches at boot for names/teams/roles regardless — no separate bulk read needed at all for either tab.What's left: the Admin tab's Participants table still bulk-fetches the (now much smaller, photo-free) steps collection for its per-week breakdown and review workflow. That's intentionally left as-is — it's bounded by how many admins exist and how often they check it, not by the participant count, so it doesn't scale with event size the way the leaderboard did.
One tradeoff worth knowing: like days before it, total/byWeek are self-reported and self-writable (Firestore rules only stop a participant from touching someone else's total, not from writing an inflated one on their own). This isn't a new risk — a determined participant could already fake their days entries directly via the console before this change — just noting it applies equally to the new fields.
This is an internal team-activity tool, not a system handling sensitive personal or financial data, and there's a hard constraint on top of that: no billing account anywhere, which rules out Cloud Functions (needs Blaze) and effectively rules out an always-on paid server too. So the rules lean on what Firebase's client SDK and Firestore rules can enforce on their own, with two narrow exceptions carved out for the things that genuinely can't be done from a browser:
/scripts) but is gated by firestore.rules' isAdmin() check on users/{uid} and teams/{teamId} — only a caller whose own Firestore profile already has role: 'admin' can write a new one. The Auth account itself is created via a second, throwaway Firebase app instance in the page (see provisioningAuth / ensureUserClient() in index.html) so creating someone else's login never disturbs the admin's own signed-in session.handleAuthChange() checks for a matching Firestore profile on every sign-in, and treats "signed in but no profile" as "not registered," refusing to load the app. Since users/{uid} creation requires isAdmin(), a rogue self-registered Auth account can never get itself a profile, and so can never get past that screen./scripts. Deleting the Firestore profile and step data is a different matter and is allowed client-side for admins (allow delete: if isAdmin() on users/{uid}, steps/{uid}, and proofs/{uid}) — that's what powers "Key in the team roster" and "Clear all participants" wiping and regenerating cleanly prestart. A wiped person's dangling Auth login is harmless: with no profile, handleAuthChange() treats them as unregistered.proofs/{uid} follows the same shape as steps/{uid}: the owner can create/update their own, an admin can too (needed for the review workflow), and only an admin can delete (the prestart wipe path). Splitting photos into their own collection (see Scaling to hundreds of participants) didn't change who's allowed to touch them, only how the data's laid out.isPrestart()index.htmlfirestore.rulesfirestore.rulesfirestore.rulesfirestore.rules, not just the UI. Admins can otherwise update any field on any users/{uid} doc (needed so client-side provisioning can reassign teamId), but a non-admin can only ever touch their own profile, and never their own role.days, weekProof) can be written by their own owner or an admin (admin access is used by the "reset challenge" feature). The verified (whole-week) and dayReviews (per-day) audit stamps are the exceptions carved out of "owner can write their own doc": only an admin can ever set either, enforced in firestore.rules — a participant can't self-verify even by calling Firestore directly, bypassing the UI entirely.apiKey, authDomain, projectId, etc.) hardcoded in index.html was never a secret in the first place — Firebase's own docs are explicit about this, it's a public client identifier by design. Actual access control lives entirely in firestore.rules and Auth settings, both of which are designed to be safe with a public API key.