Fill gaps in shared pattern contracts
- modal-workflows: full state machine, htmx pattern, validation rules - go-api: REST conventions, URL naming, status codes, error format, list response - import-export: streaming export 3-layer architecture with Go example - CLAUDE.template.md: updated to include modals and REST API references Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,57 +1,55 @@
|
|||||||
# {{ .project_name }} — Instructions for Claude
|
# {{ .project_name }} — Instructions for Claude
|
||||||
|
|
||||||
Read and follow the project Bible before making any changes:
|
## Shared Engineering Rules
|
||||||
|
Read `bible/` — shared rules for all projects (CSV, logging, DB, tables, background tasks, code style).
|
||||||
|
Start with `bible/kit/patterns/` for specific contracts.
|
||||||
|
|
||||||
**[`bible/README.md`](bible/README.md)**
|
## Project Architecture
|
||||||
|
Read `bible-local/` — project-specific architecture.
|
||||||
The Bible is the single source of truth for architecture, data models, API contracts, and UI
|
Every architectural decision specific to this project must be recorded in `bible-local/`.
|
||||||
pattern conventions. Every significant architectural decision must be recorded in the Bible
|
|
||||||
decision log before or alongside the code change.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Shared Engineering Rules
|
## Quick Reference (full contracts in `bible/kit/patterns/`)
|
||||||
|
|
||||||
The following rules apply to ALL changes in this project.
|
### Go Code Style (`go-code-style/contract.md`)
|
||||||
They are maintained centrally in `tools/ui-design-code/kit/patterns/`.
|
- Handler → Service → Repository. No SQL in handlers, no HTTP writes in services.
|
||||||
|
- Errors: `fmt.Errorf("context: %w", err)`. Never discard with `_`.
|
||||||
|
- `gofmt` before every commit.
|
||||||
|
- Thresholds and status logic on the server — UI only reflects what server returns.
|
||||||
|
|
||||||
### Go Code Style
|
### Logging (`go-logging/contract.md`)
|
||||||
See [`tools/ui-design-code/kit/patterns/go-code-style/contract.md`](tools/ui-design-code/kit/patterns/go-code-style/contract.md)
|
- `slog`, stdout/stderr only. Never `console.log` as substitute for server logging.
|
||||||
- Handler → Service → Repository layering
|
- Always log: startup, task start/finish/error, export row counts, ingest results, any 500.
|
||||||
- Error wrapping with `fmt.Errorf("...: %w", err)`
|
|
||||||
- Startup sequence: connect DB → migrate → start server
|
|
||||||
- Business logic and status thresholds live on the server, not in JS
|
|
||||||
|
|
||||||
### Logging
|
### Database (`go-database/contract.md`)
|
||||||
See [`tools/ui-design-code/kit/patterns/go-logging/contract.md`](tools/ui-design-code/kit/patterns/go-logging/contract.md)
|
- **CRITICAL**: never run SQL on the same tx while iterating a cursor. Two-phase: read all → close → write.
|
||||||
- Use `slog`, log to binary stdout/stderr only
|
- Soft delete via `is_active = false`.
|
||||||
- Never use `console.log` as a substitute for server-side logging
|
- Fail-fast DB ping before starting HTTP server.
|
||||||
- Always log: startup, background task start/finish/error, export row counts, ingest results
|
- No N+1: use JOINs or batch `WHERE id IN (...)`.
|
||||||
|
- GORM: `gorm:"-"` = fully ignored; `gorm:"-:migration"` = skip migration only.
|
||||||
|
|
||||||
### Database
|
### REST API (`go-api/contract.md`)
|
||||||
See [`tools/ui-design-code/kit/patterns/go-database/contract.md`](tools/ui-design-code/kit/patterns/go-database/contract.md)
|
- Plural nouns: `/api/assets`, `/api/components`.
|
||||||
- **CRITICAL**: never execute SQL on the same tx while iterating a cursor — two-phase only
|
- Never `200 OK` for errors — use `422` for validation, `404`, `500`.
|
||||||
- Soft delete via `is_active = false`, not physical deletes
|
- Error body: `{"error": "message", "fields": {"field": "reason"}}`.
|
||||||
- Fail-fast DB check before starting the HTTP server
|
- List response always includes `total_count`, `page`, `per_page`, `total_pages`.
|
||||||
- No N+1 queries: use JOINs or batch `IN` queries
|
- `/health` and `/api/db-status` required in every app.
|
||||||
|
|
||||||
### Background Tasks
|
### Background Tasks (`go-background-tasks/contract.md`)
|
||||||
See [`tools/ui-design-code/kit/patterns/go-background-tasks/contract.md`](tools/ui-design-code/kit/patterns/go-background-tasks/contract.md)
|
- Slow ops (>300ms): POST → `{task_id}` → client polls `/api/tasks/:id`.
|
||||||
- All slow operations: POST → task_id → client polls `/api/tasks/:id`
|
- No SSE. Polling only. Return `202 Accepted`.
|
||||||
- No SSE — polling only
|
|
||||||
- Return `202 Accepted` when task is created
|
|
||||||
|
|
||||||
### Tables, Filtering, Pagination
|
### Tables, Filtering, Pagination (`table-management/contract.md`)
|
||||||
See [`tools/ui-design-code/kit/patterns/table-management/contract.md`](tools/ui-design-code/kit/patterns/table-management/contract.md)
|
- Server-side only. Filter state in URL params. Filter resets to page 1.
|
||||||
- Server-side filtering and pagination only
|
- Display: "51–100 из 342".
|
||||||
- Filter state in URL query params; applying a filter resets to page 1
|
|
||||||
- Response must include `total_count`, `page`, `per_page`, `total_pages`
|
|
||||||
- Display format: "51–100 из 342"
|
|
||||||
|
|
||||||
### CSV Export
|
### Modals (`modal-workflows/contract.md`)
|
||||||
See [`tools/ui-design-code/kit/patterns/import-export/contract.md`](tools/ui-design-code/kit/patterns/import-export/contract.md)
|
- States: open → submitting → success | error.
|
||||||
- UTF-8 BOM required (`\xEF\xBB\xBF`)
|
- Destructive actions require confirmation modal naming the target.
|
||||||
- Semicolon delimiter (`;`), not comma
|
- Never close on error. Use `422` for validation errors in htmx flows.
|
||||||
- Decimal separator: comma (`1 234,56`), not period
|
|
||||||
- Dates as `DD.MM.YYYY`
|
### CSV Export (`import-export/contract.md`)
|
||||||
- `csv.Writer` with `Comma = ';'`
|
- BOM: `\xEF\xBB\xBF`. Delimiter: `;`. Decimal: `,` (`1 234,56`). Dates: `DD.MM.YYYY`.
|
||||||
|
- Stream via callback — never load all rows into memory.
|
||||||
|
- Always call `w.Flush()` after the loop.
|
||||||
|
|||||||
91
kit/patterns/go-api/contract.md
Normal file
91
kit/patterns/go-api/contract.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# Contract: REST API Conventions (Go Web Applications)
|
||||||
|
|
||||||
|
## URL Naming
|
||||||
|
|
||||||
|
- Resources are plural nouns: `/api/assets`, `/api/components`, `/api/pricelists`.
|
||||||
|
- Nested resources use parent path: `/api/assets/:id/components`.
|
||||||
|
- Actions that are not CRUD use a verb suffix: `/api/pricelists/:id/export-csv`,
|
||||||
|
`/api/tasks/:id/cancel`, `/api/sync/push`.
|
||||||
|
- No verbs in resource paths: use `/api/assets` + DELETE, not `/api/delete-asset`.
|
||||||
|
|
||||||
|
## HTTP Methods and Status Codes
|
||||||
|
|
||||||
|
| Operation | Method | Success | Notes |
|
||||||
|
|-----------|--------|---------|-------|
|
||||||
|
| List | GET | 200 | |
|
||||||
|
| Get one | GET | 200 / 404 | |
|
||||||
|
| Create | POST | 201 | Return created resource |
|
||||||
|
| Update | PUT / PATCH | 200 | Return updated resource |
|
||||||
|
| Delete / Archive | DELETE | 200 or 204 | |
|
||||||
|
| Async action | POST | 202 | Return `{task_id}` |
|
||||||
|
| Validation error | POST/PUT | 422 | Return field errors |
|
||||||
|
| Server error | any | 500 | Log full error server-side |
|
||||||
|
| Not found | any | 404 | |
|
||||||
|
| Unauthorized | any | 401 | |
|
||||||
|
|
||||||
|
- Never return `200 OK` for validation errors — use `422 Unprocessable Entity`.
|
||||||
|
- Never return `200 OK` with `{"error": "..."}` in the body — use the correct status code.
|
||||||
|
|
||||||
|
## Error Response Format
|
||||||
|
|
||||||
|
All non-2xx responses return a consistent JSON body:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "human-readable message",
|
||||||
|
"fields": {
|
||||||
|
"serial_number": "Serial number is required",
|
||||||
|
"price": "Must be greater than 0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `error` — always present, describes what went wrong.
|
||||||
|
- `fields` — optional, present only for validation errors (422). Keys match form field names.
|
||||||
|
- Never expose raw Go error strings, stack traces, or SQL errors to the client.
|
||||||
|
|
||||||
|
## List Response Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [...],
|
||||||
|
"total_count": 342,
|
||||||
|
"page": 2,
|
||||||
|
"per_page": 50,
|
||||||
|
"total_pages": 7
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Always include pagination metadata even if the client did not request it.
|
||||||
|
- `items` is always an array — never `null`, use `[]` for empty.
|
||||||
|
|
||||||
|
## Request Conventions
|
||||||
|
|
||||||
|
- Accept `application/json` for API endpoints.
|
||||||
|
- HTML form submissions (htmx) use `application/x-www-form-urlencoded` or `multipart/form-data`.
|
||||||
|
- File uploads use `multipart/form-data`.
|
||||||
|
- Query parameters for filtering and pagination: `?page=1&per_page=50&search=abc&status=active`.
|
||||||
|
|
||||||
|
## Health and Utility Endpoints
|
||||||
|
|
||||||
|
Every application must expose:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /health → 200 {"status": "ok"}
|
||||||
|
GET /api/db-status → 200 {"ok": true} or 500 {"ok": false, "error": "..."}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `/health` must respond even if DB is down (for load balancer probes).
|
||||||
|
- `/api/db-status` checks the actual DB connection and returns its state.
|
||||||
|
|
||||||
|
## Async Actions
|
||||||
|
|
||||||
|
For long-running operations return immediately with a task reference:
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/pricelists/create → 202 {"task_id": "abc123"}
|
||||||
|
GET /api/tasks/abc123 → 200 {"status": "running", "progress": 42, "message": "Processing..."}
|
||||||
|
GET /api/tasks/abc123 → 200 {"status": "success", "result": {...}}
|
||||||
|
```
|
||||||
|
|
||||||
|
See `go-background-tasks/contract.md` for full task contract.
|
||||||
@@ -71,8 +71,53 @@ fmt.Sprintf("%.2f", price)
|
|||||||
- Escape embedded double-quotes by doubling them: `""`.
|
- Escape embedded double-quotes by doubling them: `""`.
|
||||||
- Use `encoding/csv` with `csv.Writer` and set `csv.Writer.Comma = ';'`; it handles quoting automatically.
|
- Use `encoding/csv` with `csv.Writer` and set `csv.Writer.Comma = ';'`; it handles quoting automatically.
|
||||||
|
|
||||||
|
## Streaming Export Architecture (Go)
|
||||||
|
|
||||||
|
For exports with potentially large row counts use a 3-layer streaming pattern.
|
||||||
|
Never load all rows into memory before writing — stream directly to the response writer.
|
||||||
|
|
||||||
|
```
|
||||||
|
Handler → sets HTTP headers + writes BOM → calls Service
|
||||||
|
Service → delegates to Repository with a row callback
|
||||||
|
Repository → queries in batches → calls callback per row
|
||||||
|
Handler/Service → csv.Writer.Flush() after all rows
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Handler
|
||||||
|
func ExportCSV(c *gin.Context) {
|
||||||
|
c.Header("Content-Type", "text/csv; charset=utf-8")
|
||||||
|
c.Header("Content-Disposition", `attachment; filename="export.csv"`)
|
||||||
|
c.Writer.Write([]byte{0xEF, 0xBB, 0xBF}) // BOM
|
||||||
|
|
||||||
|
w := csv.NewWriter(c.Writer)
|
||||||
|
w.Comma = ';'
|
||||||
|
w.Write([]string{"ID", "Name", "Price"}) // header row
|
||||||
|
|
||||||
|
err := svc.StreamRows(ctx, filters, func(row Row) error {
|
||||||
|
return w.Write([]string{row.ID, row.Name, formatPrice(row.Price)})
|
||||||
|
})
|
||||||
|
w.Flush()
|
||||||
|
if err != nil {
|
||||||
|
// headers already sent — log only, cannot change status
|
||||||
|
slog.Error("csv export failed mid-stream", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repository — batch fetch with callback
|
||||||
|
func (r *Repo) StreamRows(ctx, filters, fn func(Row) error) error {
|
||||||
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
||||||
|
// ... scan and call fn(row) for each row
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Use `JOIN` in the repository query to avoid N+1 per row.
|
||||||
|
- Batch size is optional; streaming row-by-row is fine for most datasets.
|
||||||
|
- Always call `w.Flush()` after the loop — `csv.Writer` buffers internally.
|
||||||
|
|
||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
- Import errors should map to clear user-facing messages.
|
- Import errors should map to clear user-facing messages.
|
||||||
- Export errors after streaming starts must degrade gracefully (human-readable fallback).
|
- Export errors after streaming starts must be logged server-side only — HTTP headers are already
|
||||||
|
sent and the status code cannot be changed mid-stream.
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,56 @@
|
|||||||
# Contract: Modal Workflows
|
# Contract: Modal Workflows
|
||||||
|
|
||||||
## Shared Rules
|
## State Machine
|
||||||
|
|
||||||
- Destructive actions require explicit confirmation.
|
Every modal has exactly these states:
|
||||||
- Validation and backend errors are rendered in human-readable form.
|
|
||||||
- UI state transitions are explicit (`open` / `submit` / `success` / `error` / `cancel`).
|
|
||||||
- API contracts and UI copy should be documented in the host project's Bible.
|
|
||||||
|
|
||||||
|
```
|
||||||
|
closed → open → submitting → success | error
|
||||||
|
↓
|
||||||
|
cancel → closed
|
||||||
|
```
|
||||||
|
|
||||||
|
- `open`: form visible, submit enabled.
|
||||||
|
- `submitting`: form disabled, spinner on submit button, no double-submit possible.
|
||||||
|
- `success`: close modal, show toast, refresh affected data.
|
||||||
|
- `error`: stay open, show error message inline, re-enable form.
|
||||||
|
- `cancel`: close without changes, no confirmation needed unless form is dirty.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- Destructive actions (delete, archive, bulk remove) require a separate confirmation modal — not
|
||||||
|
just a disabled button. Confirmation modal must name the target: "Delete component ABC-123?"
|
||||||
|
- Never close a modal automatically on error — keep it open so the user can retry or copy the error.
|
||||||
|
- Submit button text must describe the action: "Save", "Delete", "Archive" — not "OK" or "Confirm".
|
||||||
|
- Modal title must match the action: "Edit Component", "Delete Asset" — not generic "Modal".
|
||||||
|
- Escape key and clicking the backdrop close the modal (unless in `submitting` state).
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- Validate on submit server-side. Client-side validation is optional progressive enhancement only.
|
||||||
|
- Show field-level errors inline below each field.
|
||||||
|
- Show a form-level error summary at the top if multiple fields fail.
|
||||||
|
- Error messages must be human-readable and action-oriented: "Serial number is required" — not
|
||||||
|
"serial_number: cannot be null".
|
||||||
|
|
||||||
|
## htmx Pattern (server-rendered modals)
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/entity → 200 OK + HX-Trigger: "entitySaved" (success)
|
||||||
|
→ 422 Unprocessable + partial HTML (validation error, re-render form)
|
||||||
|
→ 500 + error message (server error)
|
||||||
|
```
|
||||||
|
|
||||||
|
- On success: server sends `HX-Trigger` header, JS listener closes modal and refreshes list.
|
||||||
|
- On validation error: server re-renders the form partial with inline errors (422).
|
||||||
|
- On server error: show generic error toast, log full error server-side.
|
||||||
|
- Do not use `200 OK` for validation errors — use `422` so htmx can differentiate.
|
||||||
|
|
||||||
|
## Multi-Step Modals
|
||||||
|
|
||||||
|
Use only when the workflow genuinely requires staged input (e.g. import preview → confirm).
|
||||||
|
|
||||||
|
- Show a step indicator (Step 1 of 3).
|
||||||
|
- Back button must restore previous step values.
|
||||||
|
- Final confirm step must summarise what will happen before the destructive/irreversible action.
|
||||||
|
- Single-step edits must NOT be split into multi-step without good reason.
|
||||||
|
|||||||
Reference in New Issue
Block a user