8.6 KiB
Design
Architecture
This task keeps the app in a single Next.js project and continues the Drizzle/PostgreSQL-backed server domain layer inside modules/:
modules/core/server/: Drizzle connection, schema, session, permissions, audit helpers, and compatibility read models.modules/auth/: UI and client interactions for setup, register, login, logout, and session state.modules/elders/: elder schemas/types, validation, and CRUD UI components.modules/settings/: role/account/organization/audit/status display and management components.modules/facilitiesor existing page-local modules: room, bed, occupancy, admission, transfer, and discharge UI as the feature grows.app/api/.../route.ts: Route Handlers for auth, session, elders, facilities, admissions, accounts, roles, organizations, status, and audit APIs.
The persistence boundary is now Drizzle/PostgreSQL, not JSON files. UI and route handlers should use domain helpers or direct Drizzle queries through getDatabase() in server-only modules. New mutation paths must not use writeData() or updateData().
Data Storage
The current persistent model is PostgreSQL with Drizzle schema in modules/core/server/schema.ts and generated migrations under drizzle/.
Important existing tables:
- Identity and tenancy:
accounts,sessions,organizations,memberships,join_requests,organization_invitations. - Authorization:
roles,permissions,role_permissions. - Operations:
elders,rooms,beds,admissions. - Governance and status:
audit_logs,system_incidents,system_settings.
modules/core/server/store.ts should be treated as a compatibility read model only:
readData()may aggregate Drizzle rows into existing UI-friendly types while legacy pages are migrated.writeData()andupdateData()intentionally throw after the PostgreSQL migration.- New feature work should prefer focused query helpers so the compatibility model does not become the long-term domain layer.
API Contracts
Route Handlers return a consistent response shape:
type ApiResult<T extends Record<string, unknown>> =
| ({ success: true; reason: string } & T)
| { success: false; reason: string };
Existing or expected endpoints:
GET /api/auth/bootstrap: returns whether setup is required.POST /api/auth/setup: creates the first platform admin, first organization, session, roles/membership, and audit log.POST /api/auth/register: creates a user registration or invitation-based account flow where enabled.POST /api/auth/login: validates credentials, creates a session, logs login.POST /api/auth/logout: deletes current session cookie/session, logs logout.GET /api/auth/session: returns current account, organization, membership, and permissions.GET /api/elders: lists elder profiles for the active organization.POST /api/elders: creates an elder profile and optional active admission.PATCH /api/elders/[id]: updates an elder profile.DELETE /api/elders/[id]: deletes an elder profile.GET /api/facilities/rooms: lists rooms for the active organization.POST /api/facilities/rooms: creates room records when facility management UI exposes this.GET /api/facilities/beds: lists beds and current occupancy.POST /api/facilities/beds: creates bed records when facility management UI exposes this.GET /api/admissions: lists admission and transfer history.POST /api/admissions: admits or transfers an elder into an available bed.- Planned:
PATCH /api/admissions/[id]or equivalent mutation endpoint for transfer/discharge if POST cannot express the workflow cleanly. GET /api/settings/accounts: lists accounts for authorized roles.GET /api/settings/roles: lists built-in and organization roles.GET /api/settings/permissions: lists permission coverage.GET /api/audit-logs: lists recent audit events.
Authentication
Sessions use an HTTP-only cookie named teatea_session. Route Handlers read and write cookies with await cookies() from next/headers, matching current Next.js behavior.
Passwords are never stored in plain text. The current implementation uses Node built-in crypto with per-account salt and scryptSync, which is acceptable for this local MVP until a dedicated auth library is introduced in a separate task.
Protected app routes should use server session state where practical instead of client-only localStorage guards. Server-rendered pages that depend on session cookies must opt out of static prerendering with export const dynamic = "force-dynamic" where needed.
Permissions
Permissions and role definitions live in the core server/shared type boundary:
- Platform:
platform:manage,organization:read,organization:manage. - Account/role/security:
account:read,account:manage,role:read,role:manage,permission:read,audit:read. - Operations:
facility:read,facility:manage,admission:read,admission:manage,elder:read,elder:create,elder:update,elder:delete. - Status:
incident:read,incident:manage.
Route Handlers call a shared requirePermission(permission) helper. This helper returns an authenticated context or a structured forbidden/unauthorized response and writes denied audit entries.
Admission Transactions
Bed/admission mutations must run inside a Drizzle transaction.
Admit flow:
- Validate active organization and
admission:manage. - Validate elder belongs to the active organization.
- Validate target bed belongs to the active organization and has status
available. - Close or transfer any active admission for that elder if the operation is a transfer.
- Insert a new
admissionsrow with statusactive. - Set target bed status to
occupied. - Set elder status to
active. - Record an audit log.
Transfer flow:
- Find active admission for the elder.
- Set previous admission status to
transferredanddischargedAtto now. - Set previous bed status to
available. - Insert the new active admission and occupy the target bed.
- Record an audit log.
Discharge flow:
- Find active admission.
- Set admission status to
dischargedanddischargedAtto now. - Set bed status to
available. - Set elder status to
dischargedor another explicitly selected status. - Record an audit log.
All conflict checks must return structured API failures rather than partially mutating state.
UI Flow
The app should remain an operational workspace: dense, restrained, and action-oriented.
Elder page:
- Server Component loads initial elders and available beds.
- Client component handles create/edit/delete forms and refreshes after mutations.
- Controls are disabled or hidden based on current permissions, while APIs still enforce permission checks.
Bed/admission page:
- Replace raw API placeholders with page-local controls.
- Use tabs or segmented navigation for overview, bed status, admissions/history, and management actions.
- Place admit/transfer/discharge actions inside the bed/admission workspace, not as a dead global top-bar button.
- Show occupancy metrics, active admissions, room/bed tables, and history from Drizzle-backed data.
Dashboard:
- Replace hard-coded counters for implemented domains with Drizzle-backed data.
- Use a standard chart library for selected charts such as occupancy distribution or admission activity.
- Keep chart usage modest; tables and status lists remain the primary record surfaces.
Screenshot feedback:
- Move page-level secondary navigation into tabs where appropriate.
- Remove oversized intro/hero blocks from routine operational pages.
- Remove redundant header action controls that are not wired to the current page workflow.
Compatibility
Static module pages not in scope remain untouched except for auth/layout integration. Existing visual design should be preserved: dense operational screens, restrained cards/tables, project UI adapters under components/ui/*, and Tailwind-compatible layout.
Risks and Rollback
- Drizzle schema and migrations are now the persistence source of truth; mismatches between schema and migrations can block deployment. Rollback point: keep migration changes separate from UI-only work.
- Admission mutations touch multiple tables. Rollback point: keep transaction helpers isolated and temporarily render read-only admission data if mutation UI is unstable.
- Replacing global header actions can affect user navigation habits. Rollback point: remove only the dead top-bar "入住" button while keeping the sidebar and pages stable.
- Adding a chart library increases client bundle size. Rollback point: limit chart usage to one focused client component and keep tables as the fallback data surface.