docs(presence): API_REFERENCE + ARCHITECTURE sections for migration 038
This commit is contained in:
@@ -936,3 +936,12 @@ Card UID is a credential. `member-cards.ts`: `addMemberCard`/`updateMemberCard`/
|
||||
Phase 3 live actions (all `door.operate`, rate-limited, every attempt writes one redacted `door_access_log` row, service client after the permission check since operators have no RLS read on `door_connections`/`member_cards`): `grantCard({connectionId, cardId, permissionMask?})` allocates the card's per-connection integer slot via the pure lowest-free allocator (`door_card_slots`; idempotent re-grant, `slot_exhausted` when full), reserves it in the DB first, calls the controller, and rolls the reservation back if the call fails. `revokeCard({connectionId, cardId})` is idempotent (no slot assigned means already revoked) and frees the slot only on confirmed controller success so the app's map never diverges from the device. `doorControl({connectionId, verb})` with `verb` of `open`/`unlock`/`lock` (HeatSync `open` is a momentary `o1`; generic controllers need the matching verb template) touches no slot.
|
||||
|
||||
Member self-entry (no permission code; any active member): `selfEntry({connectionId})` triggers a momentary OPEN only, allowed only when the connection is `is_enabled` AND `allow_member_self_entry` AND the caller has at least one active `member_card` on file (the locked eligibility rule: a `door_card_slots` row is NOT required). Membership and cards are resolved server-side, so a member can only ever open a door for themselves; never unlock/lock/grant/revoke, never anonymous. Strict per-member rate limit (5/min), one redacted `door_access_log` row `action='self_entry'` (`target_member_id` = self), through the same hardened executor. `listSelfEntryDoors()` returns the enabled self-entry connections for the caller's space, but only when the caller has an active card (otherwise empty), so the surface is hidden for ineligible members. `listMyDoorActivity()` returns the caller's own `door_access_log` rows (actor or target; service client after `requireMember`; `detail` is already secret-redacted at write time). Surfaced as a "Door access" panel on the dashboard and on the member `/doors` page (self-entry + masked own cards via `getMyCards` + recent personal activity).
|
||||
|
||||
### Presence & attendance: `presence.ts` (migration 038)
|
||||
No permission code; any active member. All writes are self-only — the member is resolved server-side, never taken from input — and go through validated service-client actions (`space_visits` has no client write policy). Pure logic in `lib/presence-logic.ts` (`presenceStatus`, `hostEligibility`, `summarizePresence`) is unit-tested.
|
||||
- `checkIn({asHost?, note?})` — opens a visit. Enforces one open visit per member (partial unique index); if an existing open visit is stale (`presenceStatus` past `PRESENCE_MAX_OPEN_HOURS`, 18h) it is auto-closed first (handles forgotten check-outs without a cron), and a fresh existing open visit is rejected ("already checked in"). `asHost` runs `hostEligibility`: if `spaces.host_requires_card` (default true) the caller must have an active `member_card` on file (count checked via the service client); a space may flip the toggle off to let anyone self-mark host. Writes an `activity_log` row.
|
||||
- `checkOut({note?})` — closes the caller's current open visit (optional note). Errors if not checked in.
|
||||
- `listPresentNow()` — any member; who is currently here (open visits with staleness filtered out in pure logic), with `isHost`/`isMe` flags. Presence is intentionally visible to all space members.
|
||||
- `getMyVisits()` — the caller's own recent visits (the `/me` history).
|
||||
- `listAttendance()` — any member (org-wide attendance history is all-members by product decision); last 250 visits with computed status. No new permission code; managers are not special here.
|
||||
Surfaced as the "Who's here" panel on the dashboard (present list + self check-in/out + host + note), the `/attendance` page (all members), and "My recent visits" on `/me`.
|
||||
|
||||
@@ -566,6 +566,29 @@ universal API-call UI builder. No anonymous path.
|
||||
|
||||
---
|
||||
|
||||
### Presence & attendance (migration 038)
|
||||
|
||||
`space_visits` records one row per visit (check-in/out, `is_host`, in/out
|
||||
notes); an open row (`checked_out_at IS NULL`) means the member is here. A
|
||||
partial unique index keeps at most one open visit per member. Pure logic in
|
||||
`lib/presence-logic.ts` (`presenceStatus`, `hostEligibility`,
|
||||
`summarizePresence`), unit-tested. Server actions in
|
||||
`lib/actions/presence.ts` are self-only (member resolved server-side) and
|
||||
funnel through the service client; `space_visits` has SELECT for any space
|
||||
member (presence is social) and **no client write policy**. There is no new
|
||||
permission code: current presence is visible to all members, the org-wide
|
||||
`/attendance` history is all-members by product decision, and every member
|
||||
sees their own history on `/me`. Forgotten check-outs are handled without a
|
||||
cron: an open visit older than `PRESENCE_MAX_OPEN_HOURS` (18h) is treated as
|
||||
not-present in pure logic and auto-closed on the member's next check-in.
|
||||
Checking in as a host is gated by `spaces.host_requires_card` (default
|
||||
true): when set, the member needs an active `member_card` on file
|
||||
(reuses the access-card model); a space may relax it to let anyone
|
||||
self-mark host. Surfaced as the dashboard "Who's here" panel, the
|
||||
`/attendance` page, and "My recent visits" on `/me`. No anonymous path.
|
||||
|
||||
---
|
||||
|
||||
## 13. Known Limitations
|
||||
|
||||
1. **No test suite** - Unit, integration, and E2E tests needed
|
||||
|
||||
Reference in New Issue
Block a user