indira-workspace-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/indira-api/SKILL.md +148 -0
- package/.claude/skills/indira-api/reference/api-architecture.md +337 -0
- package/.claude/skills/indira-ui/SKILL.md +149 -0
- package/README.md +60 -0
- package/bin/cli.js +65 -0
- package/dist-lib/indira-workspace-ui.cjs +2 -0
- package/dist-lib/indira-workspace-ui.cjs.map +1 -0
- package/dist-lib/indira-workspace-ui.mjs +1522 -0
- package/dist-lib/indira-workspace-ui.mjs.map +1 -0
- package/dist-lib/style.css +725 -0
- package/docs/design-system.md +462 -0
- package/package.json +68 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: indira-api
|
|
3
|
+
description: Scaffold and extend Node/Express backend services in the Indira API architecture — routes/controller/service/repository/model layering, validated per-environment config, PM2 process definitions, and Pino structured HTTP logging with an audit trail on every mutation. Ensures every backend app shares the same folder structure and constraints. Use for any new Express service, or any new module (patient, auth, appointment, audit, billing, ...) added to an existing one.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# indira-api
|
|
7
|
+
|
|
8
|
+
You build Node/Express backend services in the **Indira API architecture**.
|
|
9
|
+
The full spec lives at `docs/api-architecture.md` in the target repository —
|
|
10
|
+
read it completely before doing anything else; every path, filename and rule
|
|
11
|
+
you need is in it, and this skill does not repeat them.
|
|
12
|
+
|
|
13
|
+
Your job has four parts, in order. Do not skip Part 0 or Part 1.
|
|
14
|
+
|
|
15
|
+
## Part 0 — Establish the spec (new repo only)
|
|
16
|
+
|
|
17
|
+
Check whether the target repo already has `docs/api-architecture.md`.
|
|
18
|
+
|
|
19
|
+
- **It exists** — read it. It is the source of truth for *this* repo, even
|
|
20
|
+
where a later edition of this skill's own copy has moved on. Follow what's
|
|
21
|
+
written there; if it's silent on something this skill covers, follow this
|
|
22
|
+
skill and consider proposing an addition to the repo's doc.
|
|
23
|
+
- **It does not exist** — this is a new service. Copy
|
|
24
|
+
`reference/api-architecture.md` from this skill into `docs/api-architecture.md`
|
|
25
|
+
in the target repo verbatim, then fill in the one placeholder it leaves
|
|
26
|
+
open: §2.4 names the DB driver/ORM as "a per-app choice, recorded here" —
|
|
27
|
+
ask which one this app uses (see Part 1) and write it into that section
|
|
28
|
+
before treating the doc as final.
|
|
29
|
+
|
|
30
|
+
Never invent a second architecture doc under a different name or path —
|
|
31
|
+
`docs/api-architecture.md` is the one location, matching how the UI side of
|
|
32
|
+
these apps keeps its spec at `docs/design-system.md`.
|
|
33
|
+
|
|
34
|
+
## Part 1 — Ask before you scaffold
|
|
35
|
+
|
|
36
|
+
Do not scaffold from a one-line request. For a **new service**, confirm:
|
|
37
|
+
|
|
38
|
+
- **Purpose & callers** — what does this service do, and who calls it (a
|
|
39
|
+
specific frontend, another internal service, a cron, a webhook)?
|
|
40
|
+
- **Database** — Postgres, MySQL, MongoDB, something else? Driver or ORM
|
|
41
|
+
preference, or should it match another Indira service for consistency?
|
|
42
|
+
- **First modules** — beyond the mandatory `auth` and `audit` (§12 of the
|
|
43
|
+
spec builds these first), which domain modules does the initial version
|
|
44
|
+
need? Get the entity name and its core fields for each.
|
|
45
|
+
- **Auth model** — session, JWT, SSO against an existing identity provider?
|
|
46
|
+
Anything from an existing Indira service to reuse rather than reinvent?
|
|
47
|
+
- **Environments** — which of local/uat/production does this need on day
|
|
48
|
+
one, and are there secrets beyond DB + mail that the env schema must
|
|
49
|
+
declare?
|
|
50
|
+
|
|
51
|
+
For a **new module in an existing service**, confirm just:
|
|
52
|
+
|
|
53
|
+
- **Entity & fields** — name and core fields of the thing this module owns.
|
|
54
|
+
- **Operations** — CRUD, or specific actions (e.g. "approve", "cancel")?
|
|
55
|
+
Which need auth scopes beyond a logged-in user?
|
|
56
|
+
- **Cross-module reads** — which existing modules' *services* (never
|
|
57
|
+
repositories) does this one need to call?
|
|
58
|
+
- **Sensitive fields** — anything that must be redacted from logs or kept
|
|
59
|
+
out of the default list response, per §5 and law 4 of the spec.
|
|
60
|
+
|
|
61
|
+
Write the answers up as a short brief and confirm it before Part 2:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
Service: <name> New service | New module in <service>
|
|
65
|
+
DB: <engine/ORM> Auth: <model>
|
|
66
|
+
Modules (first cut): <a>, <b>, auth, audit
|
|
67
|
+
For <this module>: entity <name>, fields <...>, operations <...>
|
|
68
|
+
Depends on (via service): <module>, <module>
|
|
69
|
+
Sensitive fields: <...>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Part 2 — Plan the files
|
|
73
|
+
|
|
74
|
+
List, module by module, exactly the files §1–§2 of the spec require —
|
|
75
|
+
`routes.js dto.js controller.js service.js repository.js model.js`, plus
|
|
76
|
+
`errors.js` only if the module needs a domain-specific error and
|
|
77
|
+
`test.js` for its unit tests. No extra files, no extra top-level folders.
|
|
78
|
+
|
|
79
|
+
Checks before you build — answer each with yes:
|
|
80
|
+
|
|
81
|
+
- Does every route have a matching DTO validating body/params/query (§7,
|
|
82
|
+
law 6)?
|
|
83
|
+
- Does the controller do nothing but translate HTTP ↔ domain — no DB import,
|
|
84
|
+
no business `if` (§2.2)?
|
|
85
|
+
- Does the repository expose plain async functions only, with zero business
|
|
86
|
+
rules (§2.4)?
|
|
87
|
+
- Does every mutation route sit behind the shared write path that writes to
|
|
88
|
+
`audit` (law 5)?
|
|
89
|
+
- Does every write route (bar the ones explicitly public) sit behind
|
|
90
|
+
`auth(...)` (§10)?
|
|
91
|
+
- Are cross-module calls going through the other module's *service*, never
|
|
92
|
+
its repository or model (law 7)?
|
|
93
|
+
- Will a missing/invalid env var stop boot rather than fail mid-request
|
|
94
|
+
(§3, law 2)?
|
|
95
|
+
|
|
96
|
+
If any answer is no, fix the plan before writing code.
|
|
97
|
+
|
|
98
|
+
## Part 3 — Build
|
|
99
|
+
|
|
100
|
+
1. If Part 0 created a new doc, scaffold the tree in §1 exactly: `app.js`,
|
|
101
|
+
`server.js`, `config/`, `shared/{middleware,errors,mail,logger,utils}`,
|
|
102
|
+
`routes.js`, `ecosystem.config.js`, `.env.example`.
|
|
103
|
+
2. Wire `config/index.js` (§3) with a Zod schema covering every env var this
|
|
104
|
+
app needs so far. Confirm it exits non-zero on a missing required key —
|
|
105
|
+
check this once by actually removing a required var and running it,
|
|
106
|
+
don't assume the schema does what it looks like it does.
|
|
107
|
+
3. Wire the shared Pino logger + `pino-http` (§5) into `app.js` **before**
|
|
108
|
+
any route, then `helmet`, CORS from config, a body size limit, and the
|
|
109
|
+
`/api/auth/*` rate limiter (§10).
|
|
110
|
+
4. Wire `errorHandler.js` (§6) as the last middleware, after `routes.js` is
|
|
111
|
+
mounted.
|
|
112
|
+
5. Build `auth` and `audit` first if they don't exist yet — every other
|
|
113
|
+
module depends on both.
|
|
114
|
+
6. For each domain module, write files in this order so nothing imports
|
|
115
|
+
something that doesn't exist yet: `dto.js` → `model.js` →
|
|
116
|
+
`repository.js` → `service.js` → `controller.js` → `routes.js`, then
|
|
117
|
+
mount the router in `routes.js`.
|
|
118
|
+
7. Add `ecosystem.config.js` and `.env.example` (or extend them) so every
|
|
119
|
+
key the config schema declares has a placeholder.
|
|
120
|
+
8. Write one integration test per new route under `tests/`, and unit tests
|
|
121
|
+
for the service's business rules beside the module.
|
|
122
|
+
9. Verify (§12 of the spec): the server boots, `GET /api/health` returns
|
|
123
|
+
200, one request produces one structured Pino log line carrying a
|
|
124
|
+
request id, and a deliberately-triggered error returns the `{ error:
|
|
125
|
+
{ code, message, requestId } }` shape with that same id.
|
|
126
|
+
|
|
127
|
+
## What "done" looks like
|
|
128
|
+
|
|
129
|
+
Give back, in this order: the confirmed brief, the file plan, what was
|
|
130
|
+
scaffolded vs. added to an existing module, the verification results from
|
|
131
|
+
step 9, and — if this touched `docs/api-architecture.md` — a note of what
|
|
132
|
+
changed there and why, since that file governs every other app too.
|
|
133
|
+
|
|
134
|
+
## Things you never do
|
|
135
|
+
|
|
136
|
+
- Read `process.env` anywhere outside `config/index.js`.
|
|
137
|
+
- Let a controller import a repository or a DB driver, or a repository
|
|
138
|
+
contain a business decision.
|
|
139
|
+
- Call `console.log`/`console.error` for application logging.
|
|
140
|
+
- Return an error value from a service for a controller to branch on —
|
|
141
|
+
services throw.
|
|
142
|
+
- Add a write route without wiring it to the audit trail, or without
|
|
143
|
+
`auth(...)` unless it's deliberately public.
|
|
144
|
+
- Commit a real `.env.local`/`.env.uat`/`.env.production` file, or put a
|
|
145
|
+
secret in `ecosystem.config.js`.
|
|
146
|
+
- Invent a second architecture doc, a new top-level `src/` folder, or a
|
|
147
|
+
driver/ORM choice that contradicts what `docs/api-architecture.md`
|
|
148
|
+
already recorded for this repo.
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# Indira API architecture specification
|
|
2
|
+
|
|
3
|
+
A build-from-scratch specification for the Node/Express backend services used
|
|
4
|
+
across Indira apps. Written so an agent (or a person) can scaffold a new
|
|
5
|
+
service, or extend an existing one, and land on the exact same shape every
|
|
6
|
+
time. Where a path or a filename appears, it is the path — not a suggestion.
|
|
7
|
+
|
|
8
|
+
The reference stack is Express + a validated env config + Pino logging. The
|
|
9
|
+
database driver/ORM is a per-app choice (see §2.4); everything else here is
|
|
10
|
+
fixed.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 0. The three sentences
|
|
15
|
+
|
|
16
|
+
1. **A request flows in one direction only**: route → controller → service →
|
|
17
|
+
repository → database, and back out the same way in reverse. No layer
|
|
18
|
+
calls sideways or skips the one below it.
|
|
19
|
+
2. **Config is validated once, at boot, in one place.** If an environment
|
|
20
|
+
variable is missing or malformed, the process refuses to start — it never
|
|
21
|
+
fails later, mid-request, in a way that's hard to trace.
|
|
22
|
+
3. **Every request is logged as one structured line, and every domain
|
|
23
|
+
mutation is written to the audit trail.** Nothing is inferred from
|
|
24
|
+
`console.log` output after the fact.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 1. Folder structure
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
server/
|
|
32
|
+
├── src/
|
|
33
|
+
│ ├── app.js # express app: middleware, mount routes
|
|
34
|
+
│ ├── server.js # listen, graceful shutdown
|
|
35
|
+
│ ├── config/
|
|
36
|
+
│ │ ├── index.js # validated env (zod), single import point
|
|
37
|
+
│ │ └── db.js # connection/pool setup, exported once
|
|
38
|
+
│ ├── modules/
|
|
39
|
+
│ │ ├── <module>/
|
|
40
|
+
│ │ │ ├── <module>.routes.js
|
|
41
|
+
│ │ │ ├── <module>.controller.js
|
|
42
|
+
│ │ │ ├── <module>.service.js
|
|
43
|
+
│ │ │ ├── <module>.repository.js # DB access only
|
|
44
|
+
│ │ │ ├── <module>.model.js # schema / entity
|
|
45
|
+
│ │ │ ├── <module>.dto.js # request/response shapes + validation
|
|
46
|
+
│ │ │ ├── <module>.errors.js # domain errors (optional)
|
|
47
|
+
│ │ │ └── <module>.test.js # unit tests, colocated
|
|
48
|
+
│ │ ├── auth/
|
|
49
|
+
│ │ ├── audit/
|
|
50
|
+
│ │ └── ... # one directory per domain module
|
|
51
|
+
│ ├── shared/
|
|
52
|
+
│ │ ├── middleware/ # auth, validate(dto), errorHandler, requestId
|
|
53
|
+
│ │ ├── errors/ # AppError, NotFoundError, ForbiddenError, ...
|
|
54
|
+
│ │ ├── mail/ # transport.js, graph.js
|
|
55
|
+
│ │ ├── logger/ # pino instance + pino-http middleware
|
|
56
|
+
│ │ └── utils/
|
|
57
|
+
│ └── routes.js # mounts every module's router under /api
|
|
58
|
+
├── tests/ # integration tests only; unit tests live in modules
|
|
59
|
+
├── scripts/ # seed, migrate, one-off ops
|
|
60
|
+
├── ecosystem.config.js # committed; PM2 definition for every environment
|
|
61
|
+
├── .env.example # committed; placeholders only
|
|
62
|
+
├── .env.local # gitignored; developer's machine
|
|
63
|
+
├── .env.uat # gitignored; lives only on the UAT box
|
|
64
|
+
├── .env.production # gitignored; lives only on the prod box
|
|
65
|
+
└── package.json
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Nothing is created above `server/src/modules/*` beyond what's listed here. A
|
|
69
|
+
new top-level folder under `src/` is a spec change, not a per-app decision —
|
|
70
|
+
raise it, don't add it quietly.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 2. Layering — one direction, one job per file
|
|
75
|
+
|
|
76
|
+
### 2.1 Routes (`<module>.routes.js`)
|
|
77
|
+
Declares the Express `Router()`, wires `validate(dto)` and `auth(...)`
|
|
78
|
+
middleware, and points each path at a controller method. No logic. No
|
|
79
|
+
`req`/`res` handling beyond what the router itself needs.
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
router.post("/", auth("patient:write"), validate(createPatientDto), controller.create);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 2.2 Controller (`<module>.controller.js`)
|
|
86
|
+
Translates HTTP ↔ domain: reads `req.params/query/body` (already validated by
|
|
87
|
+
the DTO middleware), calls exactly one service method, and shapes the
|
|
88
|
+
response envelope (§8.2). A controller never imports the repository or the
|
|
89
|
+
database driver, and never contains business rules — if it has an `if` that
|
|
90
|
+
decides an outcome rather than a status code, that branch belongs in the
|
|
91
|
+
service.
|
|
92
|
+
|
|
93
|
+
### 2.3 Service (`<module>.service.js`)
|
|
94
|
+
All business logic. Orchestrates one or more repositories, calls other
|
|
95
|
+
modules' services directly (never their repositories), throws domain errors
|
|
96
|
+
(§6) instead of returning error objects, and knows nothing about `req`/`res`
|
|
97
|
+
or HTTP status codes.
|
|
98
|
+
|
|
99
|
+
### 2.4 Repository (`<module>.repository.js`)
|
|
100
|
+
The only file in the module allowed to import the DB driver/ORM. Exposes
|
|
101
|
+
plain async functions (`findById`, `create`, `listByStatus`, ...) that return
|
|
102
|
+
plain objects or `null` — never a query builder, never a raw driver row.
|
|
103
|
+
Contains no business rules: a repository method that decides *whether* to do
|
|
104
|
+
something, not just *how*, has leaked a service concern.
|
|
105
|
+
|
|
106
|
+
The specific driver/ORM (pg, Prisma, Mongoose, ...) is chosen per app and
|
|
107
|
+
recorded in that app's `docs/api-architecture.md` addendum — the layering
|
|
108
|
+
contract above is what's fixed, not the driver.
|
|
109
|
+
|
|
110
|
+
### 2.5 Model (`<module>.model.js`)
|
|
111
|
+
The schema/entity definition in whatever the chosen ORM/driver expects, or a
|
|
112
|
+
plain shape comment if the app is schema-less. Read only by the repository.
|
|
113
|
+
|
|
114
|
+
### 2.6 DTO (`<module>.dto.js`)
|
|
115
|
+
Zod schemas for every request shape the module accepts, and for the response
|
|
116
|
+
shape(s) it returns. The `validate(dto)` middleware (§7) runs these against
|
|
117
|
+
`req.body/params/query` before the controller ever sees the request.
|
|
118
|
+
|
|
119
|
+
### 2.7 Errors (`<module>.errors.js`, optional)
|
|
120
|
+
Module-specific subclasses of the shared `AppError` (§6) — e.g.
|
|
121
|
+
`PatientAlreadyDischargedError` — for failures that need a distinct code or
|
|
122
|
+
message beyond the generic `NotFoundError`/`ConflictError`.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## 3. Config & environments
|
|
127
|
+
|
|
128
|
+
`src/config/index.js` is the **only** place `process.env` is read directly
|
|
129
|
+
anywhere in the codebase. It:
|
|
130
|
+
|
|
131
|
+
1. Picks the env file by `NODE_ENV` (`local` → `.env.local`, `uat` →
|
|
132
|
+
`.env.uat`, `production` → `.env.production`), loaded with `dotenv`.
|
|
133
|
+
2. Validates the merged result against a Zod schema — every variable the app
|
|
134
|
+
uses is declared, typed, and either required or given a default.
|
|
135
|
+
3. Exports one frozen config object. Everywhere else imports from here:
|
|
136
|
+
`import { config } from "../config/index.js"`.
|
|
137
|
+
|
|
138
|
+
If validation fails, the process logs the specific missing/invalid keys and
|
|
139
|
+
exits non-zero — it does not start in a half-configured state.
|
|
140
|
+
|
|
141
|
+
`.env.example` is committed with every key the schema declares, set to a
|
|
142
|
+
placeholder value. `.env.local`, `.env.uat`, `.env.production` are gitignored
|
|
143
|
+
and never leave the machine/box they belong to.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## 4. Process management (PM2)
|
|
148
|
+
|
|
149
|
+
`ecosystem.config.js` is committed and defines one app entry per environment
|
|
150
|
+
that box runs, e.g.:
|
|
151
|
+
|
|
152
|
+
```js
|
|
153
|
+
module.exports = {
|
|
154
|
+
apps: [
|
|
155
|
+
{
|
|
156
|
+
name: "api-uat",
|
|
157
|
+
script: "src/server.js",
|
|
158
|
+
env: { NODE_ENV: "uat" },
|
|
159
|
+
instances: 1,
|
|
160
|
+
exec_mode: "fork",
|
|
161
|
+
watch: false,
|
|
162
|
+
autorestart: true,
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
};
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
PM2 supplies `NODE_ENV`; `src/config/index.js` does the rest. PM2 captures
|
|
169
|
+
stdout/stderr for process supervision, but log **content** is Pino's job
|
|
170
|
+
(§5) — PM2 log files are a fallback, not where anyone reads structured logs
|
|
171
|
+
from.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## 5. Logging — Pino, always on
|
|
176
|
+
|
|
177
|
+
`shared/logger/index.js` exports one Pino instance for the whole app:
|
|
178
|
+
|
|
179
|
+
- **Pretty-printed** (`pino-pretty`) only when `NODE_ENV === "local"`.
|
|
180
|
+
Every other environment emits newline-delimited JSON to stdout.
|
|
181
|
+
- **Redacts** `req.headers.authorization`, `req.headers.cookie`, and any
|
|
182
|
+
field named `password`/`token`/`otp` at any depth.
|
|
183
|
+
- Log level from config (`debug` local, `info` uat/production), never
|
|
184
|
+
hardcoded.
|
|
185
|
+
|
|
186
|
+
`shared/middleware/httpLogger.js` wires `pino-http` into `app.js` **before**
|
|
187
|
+
the routes are mounted:
|
|
188
|
+
|
|
189
|
+
```js
|
|
190
|
+
app.use(pinoHttp({ logger, genReqId: (req) => req.headers["x-request-id"] ?? randomUUID() }));
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
This gives one structured line per request — method, path, status, response
|
|
194
|
+
time, and the request id — and that same request id is attached to every
|
|
195
|
+
`req.log.*` call made deeper in the stack (service, repository) and to the
|
|
196
|
+
error response body (§6.2), so a single request's logs and its error can be
|
|
197
|
+
correlated by id. **No module ever calls `console.log`.**
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## 6. Errors
|
|
202
|
+
|
|
203
|
+
### 6.1 Hierarchy
|
|
204
|
+
`shared/errors/AppError.js` is the base class: `{ statusCode, code, message,
|
|
205
|
+
details? }`. Concrete subclasses in the same folder: `NotFoundError` (404),
|
|
206
|
+
`ValidationError` (400), `UnauthorizedError` (401), `ForbiddenError` (403),
|
|
207
|
+
`ConflictError` (409). Modules add their own subclasses (§2.7) only for
|
|
208
|
+
failures the generic set doesn't name.
|
|
209
|
+
|
|
210
|
+
### 6.2 Central handler
|
|
211
|
+
`shared/middleware/errorHandler.js` is the last middleware in `app.js`. Any
|
|
212
|
+
`AppError` maps straight to its status/code/message; any other thrown error
|
|
213
|
+
is logged at `error` level with the request id and returned as a generic 500
|
|
214
|
+
— its internal message is never sent to the client. Response shape:
|
|
215
|
+
|
|
216
|
+
```json
|
|
217
|
+
{ "error": { "code": "NOT_FOUND", "message": "Patient not found", "requestId": "..." } }
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Every service throws; nothing downstream of a controller returns an error
|
|
221
|
+
value for the controller to inspect.
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## 7. Validation
|
|
226
|
+
|
|
227
|
+
`shared/middleware/validate.js` takes a DTO's Zod schema and validates
|
|
228
|
+
`req.body`, `req.params`, and `req.query` before the controller runs,
|
|
229
|
+
throwing a `ValidationError` (with the Zod issue list as `details`) on
|
|
230
|
+
failure. A route with a body, params, or query shape always has a matching
|
|
231
|
+
DTO — a controller reading unvalidated `req.body` directly is a bug, not a
|
|
232
|
+
shortcut.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## 8. API surface
|
|
237
|
+
|
|
238
|
+
### 8.1 Mounting
|
|
239
|
+
`src/routes.js` imports every module's router and mounts it under `/api`:
|
|
240
|
+
|
|
241
|
+
```js
|
|
242
|
+
router.use("/patients", patientRoutes);
|
|
243
|
+
router.use("/auth", authRoutes);
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
A version prefix (`/api/v1`) is added only when the app has already shipped
|
|
247
|
+
a v1 and needs to change it — new apps start unversioned.
|
|
248
|
+
|
|
249
|
+
### 8.2 Response envelope
|
|
250
|
+
Success: `{ "data": <result> }` (a single object, or an array for a list
|
|
251
|
+
endpoint). Paginated lists: `{ "data": [...], "page": { "cursor", "limit",
|
|
252
|
+
"hasMore" } }`. Errors: §6.2's shape. A route never returns a bare array or
|
|
253
|
+
a bare object at the top level — the envelope is constant so a client never
|
|
254
|
+
has to branch on shape.
|
|
255
|
+
|
|
256
|
+
### 8.3 Health check
|
|
257
|
+
`GET /api/health` (no auth) returns `{ "data": { "status": "ok" } }` and is
|
|
258
|
+
wired before any DB-dependent middleware, so it answers even if a downstream
|
|
259
|
+
dependency is degraded — that's what makes it useful to PM2/monitoring.
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## 9. Testing
|
|
264
|
+
|
|
265
|
+
- **Unit tests** live next to the code they test (`<module>.test.js`),
|
|
266
|
+
exercise the service and repository logic directly, and mock across
|
|
267
|
+
layers (a service test mocks its repository; it doesn't hit a database).
|
|
268
|
+
- **Integration tests** live in the top-level `tests/`, boot the real
|
|
269
|
+
`app.js` with `supertest`, and hit it over HTTP against a real (test)
|
|
270
|
+
database — they verify the layers wired together correctly, not business
|
|
271
|
+
logic already covered by a unit test.
|
|
272
|
+
- Both run against `.env.local`'s test counterpart or an in-memory/test
|
|
273
|
+
database — never against `.env.uat` or `.env.production`.
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## 10. Security baseline
|
|
278
|
+
|
|
279
|
+
`app.js` always includes, in this order: `helmet()`, CORS configured from
|
|
280
|
+
`config.corsOrigins` (never `*` outside local), a body size limit, and a rate
|
|
281
|
+
limiter on `/api/auth/*`. Every write route sits behind `auth(...)` unless
|
|
282
|
+
it's explicitly public (health check, login). No raw string-concatenated
|
|
283
|
+
SQL anywhere — parameterized queries or the chosen ORM's query builder only.
|
|
284
|
+
Secrets (DB passwords, JWT signing keys, mail credentials) exist only in the
|
|
285
|
+
gitignored `.env.*` files and in the box's process environment — never in
|
|
286
|
+
`ecosystem.config.js`, never in a committed file.
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## 11. Laws
|
|
291
|
+
|
|
292
|
+
1. **One direction.** Route → controller → service → repository → DB, and
|
|
293
|
+
back. A controller that queries the DB, or a repository that contains an
|
|
294
|
+
`if` deciding business outcome, is a layering violation — fix the layer,
|
|
295
|
+
don't work around it.
|
|
296
|
+
2. **Fail fast on config.** A missing or invalid env var stops boot. It never
|
|
297
|
+
surfaces later as an obscure runtime error.
|
|
298
|
+
3. **No naked errors.** Every thrown error is an `AppError` subclass or gets
|
|
299
|
+
wrapped into one by the central handler before it reaches a client.
|
|
300
|
+
4. **No naked console.** All logging goes through the shared Pino instance;
|
|
301
|
+
`req.log` inside a request, `logger` outside one.
|
|
302
|
+
5. **Every mutation is audited.** A `POST`/`PATCH`/`PUT`/`DELETE` that
|
|
303
|
+
changes domain state writes an audit entry (actor, action, entity,
|
|
304
|
+
before/after or a diff, timestamp) via the `audit` module — this is not
|
|
305
|
+
optional per-module, it's wired once in the shared write path.
|
|
306
|
+
6. **DTOs are mandatory.** A route accepting a body, params, or query has a
|
|
307
|
+
DTO validating it before the controller runs.
|
|
308
|
+
7. **One module, one concern.** Cross-module reads go through the other
|
|
309
|
+
module's service, never its repository or model directly.
|
|
310
|
+
8. **Consistency beats local optimisation.** If a new module wants a shape
|
|
311
|
+
the spec doesn't have, the answer is usually the existing pattern used
|
|
312
|
+
plainly. Extend this document (and every app's copy of it) before
|
|
313
|
+
inventing a one-off.
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## 12. Building it for a new app
|
|
318
|
+
|
|
319
|
+
1. Scaffold the tree in §1 exactly. Nothing above `modules/*` beyond what's
|
|
320
|
+
listed.
|
|
321
|
+
2. Wire `config/index.js` (§3) and confirm the process exits non-zero when a
|
|
322
|
+
required var is missing — check this once, don't assume it.
|
|
323
|
+
3. Wire the shared Pino logger + `pino-http` (§5) into `app.js` before any
|
|
324
|
+
route is mounted, then `helmet`, CORS, and the body parser (§10).
|
|
325
|
+
4. Wire `errorHandler.js` (§6) as the last middleware, after `routes.js`.
|
|
326
|
+
5. Add the `auth` and `audit` modules first — every other module depends on
|
|
327
|
+
both (identity for `auth(...)`, the write path for the audit law).
|
|
328
|
+
6. For each domain module: routes → dto → controller → service →
|
|
329
|
+
repository → model, in that order, so each file only ever imports things
|
|
330
|
+
that already exist.
|
|
331
|
+
7. Add `ecosystem.config.js` (§4) and `.env.example` with every key the
|
|
332
|
+
config schema declares.
|
|
333
|
+
8. Write one integration test per new route in `tests/`, and unit tests for
|
|
334
|
+
the service's business rules in the module.
|
|
335
|
+
9. Confirm: the server boots, `/api/health` returns 200, one request
|
|
336
|
+
produces one structured Pino log line carrying a request id, and a
|
|
337
|
+
deliberately-triggered error returns the §6.2 shape with that same id.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: indira-ui
|
|
3
|
+
description: Design and build screens in the Indira Workspace UI language. Interviews the developer or business team first (who uses it, what they come to do, what must be found in two seconds), turns the answers into a component hierarchy that shows no more data than the task needs, then builds it from the design-system spec. Use for any new page, dashboard, form, list, or internal tool that should look and behave like the BRD Governance Tool.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# indira-ui
|
|
7
|
+
|
|
8
|
+
You are a senior product designer who builds. You work in the **Indira
|
|
9
|
+
Workspace UI** language, specified in `docs/design-system.md` in this
|
|
10
|
+
repository. Read that file completely before doing anything else; every
|
|
11
|
+
number and rule you need is in it, and this skill does not repeat them.
|
|
12
|
+
|
|
13
|
+
Your job has three parts, in order, and you do not skip the first one.
|
|
14
|
+
|
|
15
|
+
## Part 1 — Grill before you draw
|
|
16
|
+
|
|
17
|
+
Do not design from a one-line request. Interview until you can answer every
|
|
18
|
+
question below in the requester's own words. Ask in batches of three or four,
|
|
19
|
+
not one at a time, and stop as soon as the answers are clear — this is a
|
|
20
|
+
discovery, not a form. Use the question tool when one is available; otherwise
|
|
21
|
+
ask in prose and wait.
|
|
22
|
+
|
|
23
|
+
**Who**
|
|
24
|
+
- Who opens this screen, by role? How often — several times a day, once a
|
|
25
|
+
week, once a quarter?
|
|
26
|
+
- What do they already live in all day (Outlook, Excel, a specific ERP)? Their
|
|
27
|
+
habits from that tool are the habits to borrow.
|
|
28
|
+
|
|
29
|
+
**Why they come**
|
|
30
|
+
- What is the *one thing* they most often come here to do? That becomes the
|
|
31
|
+
primary action — the single filled button, top-right.
|
|
32
|
+
- What is the second and third thing? Those are secondary controls or row
|
|
33
|
+
actions, never a second filled button.
|
|
34
|
+
- What question are they trying to answer when they land? ("Is anything
|
|
35
|
+
waiting on me?" "Where is X?") The answer to that question is the first
|
|
36
|
+
thing the screen shows.
|
|
37
|
+
|
|
38
|
+
**What they need to see**
|
|
39
|
+
- For each record shown, what are the three facts they scan for? Everything
|
|
40
|
+
else is a click or a hover away.
|
|
41
|
+
- What do they currently squint at, scroll for, or export to Excel to find?
|
|
42
|
+
- What has been shown on the old screen that nobody uses? (Ask this directly.
|
|
43
|
+
The answer is what you leave out.)
|
|
44
|
+
|
|
45
|
+
**What must not go wrong**
|
|
46
|
+
- Which action is irreversible or expensive? It gets a named, status-coloured
|
|
47
|
+
button and a confirmation with the consequence spelled out.
|
|
48
|
+
- Which data is sensitive to show broadly? It goes behind a hover or a detail
|
|
49
|
+
view, not a list column.
|
|
50
|
+
|
|
51
|
+
**Where it lives**
|
|
52
|
+
- Which sidebar section does it belong under? (Its sidebar item is its name —
|
|
53
|
+
the screen gets no page-title heading.)
|
|
54
|
+
- Is it a list, a detail, a form, or a dashboard? Mixed screens are almost
|
|
55
|
+
always two screens.
|
|
56
|
+
|
|
57
|
+
When the requester says "show everything" or "add a description under each
|
|
58
|
+
field", push back once with the reason (laws 3 and 4 in the spec), then build
|
|
59
|
+
what they decide. When they ask for a second primary action, ask which one a
|
|
60
|
+
newcomer should find first; the other becomes tonal.
|
|
61
|
+
|
|
62
|
+
Write the answers up as a short brief — five lines, not a document — and
|
|
63
|
+
confirm it before Part 2:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
Screen: <name> Under: <sidebar section> Kind: list | detail | form | dashboard
|
|
67
|
+
Users: <roles, frequency> Habitat: <Outlook / Excel / …>
|
|
68
|
+
Landing question: "<…>" Primary action: <verb noun> Secondary: <…>, <…>
|
|
69
|
+
Per-record facts: <a>, <b>, <c> Hidden until hover/click: <…>
|
|
70
|
+
Irreversible: <action> → confirm with "<consequence>"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Part 2 — Component hierarchy
|
|
74
|
+
|
|
75
|
+
Turn the brief into a hierarchy using **only** components from §4 of the spec.
|
|
76
|
+
Write it as an indented tree with the component name, what it shows, and which
|
|
77
|
+
law justifies it. Example shape:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
Shell (§3)
|
|
81
|
+
└─ Content panel
|
|
82
|
+
├─ Toolbar row — law 2: no title; the sidebar names the screen
|
|
83
|
+
│ ├─ Filter chips (.fchip) by status — counts from the full set
|
|
84
|
+
│ └─ [Primary action] filled button — law 1: the one thing, top-right
|
|
85
|
+
├─ Record grid (.brd-card ×N) — state-first list; orb = status
|
|
86
|
+
│ └─ per card: title · dept · who · date · approver stack · progress
|
|
87
|
+
│ (id, priority, status text → title= / sr-only) — law 3
|
|
88
|
+
└─ Empty state — one tonal button
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Checks before you build — answer each with yes:
|
|
92
|
+
|
|
93
|
+
- Is there exactly one filled button, top-right of the content panel?
|
|
94
|
+
- Is there no page-title heading? (A record title on a detail screen is
|
|
95
|
+
content, and allowed.)
|
|
96
|
+
- Does every list row show ≤ 5 facts, and is everything else reachable by
|
|
97
|
+
hover (contact card, tooltip) or click?
|
|
98
|
+
- Is every explanatory sentence gone — replaced by a `title=` tooltip, a
|
|
99
|
+
contact card, or nothing?
|
|
100
|
+
- Is every status expressed as a shape *and* a colour from the six?
|
|
101
|
+
- Do views use segmented tabs and filters use chips, never the other way round?
|
|
102
|
+
- Does every person render through the Person component (hoverable card)?
|
|
103
|
+
- Can a 4-item form or dialog fit a 900px-tall viewport without scrolling its
|
|
104
|
+
chrome?
|
|
105
|
+
- Will nothing overflow at 1180px — column plan for >5 columns, truncate +
|
|
106
|
+
tooltip on single-line cells, clamp-2 on titles?
|
|
107
|
+
- Is the spacing on the 4/8 grid using the recurring numbers in §1.6?
|
|
108
|
+
|
|
109
|
+
If any answer is no, fix the hierarchy before writing code.
|
|
110
|
+
|
|
111
|
+
## Part 3 — Build
|
|
112
|
+
|
|
113
|
+
Build from the spec, not from memory of other design systems.
|
|
114
|
+
|
|
115
|
+
1. Reuse the existing tokens, shell and components in the repository if they
|
|
116
|
+
exist (`client/src/app/index.css`, `client/src/shared/ui`, `client/src/entities/user/ui/Person.jsx`).
|
|
117
|
+
In a fresh project, create them in the order §7 of the spec gives.
|
|
118
|
+
2. Never introduce a colour, radius, shadow, font size or icon outside the
|
|
119
|
+
scales. If the framework lets you type one, the framework is misconfigured
|
|
120
|
+
(§1.5: delete the defaults).
|
|
121
|
+
3. Use state layers for hover/press, never a second colour.
|
|
122
|
+
4. Skeletons in the shape of the content while loading; a spinner only inside
|
|
123
|
+
a working control.
|
|
124
|
+
5. Every clickable person is `<Person>`; every avatar is `<Avatar>` (photo
|
|
125
|
+
when present, tinted initials otherwise).
|
|
126
|
+
6. Tables over five columns get a `<colgroup>` plan and `table-layout: fixed`.
|
|
127
|
+
7. Finish with an overflow audit at 1440 and 1180 (scan for text wider than
|
|
128
|
+
its box, clipped without ellipsis, or past the viewport) and a screenshot
|
|
129
|
+
of each new screen. Report zero offenders, or fix until it is zero.
|
|
130
|
+
|
|
131
|
+
## What "done" looks like
|
|
132
|
+
|
|
133
|
+
Give back, in this order: the confirmed brief, the hierarchy tree, the
|
|
134
|
+
screens built, the audit result, and a two-line note of any place the request
|
|
135
|
+
was pushed back on and what was decided. If a requested element was left out
|
|
136
|
+
for a law, name the law — the requester should be able to overrule it
|
|
137
|
+
knowingly, not discover it missing.
|
|
138
|
+
|
|
139
|
+
## Things you never do
|
|
140
|
+
|
|
141
|
+
- Add a page-name heading, a hero, a gradient, an emoji icon, or a second
|
|
142
|
+
filled button.
|
|
143
|
+
- Write a sentence of help text under a control.
|
|
144
|
+
- Show an id, a status word and a status colour for the same record in the
|
|
145
|
+
same place.
|
|
146
|
+
- Invent a colour for a new status. Six statuses; a seventh is a product
|
|
147
|
+
decision, not a design one.
|
|
148
|
+
- Use a spinner for page content, or animate anything on a loop.
|
|
149
|
+
- Design a screen for a user you have not asked about.
|