Skip to content

Repository files navigation

Huskyteers Portal — FTC 19516

The team communication app for FTC 19516 The Huskyteers. Leaders assign tasks, members get a daily checklist, a leader has to approve a checked item before it officially counts, members ask leaders questions, and everyone gets email updates they can switch off.

What each person can do

Who Can do
Member (Software / Build / Business) See Today's checklist (overdue, due today, coming up) · check items off → waiting for review → approved (or needs changes with the leader's note) · when a leader marks a whole task finished, it counts as done for you if they counted it for everyone; otherwise it comes off your lists as closed (no credit, listed under Done) · the Task dashboard (every subteam's tasks, sorted by category, and who's on each one; finished tasks are under Past) · sign up for tasks that are open for sign-up · ask questions to their subteam leaders, the captain, or a specific person · share resources (links or files up to 4 MB, e.g. code) — they appear once someone with admin access approves them · email preferences
Everyone Team members: everyone on the team with their position, email (tap to write to them), and when they were last on · search by name, email, or position ("build", "mentor"), filter by position or subteam. Only active accounts are listed (not sign-ups waiting for approval or people who left). People with admin access can jump from a name to managing that person in Admin → People.
Everyone My photo (Settings): upload a photo of yourself (PNG/JPG/WebP — iPhone HEIC photos get a hint to save a JPG; your phone crops it to a square you can drag and zoom, and sends a small 512 × 512 JPG; hidden details such as GPS are removed; the server takes still pictures up to 1,024 pixels a side and 1 MB), replace it, or remove it. It's shown on your name around the portal — the menu, Team members, tasks, questions, discussions, announcements, orders, parts, fundraisers — unless you turn off Show my photo in the portal (then the team sees your initials; people who manage your account still see it in Admin), and it goes on the public team website automatically — next to your name wherever the website lists you (your Our Team card, the Progress page) — unless you turn off Show my photo on the team website (public) (it comes off within a few minutes). The card says where your photo shows. People who manage your account can remove it or keep it off the website (Admin → People → a person → Photo; after a removal a new photo waits for them to allow it), and changing your own name keeps your photo off the website until they check the new name (the website finds photos by name; changing it back lifts that). Admin → Approvals → Members' photos lists photos held off the website and every photo that went up in the last week.
Everyone Attendance: answer Coming? for the next team days — regular meetings on Monday, Tuesday, and Thursday, plus any extra meeting an admin adds on the Team calendar (Regular: Yes / Half / No; on Flex days, Tuesday and Thursday, also Attend Flex? Yes / No; plus a short note) and see each day's plan · at practice, Scan QR code right in the portal (or with the phone's camera) or Enter code · your attendance this two weeks and this season. Teachers, the Business leader, the captain, mentors, and owners start attendance: a QR code that changes every 30 seconds for the projector (Full screen), a 6-digit code, the live list, "What we did" (older than a week: teachers, mentors, and owners), delete one started by mistake, and the schedule (which shows the regular days and the upcoming extra meetings). Leaders, the captain, mentors, and teachers see Who's coming each day (write the day's plan, assign a task to the people coming, Add an extra meeting) and two-week reports (sessions, each person's attendance, what got done; print or CSV).
Everyone Attendance credits: the goal is 2 a week (Monday to Sunday; the Schedule page can change it) · checking yourself in always takes the scan (QR code, typed code, or the in-app scanner): at regular practice it counts as 1, at Flex pick 1/2 Flex or 1 Flex (you can switch that day) · see "1⅔ of 2 this week" (and last week) with what it's made of ("1 regular · ½ Flex · ½ EC") on Attendance and Today. EC and Flex count toward the 2. Everyone with admin access (leaders, the captain, mentors, teachers, owners) opens any session and fixes anyone's attendance any time: add people by hand with 1/3, 1/2, 2/3, EC, or No credit (never 1 or Flex — those need the scan), take them off, or change a credit (a scan can go back to 1 or down to part / No credit) — never their own (another admin does). Someone added by hand who scans in while it's open gets a normal check-in. EC only by the Business leader, teachers, mentors, and owners. Said they'd come — not checked in: people who answered Yes / Half (Flex: Yes) and aren't on the list show on the live screen (folded under the QR code while it's open) and on admins' Today with quick actions (part credit or No credit, with Undo); closing attendance posts their names to the leaders' Discord channel (never the person who ran it). Attendance changes (admins only): every change admins made, as sentences, with filters, CSV, each session's History, and "Changes by admins" in the two-week report. No credit counts as not coming.
Everyone The Parts queue: add a part that needs to be 3D printed, laser cut, CNC'd, cut, or machined; tap I'll make it to claim one and Mark done when it's finished (Build leaders set the priority order).
Everyone Fundraising: the team's fundraisers (car washes, bake sales, donation pages…) with dates, place, goal, and money raised so far, plus a big "Raised $X of $Y" bar for the season · tap I'll help to sign up (some have a set number of spots) and see who else is helping. The Business subteam + staff add and edit fundraisers, update the amount raised, and set the season goal and when the season started (only this season's fundraisers count).
Everyone Posters: the team's posters and flyers (a picture or PDF up to 4 MB — bigger phone photos are made smaller automatically — a Canva/Drive link, or both), with what they're for and the event date · Upcoming and Past tabs, "Added by me" · add one straight from your phone (no approval). Whoever added a poster can edit or remove it; so can everyone with admin access.
Everyone Team calendar: the FTC 2026–27 season — league scrimmages and meets, the ILT, competitions, extra meetings (a meeting on top of Mon/Tue/Thu: that day shows up in Attendance's Coming? and check-in), outreach, deadlines — in a month view (color per kind), a list of what's coming, and past events · the next meet countdown on top of the calendar and on Today ("Next meet in 12 days") · Add to my calendar (.ics for Google/Apple Calendar) for everything or one event. Leaders, the captain, mentors, and teachers add, edit, and remove events. Meets, the ILT, scrimmages, competitions, and outreach also go on the team website's Events page (marked "On the team website"; tick Hide from the team website to keep one off) — never team meetings, team events, deadlines, or notes.
Everyone Announcements: news from leaders, the captain, mentors, and teachers — for the whole team or one subteam, pinned ones on top · everyone reads all of them; For you shows the whole team's plus your subteam's, and the rest are a tap away (Whole team / Software / Build / Business) · what's new for you since your last visit is marked New, counted on the menu, and listed on Today until you open Announcements. Leaders post for the whole team or their own subteam; the captain, mentors, teachers, and owners for anyone. Whoever posted one (while they can still post; and the captain, mentors, and teachers) can edit (shows "edited"), pin, or remove it. No emails.
Everyone Discussion: the team-wide forum (Questions stay private, between a member and their leaders) · start a discussion with a title, text, and a category (General, Software, Build, Business, Ideas, Off-topic) and everyone can reply · the list shows pinned ones first, then the latest activity ("last reply 2 h ago by Ethan"), with category chips, Unread, Mine, and a title search · what's new since you last opened a discussion is marked (New / "3 new"), counted on the menu, and a New line (with a "Jump to new replies" link) shows where the new replies start. Whoever wrote a post can edit it (shows "edited") or delete it (a discussion only until someone else replies); leaders, the captain, mentors, and teachers pin, lock (no more replies, edits, or deletes by authors), and delete anything. No emails.
Everyone Team notebook: the engineering notebooks, laid out like the team's Google Doc (Hardware → Intake, Outtake; Software → Competition, Prototyping, Transfer; Business → Outreach, Sponsorships, Engineering Journal) · open a page and tap New entry: your name and the date and time are stamped automatically · text with bullet lists and links, plus pictures (photos are made smaller for you) · every change (pictures too) shows Edited by and a History of who changed what; an old version's text can be taken out (e.g. a phone number pasted by mistake) · Export a notebook or a page to Word (.docx) or Print / Save as PDF. Whoever wrote an entry can change it; so can that notebook's subteam leaders (except entries by the captain, mentors, teachers, or owners), who also manage its pages. The captain, mentors, and teachers add and remove notebooks (or create the standard ones in one click).
Everyone Strategy: the team's Pedro Pathing planner (BIOBUZZ visualizer, served by the portal itself at /planner/) right on the page — wide on a big screen, folded away on phones and tablets ("Show it here", Full screen), always one tap from Open in a new tab · Save to team library in the planner keeps a path in the portal for everyone · Our strategies below it: autos, TeleOp/endgame plans, match plans, scouting notes, with a description, a link, and files (.pp paths from the planner's Save As, pictures, PDFs; up to 10, 4 MB each) · filter by kind or "Mine" · Download path, then Load it in the planner. Whoever added a strategy can edit or remove it; so can everyone with admin access, who can also pin it to the top.
Everyone Strategy → Other teams: the other 17 teams in our league, by number · search by number, name, or city · open one to write their strategy at a glance (anyone can edit it; if two people save at once, the second sees the first's version instead of overwriting it), keep strategies about them (same files: .pp paths, pictures, PDFs), and Trace their path in the planner, then save it to the team library for them (listed on their page) or attach the .pp. Leaders, the captain, mentors, and teachers add, edit, and remove teams (removing one removes the strategies and saved paths about it).
Everyone Strategy → Paths: every path saved from the planner (ours, and other teams' autos we traced), newest first · filter by whose it is, search names and notes · Open in planner (a new tab, straight to that path), Download .pp. Whoever saved a path can rename it, change its notes or whose it is, and remove it; so can everyone with admin access. If two people save the same path at once, the second is told who changed it instead of overwriting it. Paths saved in the browser on the old GitHub Pages copy of the planner stay there: open that copy, Save As to download the .pp, then load it in the portal's planner and use Save to team library.
Everyone Order list: what the team needs to buy, like the old "Purchase List" sheet — orderer, status (Requested → Approved → Ordered → Done, or N/A), description linked to the product, notes (SKU…), quantity, cost, and who it's for · Open / Done / N/A / All tabs, subteam chips, "Mine", and the estimated total of open items · Export to spreadsheet (CSV). Anyone adds items and changes their own until they're approved; the person ordering an approved item (or anyone on the Business subteam) marks it Ordered, then Done. Everyone with admin access approves items (never their own requests, unless captain/mentor/teacher/owner), changes any status, and edits or removes anything; Today tells the Business leader and the captain what's waiting for approval.
Everyone Workshop: what the team designs and builds — 3D prints & CAD, software, other builds · add a project with what it does, the material, and who designed it · add photos of each version (Version 1 → Version 3, with what failed and what changed) and design files to download (STL, STEP, 3MF…, or a CAD link) · send it for approval; once a leader approves it, it's on the team website's Workshop page. Leaders, the captain, mentors, and teachers approve or send back projects, publish projects straight to the website (their own, or anyone's draft), and feature the best ones.
Business subteam + staff Sponsorships: every sponsor ask with its stage (Haven't started → Do soon → Needs work → Submitted → Accepted/Declined → Finished), what we're asking for, money received, who's handling it, and submitted/accepted/closing dates · attached files per sponsor (e.g. a paper application form to print, or the filled-out copy; up to 10, 4 MB each) · import rows pasted from the old spreadsheet · a shared "Info for applications" note (never put passwords there) · Team website: accepted and finished sponsors are thanked on the team website's Sponsors page with their name (or a "Name on the website"), brand logo (PNG/JPG/WebP, uploaded on the sponsor's page; its hidden photo details such as GPS are removed), their website's homepage, and years — turn "Thank them on the team website" off for donors who'd rather not be named. Sponsors still missing a logo show Logo needed (a filter on the list, and a nudge on Today for the Business leader and the person on it).
Subteam leader (Software, Build ×3, Business) Everything a member can do, plus for their subteam: task categories (e.g. Build → Intake, Drivetrain, CAD — add one with +) · create/edit tasks and either assign people or let people sign up (optionally with a number of spots) · Mark task finished when the job is done even if not everyone checked it off: count it as done for everyone still on it (their items are approved — never your own; if yours is checked off, have it approved first) or just close it (no credit); Reopen task undoes it, and Take back credit un-counts one person · review queue (approve / send back with a note / bulk approve) · team progress · answer questions · plus admin access (below)
Business leader (also) Whole-team tasks for fundraising and outreach: create them and put anyone on them (any subteam), then edit, review, finish, or delete the ones they created (questions about them come to them, not the captain). Their Business tasks stay Business-only (a Business task someone else created can't be made whole-team by them). They pick from the whole-team categories but don't add to them.
Captain, Mentor, Teacher Same as leaders, for every subteam, plus whole-team tasks (and their categories), plus admin access
Admin access (every leader, the captain, mentors, and teachers) Approve or reject sign-ups (seat limits: 1 captain, 1 software, 3 build, 1 business leader) · approve members' resources · manage people (change position, disable, email a password reset, sign out devices). Guardrails: leaders manage members and approve sign-ups up to leader level; the captain, mentors, and teachers also manage leaders. Nobody but an owner can change an owner's account, someone else's email, or see the email log.
Guests (no account) The team website's Progress page (huskyteers19516.github.io/progress) shows the portal's live progress: the team's and each subteam's checklist items done, waiting for review, and open, each student's (name, position, items done/open — never emails, notes, due dates, or questions; mentors and teachers aren't listed), tasks finished, a "Recent completions" feed, and up to the last 8 weeks of the season. It reads GET /api/public/progress (no sign-in, cached 15 s). The website's Our Team page opens one student's card with GET /api/public/progress/people/<id> (the id from that JSON): the same numbers, their weeks, and the last 25 things they finished (task titles only while titles are shown, when each was approved, and whether it was counted with the whole task) — a 404 for anyone the Progress page doesn't list, and for everyone until an admin turns on Show each person's profile on the Our Team page (off by default; the demo seed turns it on). Everyone can leave themselves off the list and the feed in Settings → Team website (their items count in the totals only while at least 2 people in their subteam are hidden, so nobody's numbers can be worked out). People with admin access turn it off and on and choose how names are shown (full, "Ethan C.", initials, or none), whether task titles show, whether profiles show, and where counting starts in Admin → Approvals → Team website. The website's Sponsors page thanks accepted and finished sponsors (name, logo, website homepage, years — never amounts, contacts, or notes; GET /api/public/sponsors) and its Events page lists the Team calendar's meets, ILT, scrimmages, competitions, and outreach from the season's start (GET /api/public/events); each has its own on/off box on the same card, off until someone turns it on (the card says how many sponsors and events would go public — check them first). The Our Team page (and the Progress page's people) also shows members' own photos (Settings → My photo) from GET /api/public/team-photos — only people who keep Show my photo on the team website on (the default), mentors and teachers included, and nobody held off it. The JSON lists each photo under a key made from the person's full name, never the name (the website hashes the names it already shows to match them, so the JSON isn't a list of who's on the team); the photos aren't for search engines (X-Robots-Tag: noindex, noimageindex). Show members' photos on the team website on the same card turns them all off (on by default).
Guests (no account) The team website's Workshop page (huskyteers19516.github.io/workshop) shows every approved Workshop project: photos of each version, what it does, the material, who designed it (names as the Progress settings say), and the design files to download. It reads GET /api/public/workshop (no sign-in, cached a minute); drafts and projects waiting for approval never show.
Owner (you, the site owner) Everything, for everyone: all accounts (including making someone else an owner), the email log + test email/Discord message.

Members' accounts work right after sign-up. Leader, captain, mentor, and teacher accounts wait on a "waiting for approval" page until someone with admin access approves them.

Emails (each person can turn them off in Settings):

  • To leaders: a member asks them a question · a member checks off a task they assigned
  • To members: someone answers their question · their item is approved or sent back · (off by default) they're assigned a new task
  • To people with admin access: someone signs up and needs approval · a member shares a resource (owners + that member's subteam leaders)
  • Always sent: password reset links, "your password was changed", "you're approved"

Put it online (about 20 minutes, free tiers)

The app needs a server and a database, so GitHub Pages can't run it (it only shows this README). Use Vercel (hosting) with its built-in Neon Postgres database. Your code is already on GitHub.

  1. Vercel project. Sign in at vercel.com with GitHub → Add New → Project → import CommunicationPortal. Vercel automatically runs the vercel-build script (database migrations, then the build). The first deploy may fail because there is no database yet. That's expected.

  2. Database. In the project → Storage → Create Database → Neon (Serverless Postgres) → region Washington, D.C. (us-east-1), the same region as Vercel's servers → connect it to the project for all environments. This sets DATABASE_URL and DATABASE_URL_UNPOOLED for you, so there's nothing to copy.

  3. Settings. In the project → Settings → Environment Variables, add:

    Variable Value
    ADMIN_EMAILS your email. The first person to sign up with it (while there is no admin yet) becomes the admin
    TEAM_JOIN_CODE a code you'll tell the team (e.g. go-huskies) so strangers can't sign up. Set this
    TEAM_TIMEZONE America/Los_Angeles
    APP_URL optional: your site URL, e.g. https://communication-portal.vercel.app. Email links use Vercel's production URL if you leave it out
    PUBLIC_PROGRESS_EXTRA_ORIGINS optional, usually left out: other sites (comma-separated, e.g. http://localhost:4321) whose pages may read the public data (progress, sponsors, events, Workshop, members' photos) besides https://huskyteers19516.github.io — only needed while working on the team website on your computer against the live portal
  4. Email (Gmail). Turn on 2-Step Verification for the Gmail account, then create an App Password (Google Account → Security → App passwords). Add SMTP_HOST=smtp.gmail.com, SMTP_PORT=465, SMTP_USER=<the gmail address>, SMTP_PASS=<the 16-character app password>, EMAIL_FROM="Huskyteers Portal <the gmail address>". Without these the app still works, but emails are only recorded in Admin → Email log. (Alternative: Resend. Set RESEND_API_KEY and an EMAIL_FROM on a domain you verified there. Its free tier allows 100 emails/day, which a busy 35-person team can exceed; Gmail allows ~500/day.)

  5. Discord (optional). To have new questions and replies posted to your leaders' Discord channel: make the channel leaders-only (questions are posted in full), then Edit Channel → Integrations → Webhooks → New Webhook → Copy Webhook URL, and add it in Vercel as DISCORD_WEBHOOK_URL. Answers still happen in the portal. Check it with Admin → Email log → Send a test message to Discord.

  6. Deploy. Deployments → ⋯ → Redeploy so the database and settings take effect.

  7. Become the admin right away: open the site → Create an account with the email from ADMIN_EMAILS. You land in the app with the Admin section in the menu. Do this before sharing the link.

  8. In Admin → Email log, press Send me a test email. Then share the link and the team code. Members can start using it immediately; approve leaders, mentors and teachers in Admin → Approvals.

  9. More room for files (optional, free). Uploaded files (Resources files, sponsor forms, posters, notebook pictures, strategy files, sponsor logos, Workshop photos and design files, members' photos) are kept in the Neon database at first, capped at 300 MB because Neon's free plan is 0.5 GB for everything. Cloudflare R2 gives 10 GB free; with it, new files go there and the cap becomes 8 GB (each person's own allowance grows 10×). Admin → Approvals → File storage shows how full it is and where new files go (everyone with admin access sees it; only owners get the buttons below, because setting it up needs the team's Cloudflare and Vercel accounts).

    1. Sign in at dash.cloudflare.com (a free account is fine) → R2 Object Storage in the left menu. If Cloudflare asks, add a payment method to turn R2 on. The free tier (10 GB stored, 1 million uploads and 10 million downloads a month) is far more than the team uses, and the portal stops uploads at 8 GB.
    2. Create bucket → name huskyteers-portal-files → location Automatic → Create bucket. Keep it private: don't turn on public access or a custom domain. The portal sends every file itself, after checking who may see it.
    3. Back on the R2 page → Manage R2 API Tokens (API → Manage API tokens) → Create API token → name it huskyteers-portal → permission Object Read & Write → Specify bucket(s) → only huskyteers-portal-files → TTL Forever → Create API Token.
    4. Copy the Access Key ID and the Secret Access Key (Cloudflare shows the secret only once) and your Account ID (on the R2 page, also the first part of the S3 address https://<ACCOUNT_ID>.r2.cloudflarestorage.com).
    5. Vercel → the project → Settings → Environment Variables → add, for Production: R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, and R2_BUCKET = huskyteers-portal-files. All four are needed; with only some, files stay in the database (the storage card says which one is missing). Leave R2_ENDPOINT out (it's for tests; on the live site anything but a Cloudflare R2 address is refused).
    6. Deployments → ⋯ → Redeploy.
    7. In the portal (as an owner): Admin → Approvals → File storage now says New files go to Cloudflare R2. Press Test connection (it should say Connected), then Move … to R2 to move the files already in the database. Each click works for up to about 30 seconds; click again until it says every file is in Cloudflare R2. The team can keep using the portal meanwhile. Files removed in the portal are deleted from R2 automatically (Clean up appears if some are waiting).

    Once files are in R2, keep the four settings and the bucket: without them those files can't be opened (the storage card warns). Never share the secret key; if it leaks, delete the token in Cloudflare, make a new one, and update R2_ACCESS_KEY_ID/R2_SECRET_ACCESS_KEY in Vercel, then redeploy.

Admins and owners: every leader, the captain, mentors, and teachers automatically have admin access. To add another owner, open Admin → People, pick the person, tick Owner. (ADMIN_EMAILS only creates the first owner.) Locked out of admin? From a computer with this repo and your production database URL as DATABASE_URL in .env, run npm run admin -- --email you@example.com --name "Your Name". To just reset someone's password without changing their role: npm run admin -- --email them@example.com --reset-only. Someone can't get emails / typo'd their email? Admin → People → (person): fix the email there. Forgot password? Same page → Send password reset. The page also shows the one-time link (works once, for 1 hour), so you can send it to them yourself even if email isn't set up.

Logo: the husky mark lives in public/brand/husky-mark.svg (a redrawn version of the team logo). Replace that file with the original artwork to update it everywhere. Colors are defined once at the top of src/app/globals.css.

Run it on your computer

Requires Node.js 22+. No Docker or database install needed. A real PostgreSQL server runs from node_modules.

npm install
cp .env.example .env          # defaults work for local development
npm run db:start              # terminal 1: local Postgres on port 54329 (keep it running)
npx prisma migrate deploy     # terminal 2: create the tables
npm run db:seed               # demo team: 27 people, tasks, questions (password: huskyteers123)
npm run dev                   # http://localhost:3000

Demo sign-ins (password huskyteers123): admin@example.com (captain + admin), marcus.johnson@example.com (build leader), priya.raman@example.com (software leader), maya.patel@example.com (software member). Without an email server configured, emails are printed in the terminal and recorded in Admin → Email log. Reset the demo data with npm run db:seed -- --reset.

Working on the team website's Progress, Our Team, Sponsors, Events, or Workshop page against this local portal? Add PUBLIC_PROGRESS_EXTRA_ORIGINS="http://localhost:4321" (the Astro dev server) to .env, restart npm run dev, and point the website at http://localhost:3000 (its PUBLIC_PORTAL_URL). The data is at localhost:3000/api/public/progress (one person: /api/public/progress/people/<people[].id>), /api/public/sponsors, /api/public/events, /api/public/workshop, and /api/public/team-photos (the demo has three sponsors with logos, one that still needs one, and outreach events; the demo seed turns the Sponsors and Events pages, and the Our Team profiles, on; Maya Patel's and Ethan Chen's demo photos are on the website — Ethan's only there, Amara Okafor's only in the portal).

Tests

npm run typecheck && npm run lint
npm test                                   # 1,281 service/permission/notification tests on real Postgres
npm run build && npm run test:e2e          # 73 browser tests in Chrome (desktop + phone), own database
npm run build && npm run tour              # full-page screenshots of every screen → e2e-artifacts/tour
npm run build && npm start                 # then, in another terminal:
npm run loadtest -- --users 40 --connections 60 --duration 20

Load test on a laptop (production build, 40 accounts signed in at once, 60 concurrent connections): ~470 page loads/second with zero errors, p99 ~220 ms. Leader dashboards: ~260/s, p99 ~300 ms. With a full season of data (~4,750 tasks) every page's database work stays under 15 ms. Real use by 30–40 people is a few requests per second.

How it's built

Next.js 16 (App Router, Server Components, Server Actions) · React 19 · Tailwind CSS 4 · Prisma 7 + PostgreSQL · zod · Nodemailer / Resend · Vitest · Playwright.

  • Architecture, roles and permissions, the checklist state machine, and coding conventions: docs/CONVENTIONS.md.
  • Security: scrypt password hashing, server-side sessions (httpOnly cookie, revocable, 30 days), permission checks in every service (never just hidden buttons), Postgres-backed rate limiting on sign-in/sign-up/reset, one-time reset links (1 hour), all user text escaped in pages and emails.
  • Reliability: emails send after the response, so a slow mail server never slows the app. Every email attempt is logged. Conditional database writes prevent two leaders from double-reviewing the same item.

Releases

Packages

Contributors

Languages