Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions GITHUB_ISSUES.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ Do not redesign routing or hosting in this issue; WEB-001 owns that. WEB-001A1 [

## CI-001 — Repair test gates and secure the deployment pipeline

**COMMUNITY-STATUS-001 #724 — source-only staging child:** correct member free-event availability/cancellation-history presentation and stale UI after uncertain status reads. Existing #707/#711/#719/#722 dependencies are closed. Additive response-only enum/after-read clock; no stored/mutation/policy/authorization/schema/migration change or new service/dependency/IAM/Rules/polling. Existing transactional checks and exact saved retries remain authoritative. Backend-first exact staging source/archive, resource/profile/security, Hosting and disposable real UI/cleanup are required before availability; real refunds/transfers/dues/production and full backend completion stay separate.

**CI-STAGING-VERIFY-001 #722:** one script-only child corrects the final community success output's undefined digest, adds closed redacted failure phases and fixed read-only native verification. Local reproduction proves the reporting bug, not the caught exception from every historical failure. Clean exact green current-main source, native ancestor provenance/exact archive, scoped owner/runtime/config/resource/invoker and unchanged profile inventory remain mandatory. No new deployment, provider/IAM/data/payment mutation, dependency, workflow or production authority. See the [officer review](./docs/officers/STAGING_COMMUNITY.md#check-an-uncertain-staging-release--722). This does not close #105/#133/#136 or full backend readiness; current command/unchanged-marker evidence must be recorded in #722 before claiming hosted verification.

**CI-001C2 owner decision, 2026-10-11:** [#697](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/697) applies Dave's direct approval to continue Firebase backend setup without a backup owner. Dave (`daliu`) is the sole release/recovery owner. Backup appointment and two-person ownership are no longer backend prerequisites; absence of backup remains documented risk. #113/#133/#136 retain their other inventory, authority, exact-source, staging, policy and rollback requirements. Jeff's #667 read-only staging role does not expand. This decision is not Firebase deployment or live payment approval.
Expand Down
2 changes: 2 additions & 0 deletions OPERATIONS_RUNBOOK.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# MPRC Platform Operations Runbook

**#724 current-status correction — SOURCE ONLY:** same scoped staging service/runtime, response-only availability and no migration. Deploy only exact reviewed communityActions after green main, verify its native archive/settings/IAM and unchanged profiles, then publish exact staging Hosting. Supply only owned disposable member/event fixtures. Check all six availability states, stored confirmed history after event cancellation, own cancellation/count preservation, failed refresh clearing and exact saved retry; sample term/window boundaries after reads. Follow the [no-terminal review](./docs/officers/STAGING_COMMUNITY.md#current-member-status--724-source-prepared-not-available-yet). Remove only owned fixtures with identity/update-time fences and sign out; no extra service/polling, new permissions, real policy/payment or production action. Fresh native acceptance is pending #724.

## Read-only community release verification — #722

The former success output used an undefined `digest` identifier; a provider-free regression reproduces `ReferenceError` and verifies serialization of `bundle.digest`. Historical generic failures remain their original reported status; this does not prove which exception every old run caught. New failures expose only a closed phase enum, never exception text, provider responses, tokens or signed URLs.
Expand Down
2 changes: 2 additions & 0 deletions SYSTEM_DESIGN.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# MPRC Platform System Design

**#724 member-status correction — SOURCE ONLY:** the existing staging status read adds a response-only closed `signupStatus` enum (open/not_open_yet/closed/ended/full/cancelled), derived from validated event/window/capacity with server time sampled after reads. No stored schema, mutation authorization, count, registration/waiver/audit, membership policy, IAM/Rules/service/dependency or production change. UI distinguishes cancelled events from stored signups, fails closed on missing/unknown availability and hides old membership/event status/actions on uncertain reads while preserving exact saved retry and account/provider fences. No polling. Backend-first staging archive/resource/readback, Hosting and disposable UI/failure/cleanup proof remain required under #724; source/tests are not hosted acceptance.

**#719 fictional account linking — STAGING VERIFIED 2026-10-12:** `admin_dues_link` deliberately connects one immutable fictional receipt to one owner-marked verified/enabled disposable pending account. Exact v3 association permission is separate from recording/approval/event permissions; native recent TOTP/App Check, exact caller/input/revision and <=24-hour disposable marker are required. One transaction creates a separate link and derived authority preserving the original current term, preventing duplicate receipt/account activation, reassignment and renewal. Saved retries recheck permission first; cancellation keeps the original term and server-clock expiry removes test membership. Receipt review adds only a linked boolean, no UID/contact data. No Auth claim/role, real policy/data/payment, dependency/service/schedule/IAM expansion/migration or production change. Default-deny browser Rules cover both new collections. PR720 merged b6a8fd57d9add466da73df1d33b69e131e3fecbd after six exact PR/main jobs and every step passed. Native communityActions version 4 and staging Hosting match that source; the downloaded 91,793-byte archive matches SHA-256 47f11b8bec7f5ae58281c9029a015e9cc751a04e5ad3eafe3234441fa3a963db. Forty-four hosted cases (32 denial cases), atomic races, immutable retries, actual officer/member UI, offline retry, phone-size DOM layout, cancellation and natural expiry passed. Six disposable users and 47 exact owned documents were removed; native absence and late profiles were verified. Both capabilities were disabled first; browser sign-out, Apps-on-device OFF, normal networking/viewport, closed tab/receiver and memory disposal complete the test handoff. Profiles v2/v1/2843388, App Check/MFA and the six-permission min0/max2 runtime remain unchanged. The release command reported its existing generic post-deploy failure; independent native checks qualified the result without repeating deployment or bypassing a guard. Reporting cause remains unqualified. Production/runmprc.com and real payments were untouched.

**Status:** Target architecture and current-state assessment
Expand Down
17 changes: 17 additions & 0 deletions docs/officers/STAGING_COMMUNITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,23 @@

## Existing membership and free-event actions

### Current member status — #724 source prepared, NOT AVAILABLE YET

**Purpose:** distinguish the event's current signup availability from the member's stored signup, and stop displaying old status after an uncertain read. **Approver/escalation:** Dave, sole staging owner. **Prerequisites:** the specialist supplies the verified #724 staging release, disposable account and made-up events. New behavior is not hosted proof until #724 records exact deployment, actual UI and cleanup.

1. Open the supplied staging `/community` link and confirm **Staging test only**.
2. Sign in with the supplied disposable account.
3. Read the event's signup message before choosing an action.
4. Stop a new signup if it is cancelled, closed, full, not open yet, ended or unknown.
5. Keep **Event cancelled** separate from **Your signup** history. An event cancellation does not itself rewrite your stored signup or issue a refund.
6. If the specialist asks, cancel your own confirmed fictional signup using the existing button.
7. Select **Refresh status** after an event change or term expiry.
8. If the refresh fails, expect old membership/event status and new-action controls to disappear.
9. Use **Retry request** only for an uncertain saved action, or **Refresh status** for a new read.
10. Confirm only a successful fresh read restores current controls, then sign out and request owned-fixture cleanup.

**Expected result/proof:** the server projects existing event window/capacity/status constraints using its clock after reads; the browser cannot turn a display value into authorization. Counts, registration/waiver evidence and paid-term cancellation policy are unchanged. A confirmed stored signup on a cancelled event is history, not proof that the event will take place. Existing transactional join checks still decide the result if availability changes after the read. Missing/unknown projection fails closed. No polling or extra service is added. **Stop:** production, real data/payment, unclear availability/count/history, stale status surviving failure or request for secrets/record edits. **Undo:** sign out; Dave/specialist uses the reviewed staging rollback if required. Account/fixture supply, deployment/readback/cleanup and rollback remain specialist-only; no officer uses a terminal or edits records. Real refunds, bib transfers and production remain unavailable.

- Request membership: records a pending request. It does not prove dues or grant membership.
- Withdraw membership request: withdraws a pending application.
- Cancel membership: records a cancellation request for an already-approved synthetic membership. Access lasts through its original paid term. No refund occurs.
Expand Down
14 changes: 14 additions & 0 deletions docs/officers/SYSTEM_MAPS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

These diagrams show where pages, information, and deployments go. Each diagram includes a one-sentence text version.

## Member status freshness — #724 source only

```mermaid
flowchart LR
Member["Disposable staging member"] --> Read["Current status read; no polling"]
Read --> Server["Validated event/capacity; clock sampled after reads"]
Server --> Display["Signup availability and separate stored signup"]
Read -- "Failure or uncertain action" --> Hide["Hide old status/new actions; keep exact saved retry"]
Hide --> Read
Display --> Mutation["Existing transactional join/cancel checks decide again"]
```

Text alternative: the staging member page displays server-derived availability separately from stored signup history, hides old status on uncertainty and restores it only after a fresh read, while existing transactions—not the display—continue deciding joins and cancellations; hosted proof remains pending #724.

## Uncertain staging release review — #722

```mermaid
Expand Down
15 changes: 15 additions & 0 deletions functions/communityActions.emulator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ describeLocal('community lifecycle real Firestore transactions (emulator only)',
now = Date.now(); service = createCommunityService(db, () => now);
});
beforeEach(async () => {
now = Date.now();
for (const root of roots) await db.recursiveDelete(db.collection(root));
await db.doc('communityConfig/events').create({ schemaVersion: 1, eventIds: ['fixture-event'] });
await db.doc('communityEvents/fixture-event').create(freeEvent(now));
Expand Down Expand Up @@ -51,6 +52,20 @@ describeLocal('community lifecycle real Firestore transactions (emulator only)',
expect((await service.status(uid)).membership.entitlement).toBe('current_member');
now = paid.authority.term.endsAtMs;
expect((await service.status(uid)).membership.entitlement).toBe('not_entitled');
}, 45000); // Concurrent native transactions may outlast Jest's five-second unit default.
test('cancelled-event availability is a read-only projection and preserves own confirmed signup evidence', async () => {
const uid = 'fixture-status-member';
await service.mutate(uid, join());
const event = db.doc('communityEvents/fixture-event');
const registration = event.collection('registrations').doc(uid);
await event.update({ status: 'cancelled' });
const before = await db.getAll(event, registration);
const view = await service.status(uid);
expect(view.events[0]).toMatchObject({ status: 'cancelled', signupStatus: 'cancelled', confirmedCount: 1, registration: { status: 'confirmed', revision: 1 } });
expect((await service.status('fixture-other')).events[0].registration.status).toBe('none');
const after = await db.getAll(event, registration);
expect(after.map(doc => doc.data())).toEqual(before.map(doc => doc.data()));
expect(after.map(doc => doc.updateTime.toMillis())).toEqual(before.map(doc => doc.updateTime.toMillis()));
});
test('officer approval/withdrawal, capacity/closure concurrency and live revocation use real transactions', async () => {
const uid = 'fixture-officer'; const runner = 'fixture-member';
Expand Down
14 changes: 12 additions & 2 deletions functions/communityActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,23 @@ function createCommunityService(db, clock = Date.now) {
const memberRef = (uid) => db.doc(`communityMemberships/${uid}`);
const emptyMember = { schemaVersion: 1, revision: 0, applicationStatus: 'none', authority: null };
async function status(uid) {
const now = clock();
const [member, manifest] = await db.getAll(memberRef(uid), db.doc('communityConfig/events'));
const ids = manifest.exists ? manifest.data().eventIds : [];
check(Array.isArray(ids) && ids.length <= 20 && new Set(ids).size === ids.length && ids.every((id) => typeof id === 'string' && ID.test(id)));
const refs = ids.flatMap((id) => [db.doc(`communityEvents/${id}`), db.doc(`communityEvents/${id}/registrations/${uid}`)]);
const docs = refs.length ? await db.getAll(...refs) : [];
const events = ids.map((id, index) => ({ ...eventView(docs[index * 2].data(), id), registration: registrationView(docs[index * 2 + 1].data()) }));
// Read completion, not request start, determines time-based presentation.
// Mutations still independently enforce their transactional constraints.
const now = clock(); check(integer(now, 8_640_000_000_000_000));
const events = ids.map((id, index) => {
const event = eventView(docs[index * 2].data(), id);
const signupStatus = event.status === 'cancelled' ? 'cancelled'
: event.status === 'closed' ? 'closed'
: now < event.opensAtMs ? 'not_open_yet'
: now >= event.closesAtMs ? 'ended'
: event.confirmedCount >= event.capacity ? 'full' : 'open';
return { ...event, signupStatus, registration: registrationView(docs[index * 2 + 1].data()) };
});
return { membership: memberView(member.exists ? member.data() : emptyMember, uid, now), events };
}

Expand Down
42 changes: 42 additions & 0 deletions functions/communityActions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,48 @@ describe('transactional membership and free-event lifecycle', () => {
await service.mutate('runner-a', joining());
expect((await service.status('runner-b')).events[0].registration.status).toBe('none');
});
test.each([
[{}, 'open'],
[{ opensAtMs: 1_800_000_000_001 }, 'not_open_yet'],
[{ opensAtMs: 1_800_000_000_000 }, 'open'],
[{ closesAtMs: 1_800_000_000_000 }, 'ended'],
[{ status: 'closed' }, 'closed'],
[{ confirmedCount: 1 }, 'full'],
[{ status: 'cancelled', confirmedCount: 1 }, 'cancelled'],
])('status projects existing signup constraints without writes %#', async (extra, expected) => {
docs.set('communityEvents/one-seat', freeEvent(now, extra));
const before = JSON.stringify([...docs]);
expect((await service.status('runner-a')).events[0].signupStatus).toBe(expected);
expect(JSON.stringify([...docs])).toBe(before);
});
test('cancelled event status retains confirmed signup history and another UID sees none', async () => {
await service.mutate('runner-a', joining());
docs.get('communityEvents/one-seat').status = 'cancelled';
const before = JSON.stringify([...docs]);
expect((await service.status('runner-a')).events[0]).toMatchObject({
status: 'cancelled', signupStatus: 'cancelled', confirmedCount: 1,
registration: { status: 'confirmed', revision: 1 },
});
expect((await service.status('runner-b')).events[0].registration.status).toBe('none');
expect(JSON.stringify([...docs])).toBe(before);
});
test('server status samples time after reads so a term/window ending during I/O is not shown active/open', async () => {
const memory = memoryDb(); const began = now; let current = began;
memory.docs.set('communityMemberships/runner-a', approvedMember('runner-a', began));
memory.docs.set('communityConfig/events', { schemaVersion: 1, eventIds: ['one-seat'] });
memory.docs.set('communityEvents/one-seat', freeEvent(began, { closesAtMs: began + 60000 }));
const original = memory.db.getAll;
memory.db.getAll = async (...refs) => { const value = await original(...refs); current = began + 60000; return value; };
const read = createCommunityService(memory.db, () => current);
const before = JSON.stringify([...memory.docs]);
const result = await read.status('runner-a');
expect(result.membership.entitlement).toBe('not_entitled');
expect(result.events[0].signupStatus).toBe('ended');
expect(JSON.stringify([...memory.docs])).toBe(before);
});
test('display availability cannot be injected into a signup request', () => {
expect(() => parseRequest({ ...joining(), signupStatus: 'open' })).toThrow('Request is invalid.');
});
test('write failure rolls back all effects and a retry remains safe', async () => {
const memory = memoryDb(); const command = request('membership_join');
const original = memory.db.runTransaction;
Expand Down
Loading
Loading