vsa-seasonal-operations
Load when a VSA-website task touches anything calendar-driven — summer-break empty states or seasonalState.ts, House signups/House Reveal content, application windows (ace_application, house_fall/wint
SKILL.md.claude/skills/vsa-seasonal-operations/SKILL.md
GitHubVSA Seasonal Operations — the academic-year clock
This site serves a student org that lives on the UCSD academic calendar (Fall/Winter/Spring quarters + summer break). Content, application windows, House data, and several UI surfaces change state with the calendar. This skill documents (a) the seasonal state machine in code, (b) the yearly content cycle, (c) the freeze windows policy — an owner-confirmed rule written down here for the first time, and (d) the yearly turnover runbook.
When NOT to use this skill:
| If your task is… | Load instead |
|---|---|
| General change gating, protected domains, PR conventions | vsa-change-control |
| Env vars, application_links as a config axis, site settings | vsa-config-and-flags |
| Leaderboard/points mechanics or dual points systems | vsa-architecture-contract (invariants) / vsa-debugging-playbook (broken) |
| RLS on application_links or new views | vsa-supabase-security-reference |
| Past incidents behind these rules | vsa-failure-archaeology |
| Domain-scoped edits (House pages, cabinet, applications UI) | .claude/agents/vsa-house-system.md, vsa-cabinet-leadership.md, vsa-applications-forms.md playbooks |
1. The seasonal state machine: src/utils/seasonalState.ts
The only computed seasonal state in the codebase (109 lines; test: src/utils/seasonalState.test.ts). Everything else seasonal (application windows, current VCN, active academic term) is configured in Supabase by admins.
States (VsaSeason union): active_year | summer_break.
isSummerBreak(date?)— true from June 15 (inclusive) to September 15 (exclusive), computed in theAmerica/Los_Angelestimezone (VSA_TIME_ZONEconst) viaIntl.DateTimeFormat. Sept 15 itself is alreadyactive_year. Boundaries are hardcoded constantsSUMMER_BREAK_START/SUMMER_BREAK_END— a code comment says they "can be updated when the next academic year calendar is finalized."getCurrentVsaSeason(date?)— maps the boolean to the union.shouldUseSummerEmptyState(hasActiveItems, date?)— summer empty states only render when there is no live content (!hasActiveItems && isSummerBreak(date)). Real content always wins over the seasonal message.getSummerBreakMessage(context)— returns{badge, title, body}copy per surface context:homepage,events,house,houseStandings,externals,gallery,points,default.
State/consumer table (verified 2026-07-06):
| State | Date window (LA time) | Surfaces affected | Source of truth |
|---|---|---|---|
summer_break |
Jun 15 – Sep 14 inclusive | Homepage "This Week in VSA" (src/components/features/home/ThisWeekInVSA.tsx — next-event card ~L90, House standings ~L254, latest-memory card ~L397); Events page (src/pages/Events.tsx ~L432); Calendar (src/pages/Calendar.tsx ~L204); Gallery (src/pages/Gallery.tsx ~L79); House page (src/pages/House.tsx ~L635); Leaderboard points note (src/pages/Leaderboard.tsx ~L593); UVSA externals (src/pages/UVSANetwork.tsx ~L119) |
Computed from system clock, hardcoded month/day constants in seasonalState.ts |
active_year |
Sep 15 – Jun 14 | Normal rendering everywhere | Same |
| Application window open/closed | Per-row open_at/due_at |
ApplicationCTA buttons on program pages |
Configured: application_links table via public_application_links view (§2) |
| Current academic term | Admin-set | Leaderboard year filters, event term assignment | Configured: academic_terms rows (one is_active) |
The test (src/utils/seasonalState.test.ts, 34 lines) pins the boundary dates (Jun 14 active / Jun 15 summer / Sep 14 summer / Sep 15 active) and the has-content override. If you ever change the boundary constants, update the test in the same PR.
2. The academic-year content cycle
Jargon: VCN = Vietnamese Culture Night (annual stage production). WNC = Wild 'N Culture. ACE = Anh Chị Em mentor-family program ("ACE lines"). House Reveal = the event where each year's new Houses and assignments are announced.
Application windows (configured, never hardcoded)
Schema: supabase/migrations/20260604000000_create_application_links.sql. Each row has application_key, open_at, due_at (timestamptz, due_at > open_at enforced), is_enabled, before_open_message, after_close_message, and a target_url that the public only ever sees while the window is open. Public access is exclusively through the public_application_links view, which masks target_url unless is_enabled AND now() BETWEEN open_at AND due_at, and computes status ∈ disabled | not_open | closed | open. The base table has admin-only RLS policies; seeded rows are disabled placeholders pointing at https://example.com/placeholder — no real URL lives in the repo, ever (AGENTS.md: "Closed or future application URLs must not be exposed publicly").
The 9 keys (CHECK-constrained in the migration; mirrored in AGENTS.md "Domain-critical facts" and ApplicationKey in src/types/database.ts):
| Key | Cycle (quarter-level) | Notes |
|---|---|---|
house_fall |
Fall quarter | House signups run once per quarter |
house_winter |
Winter quarter | |
house_spring |
Spring quarter | |
ace_application |
Fall (UNVERIFIED-ask-leadership for exact dates) | Followed by ACE Reveal — the term appears in commit 1ec81d83 ("Fix ACE Reveal copy…", changed "ACE fams" → "ACE lines" in the Wrapped recap card) |
intern_application |
Placeholder copy says "Check back next year" → annual (quarter UNVERIFIED-ask-leadership) | |
cabinet_application |
Annual, typically late spring for next year's cabinet (UNVERIFIED-ask-leadership) | |
vcn_stage_ninja_interest |
"closer to VCN" (per seeded copy) | VCN quarter itself: UNVERIFIED-ask-leadership (historically winter/spring) |
vcn_props_team_interest |
Same | |
wnc_team_form |
"closer to Wild 'N Culture" (per seeded copy); quarter UNVERIFIED-ask-leadership |
Frontend path: src/data/repos/applicationLinks.ts (repo) → src/lib/applicationLinks.ts (helpers) → src/components/common/ApplicationCTA.tsx (the gate/button rendered on program pages). Admin management: src/pages/Admin/Applications.tsx.
The rest of the year's clock (git + code evidence)
| Milestone | Timing | Repo evidence |
|---|---|---|
| Launch content for new year | Late summer / fall launch | Branch codex/launch-content-2026-2027-audit (commit cc1c5e3d); admin launch checklist page src/pages/Admin/LaunchChecklist.tsx at /admin/launch-checklist; presidents fallback src/data/presidentsContent.ts (April Pham & Havyn Nguyen for 2026–2027) |
| House Reveal | Start of fall (summer copy in seasonalState.ts: "House apps, House Reveal, and new standings will come back with the school year"); exact date UNVERIFIED-ask-leadership |
Pre-reveal placeholder UI in src/pages/House.tsx ~L673: "Houses have not been announced yet. Check back after House Reveal"; points to @vsaatucsd Instagram for official announcements |
| VCN production + archives | UNVERIFIED-ask-leadership (interest forms open "closer to VCN") | supabase/migrations/20260512000006_create_vcn_archives.sql; current-VCN content 20260513010000_add_program_content_and_current_vcn.sql; admin page src/pages/Admin/VcnArchives.tsx; public reads via the published_vcn_archives view |
| WNC | UNVERIFIED-ask-leadership | Page src/pages/WildNCulture.tsx; wnc_team_form key |
| VSA Wrapped (year in review) | End of academic year (merged 2026-07-03) | Merge fc51c96c of feat/vsa-wrapped-year-in-review (PR #178, a3d7ea60); recap card lives on the home page: src/components/features/home/WrappedRecapCard.tsx; public-safe aggregates in src/utils/wrapped.ts (community-level numbers only, never individual member stats) |
| Summer break | Jun 15 – Sep 14 | §1 |
3. FREEZE WINDOWS policy (the rule, stated as policy)
Source: owner interview, 2026-07-05. This calendar-driven discipline rule existed only in the owner's head until this skill was written. It is now policy. It complements — never overrides — AGENTS.md and
vsa-change-control.Amended by the owner, 2026-09-27: House UI is not frozen. Presentational changes to House pages — layout, styling, heroes, shared components, responsive and dark-mode polish — are routine at any time of year, including while a House Reveal is pending, as long as they do not change House data, reveal gating, or what the page shows. The House Reveal freeze covers House data and reveal content only.
The rule: around House Reveals, application-window opens, and VCN, the affected surfaces are frozen. Do not change them without explicit owner approval — even for "safe" refactors, copy tweaks, styling passes, or dependency-driven churn (House is the exception: only its data and reveal content freeze, not its UI — see the 2026-09-27 amendment above). A broken button or wrong placeholder during a reveal or an application open is a public, time-critical failure for 600+ members that cannot wait for a next deploy cycle.
Frozen surfaces per event:
| Calendar event | Frozen surfaces |
|---|---|
| House Reveal | House data and reveal content: House assets/profiles (rows and images), src/constants/houses.ts, reveal placeholder copy and the announced-vs-not-announced gating logic in src/pages/House.tsx / HouseDetail.tsx, House standings data. Not frozen: House page UI — layout, styling, heroes, shared components (routine; owner amendment 2026-09-27) |
| Application window opening (any of the 9 keys) | application_links rows and the public_application_links view, ApplicationCTA.tsx, src/lib/applicationLinks.ts, src/data/repos/applicationLinks.ts, the program page hosting that CTA (e.g. src/pages/Ace.tsx, Internship.tsx, WildNCulture.tsx) |
| VCN season | VCN program page/content, VCN archives admin + published_vcn_archives, vcn_*_interest application rows |
Pre-change check — run before touching any of the surfaces above, at any time of year:
- Are any application windows open or opening soon? Check the admin UI (
/admin/applications) or query the public view (safe, anon-readable):select application_key, status, open_at, due_at from public_application_links order by sort_order;status = 'open', ornot_openwithopen_atwithin ~2 weeks → the corresponding surfaces are frozen. - Is a House reveal pending? If the current year's Houses are still placeholders (as of 2026-07-06, 2026–2027 Houses are placeholders — AGENTS.md), a reveal is by definition pending. Never invent House names, themes, or assignments; never "pre-fill" reveal content. The placeholder state in
src/pages/House.tsxis the correct production state until leadership announces. A pending reveal freezes House data and reveal content — it does not freeze House page UI work, provided the UI change leaves the placeholder state and its copy intact. - Is VCN season active? Check whether
vcn_stage_ninja_interest/vcn_props_team_interestare open (query above) and whether current-VCN content is live (admin/admin/vcn). If yes → VCN surfaces frozen. - If any check fires and your change touches a frozen surface: stop and get explicit owner approval (see
vsa-change-controlfor the approval flow). Record the approval in the PR body.
What is not frozen: unrelated surfaces during a freeze (e.g. gallery work during an application open), and the frozen surfaces themselves outside their windows — normal vsa-change-control rules apply then.
4. Yearly turnover runbook
Tooling: most of this runbook now has an admin workflow, so a President does not need SQL. New Year Setup (
/admin/year-setup) previews and creates the year's terms, inactive Cabinet year, ACE cycle, Intern cohort, and (once profiles exist) an empty House batch, plus a separate application reset; Cabinet Rollover (/admin/cabinet/rollover) copies last year's positions (never people) and publishes the new roster without activating it. Houses stay unrevealed and application URLs are never invented. Seedocs/yearly-operations.md. The numbered steps below remain the policy; the wizard is how to carry them out.
Reconstructed from git/migration evidence of the 2025→2026 turnover. Run at end of spring / over summer, before fall launch. Every step below still goes through normal branch-and-PR flow (vsa-change-control); migrations are applied manually per vsa-run-and-operate.
- Archive last year's cabinet. The cabinet archive machinery exists: seed migration
supabase/migrations/20260512000004_seed_historical_cabinet_archive.sql, archive import commitcd2c8775(PR #29), archive URL state PR #122 (13c99b78), admin year/term management2ce699b1→src/pages/Admin/YearsTerms.tsxandsrc/pages/Admin/Cabinet.tsx. Never mix current cabinet with archive members (AGENTS.md). - Archive last year's Houses. Legacy-House machinery:
src/data/legacyHouseArchive.ts(+ its test — one of the repo's domain-fact-protecting tests; seevsa-validation-and-qa§2), seed migrations20260531000000_seed_legacy_house_assets.sqland20260601010000_restore_mario_house_assets.sql, membership history20260525000000_add_house_membership_history.sql. - Add new
academic_termsrows for the new year's quarters (supabase/migrations/20260512000000_add_academic_terms_and_cabinet_years.sql:academic_year_end = academic_year_start + 1CHECK; a unique partial index enforces exactly oneis_activeterm). Events must be assigned to terms (events.academic_term_id) for the yearly leaderboard —docs/leaderboard-system.md: leaderboard truth ismember_event_attendancejoined througheventstoacademic_terms. Admin UI:/admin/years(src/pages/Admin/YearsTerms.tsx). - Update president/launch fallbacks:
src/data/presidentsContent.ts(2026–2027: "April Pham & Havyn Nguyen"), plus degraded-mode copy insrc/config/publicFallbackContent.ts. Cross-check the launch audit pattern: branchcodex/launch-content-2026-2027-auditand the admin launch checklist (/admin/launch-checklist). - Reset/verify application windows: for each of the 9
application_linksrows, set newopen_at/due_at, refreshbefore_open_message/after_close_message, and keep rowsis_enabled = falsewith placeholder URLs until leadership provides real links. Freeze-window rules (§3) apply the moment a window opens. - Seed new House placeholders only when officially announced. Until House Reveal, the House page's built-in "not announced yet" state is correct. Never invent Houses (AGENTS.md; §5).
- Update AGENTS.md "Domain-critical facts" (new presidents, new Houses once revealed, year rollover) — and then update every skill that quotes those facts, including this one and the domain playbooks in
.claude/agents/. AGENTS.md is the canonical home; skills must not drift from it. - Review
seasonalState.tsboundaries against the new year's actual calendar (the code invites this: "can be updated when the next academic year calendar is finalized"). If changed, updateseasonalState.test.tsin the same PR.
5. Domain-facts table
Canonical home: AGENTS.md → "Domain-critical facts" (repo root, ~L229–236). Quoted as of 2026-07-06 — re-verify against AGENTS.md before relying on these, they roll over yearly:
| Fact | Value (AGENTS.md quote) |
|---|---|
| 2026–2027 presidents (fallback/copy) | "use April Pham and Havyn Nguyen" |
| 2026–2027 Houses | "placeholders only; do not invent Houses" |
| 2025–2026 Houses | "Bowser, Donkey Kong, Boo, and Toad" |
| 2023–2024 Houses | "drinks/treats, not designer Houses" |
| Designer Houses | "belong to 2019–2020"; "Mario Houses belong only to 2025–2026" |
| Application URLs | "Closed or future application URLs must not be exposed publicly" |
| Application keys | ace_application, house_fall, house_winter, house_spring, intern_application, cabinet_application, vcn_stage_ninja_interest, vcn_props_team_interest, wnc_team_form |
| Membership integrity | Never mix current with archive members; never create fake events, members, points, standings, application links, or House assignments |
6. Cross-references
- Change gating and approval flow for anything §3 freezes →
vsa-change-control. - application_links as a configuration axis (all config axes, add-a-config checklist) →
vsa-config-and-flags. - RLS/view mechanics behind
public_application_linksandpublished_vcn_archives(view-masking pattern, revoke-then-grant gotcha) →vsa-supabase-security-reference. - Domain playbooks for hands-on edits:
.claude/agents/vsa-house-system.md,vsa-cabinet-leadership.md,vsa-applications-forms.md,vsa-public-content.md. - Leaderboard/terms internals →
docs/leaderboard-system.mdandvsa-architecture-contract.
Provenance and maintenance
Sources (verified 2026-07-06, branch codex/reactbits-ui):
src/utils/seasonalState.ts(109 lines) andsrc/utils/seasonalState.test.ts(34 lines) — read in full.AGENTS.md— Domain-critical facts, Things to never do.supabase/migrations/20260604000000_create_application_links.sql(keys, window semantics, view masking);20260512000000_add_academic_terms_and_cabinet_years.sql;20260512000004_seed_historical_cabinet_archive.sql;20260512000006_create_vcn_archives.sql;20260525000000_add_house_membership_history.sql;20260531000000_seed_legacy_house_assets.sql;20260601010000_restore_mario_house_assets.sql.docs/leaderboard-system.md(terms→leaderboard dependency).- Git:
1ec81d83(ACE Reveal copy),fc51c96c/a3d7ea60(VSA Wrapped, PR #178),cd2c8775/#29 and13c99b78/#122 (cabinet archive),2ce699b1(years/terms admin),cc1c5e3d(codex/launch-content-2026-2027-audit). - Freeze-windows policy: owner interview, 2026-07-05 (previously unwritten; this file is its first written home). Amended 2026-09-27 by the owner: House UI changes are not frozen; the House Reveal freeze covers House data and reveal content only.
Re-verification one-liners:
sed -n '3,7p' src/utils/seasonalState.ts # summer boundary constants + season union
grep -rn "getSummerBreakMessage" src/pages src/components | cut -d: -f1 | sort -u # current consumers
grep -n "application_key in" -A 12 supabase/migrations/20260604000000_create_application_links.sql # the 9 keys
grep -n "Domain-critical facts" -A 10 AGENTS.md # canonical domain facts (yearly rollover)
grep -n "April Pham" src/data/presidentsContent.ts # presidents fallback
ls src/pages/Admin | grep -E "Applications|VcnArchives|YearsTerms|LaunchChecklist" # admin surfaces still exist
Volatile: everything in §5 rolls over each academic year; §1 boundary dates may be edited when a new calendar is finalized; §2 quarter-level timings marked UNVERIFIED-ask-leadership need confirmation from VSA leadership, not the repo.