From a37aec8790fd106cbe22757a155bbd85cd221064 Mon Sep 17 00:00:00 2001 From: Michael Chus Date: Sun, 1 Mar 2026 16:47:52 +0300 Subject: [PATCH] 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 --- kit/ai/claude/CLAUDE.template.md | 86 +++++++++++----------- kit/patterns/go-api/contract.md | 91 ++++++++++++++++++++++++ kit/patterns/import-export/contract.md | 47 +++++++++++- kit/patterns/modal-workflows/contract.md | 57 +++++++++++++-- 4 files changed, 231 insertions(+), 50 deletions(-) create mode 100644 kit/patterns/go-api/contract.md diff --git a/kit/ai/claude/CLAUDE.template.md b/kit/ai/claude/CLAUDE.template.md index df66f5d..228687a 100644 --- a/kit/ai/claude/CLAUDE.template.md +++ b/kit/ai/claude/CLAUDE.template.md @@ -1,57 +1,55 @@ # {{ .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)** - -The Bible is the single source of truth for architecture, data models, API contracts, and UI -pattern conventions. Every significant architectural decision must be recorded in the Bible -decision log before or alongside the code change. +## Project Architecture +Read `bible-local/` — project-specific architecture. +Every architectural decision specific to this project must be recorded in `bible-local/`. --- -## Shared Engineering Rules +## Quick Reference (full contracts in `bible/kit/patterns/`) -The following rules apply to ALL changes in this project. -They are maintained centrally in `tools/ui-design-code/kit/patterns/`. +### Go Code Style (`go-code-style/contract.md`) +- 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 -See [`tools/ui-design-code/kit/patterns/go-code-style/contract.md`](tools/ui-design-code/kit/patterns/go-code-style/contract.md) -- Handler → Service → Repository layering -- 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 (`go-logging/contract.md`) +- `slog`, stdout/stderr only. Never `console.log` as substitute for server logging. +- Always log: startup, task start/finish/error, export row counts, ingest results, any 500. -### Logging -See [`tools/ui-design-code/kit/patterns/go-logging/contract.md`](tools/ui-design-code/kit/patterns/go-logging/contract.md) -- Use `slog`, log to binary stdout/stderr only -- Never use `console.log` as a substitute for server-side logging -- Always log: startup, background task start/finish/error, export row counts, ingest results +### Database (`go-database/contract.md`) +- **CRITICAL**: never run SQL on the same tx while iterating a cursor. Two-phase: read all → close → write. +- Soft delete via `is_active = false`. +- Fail-fast DB ping before starting HTTP server. +- No N+1: use JOINs or batch `WHERE id IN (...)`. +- GORM: `gorm:"-"` = fully ignored; `gorm:"-:migration"` = skip migration only. -### Database -See [`tools/ui-design-code/kit/patterns/go-database/contract.md`](tools/ui-design-code/kit/patterns/go-database/contract.md) -- **CRITICAL**: never execute SQL on the same tx while iterating a cursor — two-phase only -- Soft delete via `is_active = false`, not physical deletes -- Fail-fast DB check before starting the HTTP server -- No N+1 queries: use JOINs or batch `IN` queries +### REST API (`go-api/contract.md`) +- Plural nouns: `/api/assets`, `/api/components`. +- Never `200 OK` for errors — use `422` for validation, `404`, `500`. +- Error body: `{"error": "message", "fields": {"field": "reason"}}`. +- List response always includes `total_count`, `page`, `per_page`, `total_pages`. +- `/health` and `/api/db-status` required in every app. -### Background Tasks -See [`tools/ui-design-code/kit/patterns/go-background-tasks/contract.md`](tools/ui-design-code/kit/patterns/go-background-tasks/contract.md) -- All slow operations: POST → task_id → client polls `/api/tasks/:id` -- No SSE — polling only -- Return `202 Accepted` when task is created +### Background Tasks (`go-background-tasks/contract.md`) +- Slow ops (>300ms): POST → `{task_id}` → client polls `/api/tasks/:id`. +- No SSE. Polling only. Return `202 Accepted`. -### Tables, Filtering, Pagination -See [`tools/ui-design-code/kit/patterns/table-management/contract.md`](tools/ui-design-code/kit/patterns/table-management/contract.md) -- Server-side filtering and pagination only -- 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" +### Tables, Filtering, Pagination (`table-management/contract.md`) +- Server-side only. Filter state in URL params. Filter resets to page 1. +- Display: "51–100 из 342". -### CSV Export -See [`tools/ui-design-code/kit/patterns/import-export/contract.md`](tools/ui-design-code/kit/patterns/import-export/contract.md) -- UTF-8 BOM required (`\xEF\xBB\xBF`) -- Semicolon delimiter (`;`), not comma -- Decimal separator: comma (`1 234,56`), not period -- Dates as `DD.MM.YYYY` -- `csv.Writer` with `Comma = ';'` +### Modals (`modal-workflows/contract.md`) +- States: open → submitting → success | error. +- Destructive actions require confirmation modal naming the target. +- Never close on error. Use `422` for validation errors in htmx flows. + +### CSV Export (`import-export/contract.md`) +- 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. diff --git a/kit/patterns/go-api/contract.md b/kit/patterns/go-api/contract.md new file mode 100644 index 0000000..fdff1d1 --- /dev/null +++ b/kit/patterns/go-api/contract.md @@ -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. diff --git a/kit/patterns/import-export/contract.md b/kit/patterns/import-export/contract.md index 68677d4..b5065ab 100644 --- a/kit/patterns/import-export/contract.md +++ b/kit/patterns/import-export/contract.md @@ -71,8 +71,53 @@ fmt.Sprintf("%.2f", price) - Escape embedded double-quotes by doubling them: `""`. - 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 - 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. diff --git a/kit/patterns/modal-workflows/contract.md b/kit/patterns/modal-workflows/contract.md index c18cea1..f56baad 100644 --- a/kit/patterns/modal-workflows/contract.md +++ b/kit/patterns/modal-workflows/contract.md @@ -1,9 +1,56 @@ # Contract: Modal Workflows -## Shared Rules +## State Machine -- Destructive actions require explicit confirmation. -- 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. +Every modal has exactly these states: +``` +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.