@nebulit/embuilder 0.1.46 → 0.1.47
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/package.json +1 -1
- package/templates/.claude/skills/slice-automation/SKILL.md +237 -35
- package/templates/backend/AGENTS.md +531 -0
- package/templates/frontend/prompt.md +8 -7
- package/templates/frontend/setup-env.sh +0 -0
- package/templates/frontend/src/App.tsx +2 -1
- package/templates/frontend/src/contexts/AuthContext.tsx +6 -5
- package/templates/server.mjs +2 -1
package/package.json
CHANGED
|
@@ -1,49 +1,251 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
3
|
-
description:
|
|
2
|
+
name: slice-automation
|
|
3
|
+
description: builds an automation slice from an event model
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
### Critical understanding
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Make sure to read the Agents.md file before building anything.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
If the processors-Array is not empty, it´s an automation slice.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Important - an automation slice is "just" a state-change slice with an additional automation that triggers the command.
|
|
13
|
+
So also read the skill for 'state-change-slice'
|
|
13
14
|
|
|
14
|
-
##
|
|
15
|
+
## Critical Requirements
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
### Restaurant ID Requirement
|
|
18
|
+
- **CRITICAL**: ALL events MUST have `restaurantId` in their metadata (camelCase)
|
|
19
|
+
- **NEVER** use `locationId` or `location_id` - these are outdated and forbidden
|
|
20
|
+
- **CRITICAL**: ALL database tables (including TODO lists) MUST have a `restaurant_id` column (snake_case)
|
|
21
|
+
- This ensures proper multi-tenancy and data isolation
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
- Look for slices with `"sliceType": "AUTOMATION"`
|
|
22
|
-
- Extract their IDs and titles
|
|
23
|
+
## Overview
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
- Or accept slice IDs from the user's original request
|
|
25
|
+
Automations are processes that happen in the background, based on a TODO List.
|
|
26
|
+
TODO Lists are always tables (read models) and each row in the table is a TODO Item.
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
The automation is only responsible to:
|
|
29
|
+
1. Fetch items from the TODO List (read model)
|
|
30
|
+
2. Fire the command for each item
|
|
31
|
+
3. Repeat on a schedule using CRON
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
## Architecture Pattern
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
┌─────────────────┐
|
|
37
|
+
│ TODO List │ (Read Model - Inbound Dependency)
|
|
38
|
+
│ "items to do" │ Example: "clerks_to_invite", "items_to_fetch"
|
|
39
|
+
└────────┬────────┘
|
|
40
|
+
│
|
|
41
|
+
│ reads
|
|
42
|
+
│
|
|
43
|
+
┌────────▼────────┐
|
|
44
|
+
│ processor.ts │ (CRON scheduled automation)
|
|
45
|
+
│ │ - Fetches TODO items
|
|
46
|
+
│ │ - Fires commands
|
|
47
|
+
└────────┬────────┘
|
|
48
|
+
│
|
|
49
|
+
│ invokes
|
|
50
|
+
│
|
|
51
|
+
┌────────▼────────┐
|
|
52
|
+
│ CommandHandler │ (State-change logic)
|
|
53
|
+
│ Command.ts │ - decide() function
|
|
54
|
+
│ routes.ts │ - evolve() function
|
|
55
|
+
└─────────────────┘
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Implementation Structure
|
|
59
|
+
|
|
60
|
+
An automation slice consists of:
|
|
61
|
+
1. **processor.ts** - CRON automation that fetches TODO items and fires commands
|
|
62
|
+
2. **[Command]Command.ts** - Command handler with decide/evolve logic
|
|
63
|
+
3. **routes.ts** - HTTP API endpoint for manual command invocation
|
|
64
|
+
|
|
65
|
+
## Processor Configuration
|
|
66
|
+
|
|
67
|
+
Two patterns exist in the codebase:
|
|
68
|
+
|
|
69
|
+
### Pattern 1: Using startProcessor helper (Recommended)
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import {ProcessorConfig, ProcessorTodoItem, startProcessor} from "../../process/process";
|
|
73
|
+
import {handleYourCommand} from "./YourCommandCommand";
|
|
74
|
+
|
|
75
|
+
export type ItemToProcess = {
|
|
76
|
+
itemId: string,
|
|
77
|
+
// other fields from read model
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const config: ProcessorConfig = {
|
|
81
|
+
schedule: "*/5 * * * * *", // Every 5 seconds (cron format)
|
|
82
|
+
endpoint: "your-todo-list-collection", // Read model endpoint
|
|
83
|
+
query: {
|
|
84
|
+
"status": "OPEN", // Filter criteria
|
|
85
|
+
"_limit": "1" // Process one at a time
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const handler = async (item: ItemToProcess & ProcessorTodoItem) => {
|
|
90
|
+
console.log(`Processing item: ${item.itemId}`)
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
await handleYourCommand(`aggregate-${item.itemId}`, {
|
|
94
|
+
type: "YourCommand",
|
|
95
|
+
data: {
|
|
96
|
+
itemId: item.itemId,
|
|
97
|
+
// map other fields
|
|
98
|
+
},
|
|
99
|
+
metadata: {}
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
console.log(`Successfully processed item: ${item.itemId}`)
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error(`Error processing item ${item.itemId}:`, error)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const processor = {
|
|
109
|
+
start: () => {
|
|
110
|
+
console.log("[YourProcessor] Starting processor...")
|
|
111
|
+
startProcessor<ItemToProcess>(config, handler)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Pattern 2: Direct Supabase query (Legacy)
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import {ProcessorConfig} from "../../process/process";
|
|
120
|
+
import {YourCommand, handleYourCommand} from "./YourCommandCommand";
|
|
121
|
+
import cron from "node-cron";
|
|
122
|
+
import {createServiceClient} from "../../supabase/api";
|
|
123
|
+
|
|
124
|
+
const config: ProcessorConfig = {
|
|
125
|
+
schedule: '*/30 * * * * *', // Every 30 seconds
|
|
126
|
+
endpoint: "your_todo_table", // Supabase table name
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const processor = {
|
|
130
|
+
start: () => {
|
|
131
|
+
cron.schedule(config.schedule, async () => {
|
|
132
|
+
console.log("Running process")
|
|
133
|
+
let client = createServiceClient()
|
|
134
|
+
let result = await client.from(config.endpoint).select("*")
|
|
135
|
+
|
|
136
|
+
if (result.count == 0) {
|
|
137
|
+
console.log(`Nothing to do for ${config.endpoint}`)
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const item of result.data ?? []) {
|
|
142
|
+
const command: YourCommand = {
|
|
143
|
+
type: "YourCommand",
|
|
144
|
+
data: {
|
|
145
|
+
itemId: item.itemId!
|
|
146
|
+
},
|
|
147
|
+
metadata: {}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const id = item.itemId
|
|
151
|
+
if (!id) {
|
|
152
|
+
throw `Cannot process Command ${command.type}. No Id available.`
|
|
153
|
+
}
|
|
154
|
+
await handleYourCommand(id, command)
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## CRON Schedule Format
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
┌───────────── second (0-59)
|
|
165
|
+
│ ┌─────────── minute (0-59)
|
|
166
|
+
│ │ ┌───────── hour (0-23)
|
|
167
|
+
│ │ │ ┌─────── day of month (1-31)
|
|
168
|
+
│ │ │ │ ┌───── month (1-12)
|
|
169
|
+
│ │ │ │ │ ┌─── day of week (0-7)
|
|
170
|
+
│ │ │ │ │ │
|
|
171
|
+
* * * * * *
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Common schedules:
|
|
175
|
+
- `*/5 * * * * *` - Every 5 seconds
|
|
176
|
+
- `*/30 * * * * *` - Every 30 seconds
|
|
177
|
+
- `0 */1 * * * *` - Every minute
|
|
178
|
+
- `0 0 * * * *` - Every hour
|
|
179
|
+
|
|
180
|
+
## Real Examples from Codebase
|
|
181
|
+
|
|
182
|
+
### Example 1: ConfirmInvitation (Clerk management)
|
|
183
|
+
|
|
184
|
+
**TODO List Read Model**: `clerks_to_invite` table
|
|
185
|
+
**Automation**: `src/slices/ConfirmInvitation/processor.ts`
|
|
186
|
+
**Command**: `ConfirmInvitationCommand`
|
|
187
|
+
**Event Emitted**: `ClerkInvitationConfirmed`
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
// processor.ts
|
|
191
|
+
const config: ProcessorConfig = {
|
|
192
|
+
schedule: '*/30 * * * * *',
|
|
193
|
+
endpoint: "clerks_to_invite",
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Fetches clerks from TODO list and confirms their invitations
|
|
197
|
+
for (const clerk of result.data ?? []) {
|
|
198
|
+
const command: ConfirmInvitationCommand = {
|
|
199
|
+
type: "ConfirmInvitation",
|
|
200
|
+
data: {
|
|
201
|
+
clerkId: clerk.clerkId!
|
|
202
|
+
},
|
|
203
|
+
metadata: {}
|
|
204
|
+
}
|
|
205
|
+
await handleConfirmInvitation(clerk.clerkId, command)
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
in each slice folder, generate a file .slice.json
|
|
210
|
+
```
|
|
211
|
+
{
|
|
212
|
+
"id" : "<slice id>",
|
|
213
|
+
"slice": "<slice title>",
|
|
214
|
+
"context": "<contextx>",
|
|
215
|
+
"link": "https://miro.com/app/board/<board-id>=/?moveToWidget=<slice id>"
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Key Points
|
|
220
|
+
|
|
221
|
+
1. **TODO List Naming**: Use format `<things>_to_<action>` (e.g., `clerks_to_invite`, `items_to_fetch`)
|
|
222
|
+
2. **Read Model Endpoint**: TODO list is accessed via `/api/query/<name>-collection` endpoint
|
|
223
|
+
3. **Processor Types**: Define TypeScript types for TODO items matching read model structure
|
|
224
|
+
4. **Error Handling**: Always wrap command execution in try-catch, log errors but continue processing
|
|
225
|
+
5. **Schedule Wisely**: Balance responsiveness vs system load (30 seconds is common for production)
|
|
226
|
+
6. **Status Management**: TODO list typically has a `status` field ("OPEN", "PROCESSING", "DONE")
|
|
227
|
+
7. **Limit Processing**: Use `_limit: "1"` to process one item at a time, preventing concurrent issues
|
|
228
|
+
8. **Stream Naming**: Use aggregate-based stream names (e.g., `catalogue-${itemId}`)
|
|
229
|
+
|
|
230
|
+
## Implementation Steps
|
|
231
|
+
|
|
232
|
+
1. **Read the input JSON** from templates/sample-input.json
|
|
233
|
+
2. **Create processor.ts** following one of the patterns above
|
|
234
|
+
3. **Implement state-change slice** using the 'state-change-slice' skill
|
|
235
|
+
- Creates `[Command]Command.ts` with decide/evolve functions
|
|
236
|
+
- No `routes.ts` for Automations.
|
|
237
|
+
4. **Register processor** in main application startup
|
|
238
|
+
5. **Ensure TODO List exists** as a read model (separate slice handles this)
|
|
239
|
+
|
|
240
|
+
## Sample Input Structure
|
|
241
|
+
|
|
242
|
+
Sample input: read 'templates/sample-input.json'
|
|
243
|
+
|
|
244
|
+
The input defines:
|
|
245
|
+
- **processors[]**: Array of automation definitions
|
|
246
|
+
- title: Automation name
|
|
247
|
+
- dependencies: Inbound (TODO list) and Outbound (Command) connections
|
|
248
|
+
- **commands[]**: Commands triggered by the automation
|
|
249
|
+
- **events[]**: Events emitted by the commands
|
|
38
250
|
|
|
39
|
-
## Important Notes
|
|
40
251
|
|
|
41
|
-
- The generator is located in `.claude/skills/gen-skeleton/generators/emmet-supabase`
|
|
42
|
-
- Multiple slices can be generated in one command (comma-separated)
|
|
43
|
-
- Slice IDs must exactly match those in config.json
|
|
44
|
-
- Generated files typically include:
|
|
45
|
-
- processor.ts (background automation logic)
|
|
46
|
-
- CRON configuration
|
|
47
|
-
- Tests
|
|
48
|
-
- Automation slices read from TODO lists (work queues) and fire commands on a schedule
|
|
49
|
-
- Common use cases: auto-confirm invitations, process checkouts, send notifications
|
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
# Agent Learnings
|
|
2
|
+
|
|
3
|
+
## Core Principles
|
|
4
|
+
|
|
5
|
+
### Source of Truth
|
|
6
|
+
- **config.json is the TRUE source of truth** for slice status - always check it, not just index.json or slice.json
|
|
7
|
+
- **slice.json reflects the current specification** for field definitions, event structures, and specifications
|
|
8
|
+
- When config.json shows "Planned" but index.json/slice.json show "Done", verify implementation and update config.json
|
|
9
|
+
- The aggregate field in slice.json determines domain entity naming (Location → locationId, Restaurant → restaurantId)
|
|
10
|
+
- When an event is used by multiple slices, fixing it requires updating ALL slices that use it
|
|
11
|
+
- Field names must be consistent across: events, commands, migrations, projections, routes, tests, API docs
|
|
12
|
+
|
|
13
|
+
### Pre-Implementation Checks
|
|
14
|
+
- **Always check if slice already exists** before implementing a "Planned" slice - verify `src/slices/{SliceName}/`
|
|
15
|
+
- **Run tests first**: existing implementations may only need test data fixes
|
|
16
|
+
- **Search for event usage**: `grep -r "EventName" src/slices/` before modifying events
|
|
17
|
+
- **Status drift**: slice status in index.json may lag behind actual implementation
|
|
18
|
+
|
|
19
|
+
### Slice Status
|
|
20
|
+
- Valid statuses: "Done", "Planned", "Assigned", "Created", "Blocked", "Informational"
|
|
21
|
+
- **"Assigned" = "Planned"** - treat as equivalent when picking next slice
|
|
22
|
+
- Always update status to "Done" after completing implementation and passing tests
|
|
23
|
+
|
|
24
|
+
## Event Management
|
|
25
|
+
|
|
26
|
+
### Event Type Naming & Registration
|
|
27
|
+
- Event type names MUST use PascalCase (RestaurantRegistered, not Restaurantregistered)
|
|
28
|
+
- All new events MUST be added to ContextEvents union type in `src/events/ContextEvents.ts`
|
|
29
|
+
- Event types follow `Event<'EventName', {...fields}, {...metadata}>` pattern
|
|
30
|
+
- Event titles in slice.json use lowercase (e.g., "Event created" not "Event Created")
|
|
31
|
+
- TypeScript type names use PascalCase (e.g., `EventCreated` type)
|
|
32
|
+
|
|
33
|
+
### Event Field Structure
|
|
34
|
+
- DateTime type in slice.json → TypeScript `Date` type (not string)
|
|
35
|
+
- Event metadata MUST include restaurantId and userId for multi-tenancy and authorization
|
|
36
|
+
- Auto-generated code may have incorrect casing or wrong field types - always verify against slice.json
|
|
37
|
+
- Field name typos in slice.json must be preserved for consistency (e.g., "restaurandId")
|
|
38
|
+
|
|
39
|
+
### Event Verification & Creation
|
|
40
|
+
- **Always verify events exist** in `src/events/` and `ContextEvents.ts` before creating them
|
|
41
|
+
- Events from unimplemented slices can be created based on their slice.json specifications
|
|
42
|
+
- STATE_VIEW slices can reference events that don't exist yet - create them based on slice.json
|
|
43
|
+
- Check slice.json → dependencies array (INBOUND) for STATE_VIEW event sources
|
|
44
|
+
|
|
45
|
+
### Event Structure Changes - CRITICAL
|
|
46
|
+
When modifying existing event structure:
|
|
47
|
+
1. Find all consumers: `grep -r "EventName" src/slices/`
|
|
48
|
+
2. Update ALL occurrences: command handlers, projections, tests, routes, API docs
|
|
49
|
+
3. Run `npm run build` BEFORE tests - TypeScript shows ALL files with type mismatches
|
|
50
|
+
4. Checklist: Event updated → handlers updated → tests updated → build passes → all tests pass
|
|
51
|
+
|
|
52
|
+
### Cross-Slice Event Reuse
|
|
53
|
+
- Events created for STATE_VIEW projection can be reused by STATE_CHANGE commands
|
|
54
|
+
- Same event consumed by multiple projections (e.g., CheckinCancelled)
|
|
55
|
+
- Create event once, reference in multiple slice implementations
|
|
56
|
+
|
|
57
|
+
## Architecture & Auto-Discovery
|
|
58
|
+
|
|
59
|
+
### Auto-Discovery System
|
|
60
|
+
- **Routes**: `src/slices/**/routes.ts` (exports `api` function returning `WebApiSetup`)
|
|
61
|
+
- **Processors**: `src/slices/**/processor.ts` (exports `processor = { start: () => {...} }`)
|
|
62
|
+
- No manual registration needed - loaded automatically at server startup
|
|
63
|
+
|
|
64
|
+
### Projections Registration
|
|
65
|
+
- All new projections must be registered in `src/common/loadPostgresEventstore.ts`
|
|
66
|
+
- **Critical**: When slice files deleted, check for stale imports causing build failures
|
|
67
|
+
- Pattern: `grep -n "DeletedSliceName" src/common/loadPostgresEventstore.ts`
|
|
68
|
+
|
|
69
|
+
### PostgreSQL Critical Imports
|
|
70
|
+
```typescript
|
|
71
|
+
import { postgreSQLRawSQLProjection } from '@event-driven-io/emmett-postgresql';
|
|
72
|
+
import { sql } from '@event-driven-io/dumbo'; // NOT from emmett-postgresql!
|
|
73
|
+
import { ContextEvents } from '../../events/ContextEvents';
|
|
74
|
+
```
|
|
75
|
+
- Always use `.withSchema('public')` in PostgreSQL queries
|
|
76
|
+
|
|
77
|
+
### Type Coercion
|
|
78
|
+
- Pongo stores numeric-looking strings (e.g., "1") as bigints (e.g., 1n)
|
|
79
|
+
- Use `String(value)` when comparing IDs in test assertions
|
|
80
|
+
|
|
81
|
+
## Stream ID Patterns Reference
|
|
82
|
+
|
|
83
|
+
### `idAttribute` and Stream ID
|
|
84
|
+
- If a field on command/event has `"idAttribute": true`, that field is the streamId
|
|
85
|
+
- Explicit stream identifier in slice.json takes precedence over `idAttribute`
|
|
86
|
+
|
|
87
|
+
### Common Stream ID Patterns
|
|
88
|
+
|
|
89
|
+
| Aggregate Type | Stream ID Format | Helper | Used By |
|
|
90
|
+
|---------------|------------------|--------|---------|
|
|
91
|
+
| Time Entry | `time-entry-{clerkId}-{month}` | `timeEntryStreamId()` | CheckIn, CheckOut, SubmitTimeEntry, DeleteTimeentry, UpdateTimeEntry |
|
|
92
|
+
| Event Assignment | `{restaurantId}-event-{eventId}` | `eventStreamId()` | AssignClerkToEvent, UnAssignClerkFromEvent |
|
|
93
|
+
| Shift Publication | `shift-publication-{shiftId}` | N/A | PublishShift, UnpublishShift |
|
|
94
|
+
|
|
95
|
+
**Notes**: Time entry month format: MMYYYY (e.g., "022026"). Multiple commands can share same stream.
|
|
96
|
+
|
|
97
|
+
## Implementation Patterns
|
|
98
|
+
|
|
99
|
+
> For templates and step-by-step guides, use skills: `/state-change-slice`, `/state-view-slice`, `/automation-slice`
|
|
100
|
+
|
|
101
|
+
### STATE_CHANGE Slice Pattern
|
|
102
|
+
|
|
103
|
+
**Key Rules**:
|
|
104
|
+
- Use proper `initialState` function (not empty object `{}`) in `DeciderSpecification.for()`
|
|
105
|
+
- Do NOT add explicit type arguments - let TypeScript infer (avoids TS2558)
|
|
106
|
+
- Switch statements: always use explicit `break` to prevent fallthrough bugs
|
|
107
|
+
|
|
108
|
+
**Structure**:
|
|
109
|
+
```typescript
|
|
110
|
+
type State = { /* track relevant state */ };
|
|
111
|
+
const initialState = (): State => ({ /* defaults */ });
|
|
112
|
+
const decide = (state: State, cmd: Command): Event[] => {
|
|
113
|
+
if (invalidCondition) throw 'error.code';
|
|
114
|
+
return [createEvent(cmd)];
|
|
115
|
+
};
|
|
116
|
+
const evolve = (state: State, event: ContextEvents): State => {
|
|
117
|
+
switch (event.type) {
|
|
118
|
+
case 'EventType': return { ...state, field: newValue };
|
|
119
|
+
default: return state;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Error Testing**: Use `.shouldFail()` or `assert.throws()`
|
|
125
|
+
|
|
126
|
+
### STATE_VIEW Slice Pattern
|
|
127
|
+
|
|
128
|
+
**Key Rules**:
|
|
129
|
+
- Always verify ALL events from slice.json are in `canHandle` array
|
|
130
|
+
- Use specific event union type (e.g., `Event1 | Event2`) NOT `ContextEvents` when handling subset
|
|
131
|
+
- Missing event handlers = incomplete implementation
|
|
132
|
+
|
|
133
|
+
**Projection Structure**:
|
|
134
|
+
```typescript
|
|
135
|
+
export const ProjectionName = postgreSQLRawSQLProjection({
|
|
136
|
+
canHandle: ['Event1', 'Event2'],
|
|
137
|
+
evolve: (event: Event1 | Event2) => {
|
|
138
|
+
switch (event.type) {
|
|
139
|
+
case 'Event1': return sql(db(table).insert({...}).onConflict().merge());
|
|
140
|
+
case 'Event2': return sql(db(table).where({...}).delete());
|
|
141
|
+
default: return [];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Event Dependencies**: Look in slice.json → readmodels → dependencies (INBOUND) for event IDs
|
|
148
|
+
|
|
149
|
+
### AUTOMATION Slice Pattern
|
|
150
|
+
|
|
151
|
+
**Structure**: STATE_CHANGE + processor.ts + work queue projection
|
|
152
|
+
|
|
153
|
+
**Processor Implementation**:
|
|
154
|
+
```typescript
|
|
155
|
+
import * as cron from 'node-cron';
|
|
156
|
+
import { createServiceClient } from '../../common/supabaseClient';
|
|
157
|
+
|
|
158
|
+
const config = { schedule: '*/30 * * * * *', endpoint: "work_queue_table" };
|
|
159
|
+
|
|
160
|
+
export const processor = {
|
|
161
|
+
start: () => {
|
|
162
|
+
cron.schedule(config.schedule, async () => {
|
|
163
|
+
const client = createServiceClient();
|
|
164
|
+
const result = await client.from(config.endpoint)
|
|
165
|
+
.select("*").eq('must_process', true).limit(1);
|
|
166
|
+
|
|
167
|
+
if (result.error) return;
|
|
168
|
+
for (const item of result.data ?? []) {
|
|
169
|
+
try {
|
|
170
|
+
await handleCommand(streamId, command, { userId: 'system', ...metadata });
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.error('Processing error:', error);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Key Rules**:
|
|
181
|
+
- Use `.limit(1)` to process one item at a time
|
|
182
|
+
- `createServiceClient()` bypasses RLS for automation
|
|
183
|
+
- Processor uses snake_case column names (e.g., `reservation_id`)
|
|
184
|
+
- Work queue lifecycle: add on trigger → update flags → delete on completion
|
|
185
|
+
|
|
186
|
+
## State Tracking Patterns
|
|
187
|
+
|
|
188
|
+
### Boolean/Toggle State
|
|
189
|
+
```typescript
|
|
190
|
+
type State = { isActive: boolean };
|
|
191
|
+
const decide = (state: State, cmd: Command): Event[] => {
|
|
192
|
+
if (state.isActive) throw "already.active"; // Activate guard
|
|
193
|
+
if (!state.isActive) throw "not.active"; // Deactivate guard
|
|
194
|
+
return [event];
|
|
195
|
+
};
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Database**: Use flag column (e.g., `is_online_active BOOLEAN`), update with upsert `.onConflict().merge()`
|
|
199
|
+
|
|
200
|
+
### Set<string> for ID Tracking
|
|
201
|
+
```typescript
|
|
202
|
+
type State = { trackedIds: Set<string> };
|
|
203
|
+
const evolve = (state: State, event: ContextEvents): State => {
|
|
204
|
+
switch (event.type) {
|
|
205
|
+
case 'ItemAdded': return { trackedIds: new Set([...state.trackedIds, event.data.id]) };
|
|
206
|
+
case 'ItemRemoved':
|
|
207
|
+
const newSet = new Set(state.trackedIds);
|
|
208
|
+
newSet.delete(event.data.id);
|
|
209
|
+
return { trackedIds: newSet };
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Examples**: UnAssign Clerk from Event, Delete Time Entry, Update Time Entry
|
|
215
|
+
|
|
216
|
+
### Multi-Flag State Machine
|
|
217
|
+
```typescript
|
|
218
|
+
type State = { submitted: boolean; reverted: boolean; approved: boolean; declined: boolean };
|
|
219
|
+
const decide = (state: State, cmd: Command): Event[] => {
|
|
220
|
+
if (state.submitted) throw 'cannot submit twice';
|
|
221
|
+
if (!state.submitted) throw 'not_submitted';
|
|
222
|
+
if (state.reverted) throw 'already_reverted';
|
|
223
|
+
return [event];
|
|
224
|
+
};
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**Workflows**: Timesheet (Submitted → Reverted → Resubmitted), Approval (Approved → Declined → Reapproved)
|
|
228
|
+
|
|
229
|
+
## Database & Projections
|
|
230
|
+
|
|
231
|
+
### Migration Rules
|
|
232
|
+
- **NEVER modify existing migrations** - always add new ones
|
|
233
|
+
- Version check: `ls -1 supabase/migrations/ | grep "^V" | sort -V | tail -5`
|
|
234
|
+
- Naming: `V{N}__{table_name}.sql` (e.g., `V41__clerk_details.sql`)
|
|
235
|
+
- PostgreSQL types: Use TEXT/VARCHAR (not "string"), valid UUID format required
|
|
236
|
+
- Composite PKs for multi-tenant: `PRIMARY KEY (restaurant_id, entity_id)`
|
|
237
|
+
|
|
238
|
+
### Projection Patterns
|
|
239
|
+
|
|
240
|
+
**Add/Remove**:
|
|
241
|
+
```typescript
|
|
242
|
+
case 'Added': return sql(db(table).insert({...}).onConflict(key).merge());
|
|
243
|
+
case 'Removed': return sql(db(table).where({id}).delete());
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Work Queue Lifecycle**:
|
|
247
|
+
```typescript
|
|
248
|
+
case 'TriggerEvent': return sql(db(table).insert({id, must_process: false}).onConflict().merge());
|
|
249
|
+
case 'ProgressEvent': return sql(db(table).where({id}).update({must_process: true}));
|
|
250
|
+
case 'CompletionEvent': return sql(db(table).where({id}).delete());
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Conditional Flags**: Set flags based on matching business data (e.g., `must_cancel = true` only when clerkId matches)
|
|
254
|
+
|
|
255
|
+
### JSONB Field Handling
|
|
256
|
+
- PostgreSQL JSONB auto-parsed by pg driver - DO NOT use `JSON.parse()` in tests
|
|
257
|
+
- Storage: `assignees JSONB DEFAULT '[]'::jsonb`
|
|
258
|
+
- Updates: `assignees: JSON.stringify(data.assignees)` (plain JavaScript, not raw SQL)
|
|
259
|
+
- Array conversion for display: `Array.isArray(field) ? field.join(', ') : field || ''`
|
|
260
|
+
|
|
261
|
+
### Query Endpoints - Authentication
|
|
262
|
+
All endpoints must:
|
|
263
|
+
1. Require JWT: `requireUser(req, res, true)`
|
|
264
|
+
2. Filter by authenticated user's ID
|
|
265
|
+
3. Use anon key Supabase client
|
|
266
|
+
|
|
267
|
+
### Date Handling
|
|
268
|
+
- slice.json DateTime → TypeScript `Date` type
|
|
269
|
+
- Routes: `date: new Date(assertNotEmpty(req.body.date))`
|
|
270
|
+
- Tests: Use Date objects: `new Date('2026-03-15T10:00:00Z')`
|
|
271
|
+
- Timezone: Europe/Berlin → UTC conversion (CET = UTC+1, CEST = UTC+2 for DST)
|
|
272
|
+
|
|
273
|
+
## Test Patterns Reference
|
|
274
|
+
|
|
275
|
+
### STATE_CHANGE Tests
|
|
276
|
+
```typescript
|
|
277
|
+
DeciderSpecification.for(decide, evolve, initialState)
|
|
278
|
+
.given([PrerequisiteEvent1, PrerequisiteEvent2])
|
|
279
|
+
.when(Command({ field: 'value' }))
|
|
280
|
+
.then([ExpectedEvent({ field: 'value' })]);
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### STATE_VIEW Tests
|
|
284
|
+
```typescript
|
|
285
|
+
PostgreSQLProjectionSpec.for(ProjectionName)
|
|
286
|
+
.given([Event1, Event2])
|
|
287
|
+
.when([]) // Always empty
|
|
288
|
+
.then(async (state) => {
|
|
289
|
+
const result = await state.query();
|
|
290
|
+
assert.equal(result[0].field, expectedValue);
|
|
291
|
+
});
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Error Case Tests
|
|
295
|
+
```typescript
|
|
296
|
+
.given([EventCreated])
|
|
297
|
+
.when(Command)
|
|
298
|
+
.shouldFail(); // Expects error
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Test Coverage
|
|
302
|
+
- **STATE_CHANGE**: Happy path + all error guards
|
|
303
|
+
- **STATE_VIEW**: One test per INBOUND event dependency
|
|
304
|
+
- **AUTOMATION**: Command logic + error guards (processor not unit tested)
|
|
305
|
+
|
|
306
|
+
## Test Specifications & code-slice.json
|
|
307
|
+
|
|
308
|
+
### Purpose of test-analyzer
|
|
309
|
+
- Analyzes tests to extract behavioral specifications
|
|
310
|
+
- Generates code-slice.json in `.slices/{Context}/{folder}/`
|
|
311
|
+
- Includes only specs NOT in slice.json (drift detection)
|
|
312
|
+
|
|
313
|
+
### Drift Detection
|
|
314
|
+
**Generate code-slice.json when**:
|
|
315
|
+
- Test specs NOT in slice.json specifications array
|
|
316
|
+
- slice.json has `specifications: []` but tests exist
|
|
317
|
+
|
|
318
|
+
**No code-slice.json when** (No Drift):
|
|
319
|
+
- All test specs already in slice.json
|
|
320
|
+
- Quality indicator: implementation matches design
|
|
321
|
+
|
|
322
|
+
### linkedId Lookup
|
|
323
|
+
**STATE_CHANGE**: Command/Event IDs from slice.json → commands/events array → id field
|
|
324
|
+
**STATE_VIEW**: Event IDs from slice.json → readmodels → dependencies (INBOUND) → id field
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
# Find slice.json for event source
|
|
328
|
+
find .slices -name "slice.json" -path "*{slicename}*"
|
|
329
|
+
|
|
330
|
+
# Extract event ID (use lowercase title)
|
|
331
|
+
cat path/to/slice.json | jq '.events[] | select(.title == "Event created") | .id'
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**Note**: Event titles in slice.json are lowercase ("Event created"), TypeScript types are PascalCase (`EventCreated`)
|
|
335
|
+
|
|
336
|
+
### Field Type Mapping
|
|
337
|
+
- UUID format → "UUID" type
|
|
338
|
+
- ISO dates → "String" or "Date"
|
|
339
|
+
- Whole numbers → "Integer"
|
|
340
|
+
- Text → "String"
|
|
341
|
+
- Cardinality: "Single" for non-array fields
|
|
342
|
+
|
|
343
|
+
## Implementation Quality Checklist
|
|
344
|
+
|
|
345
|
+
### Standard Workflow (All Slices)
|
|
346
|
+
1. [ ] Build passes: `npm run build`
|
|
347
|
+
2. [ ] All tests pass: `npm run test`
|
|
348
|
+
3. [ ] Run test-analyzer skill (if drift detected)
|
|
349
|
+
4. [ ] Update `.slices/index.json` status to "Done"
|
|
350
|
+
5. [ ] Update `progress.txt` with iteration summary
|
|
351
|
+
6. [ ] Update `AGENTS.md` with new learnings
|
|
352
|
+
7. [ ] Commit with Co-Authored-By line
|
|
353
|
+
|
|
354
|
+
### Additional: AUTOMATION Slices
|
|
355
|
+
- [ ] Processor.ts with CRON schedule
|
|
356
|
+
- [ ] Routes for manual invocation
|
|
357
|
+
- [ ] Test includes error cases
|
|
358
|
+
|
|
359
|
+
### Additional: STATE_VIEW Slices
|
|
360
|
+
- [ ] All INBOUND events in canHandle array
|
|
361
|
+
- [ ] Each event has handler in evolve
|
|
362
|
+
- [ ] Test per INBOUND event
|
|
363
|
+
- [ ] Projection registered in `loadPostgresEventstore.ts`
|
|
364
|
+
|
|
365
|
+
### "To-Do List" STATE_VIEW Pattern
|
|
366
|
+
When a STATE_VIEW acts as a work queue (items appear when created, disappear when processed):
|
|
367
|
+
- Use `insert().onConflict().ignore()` for the trigger event (e.g., RestaurantRegistered)
|
|
368
|
+
- Use `where({id}).delete()` for the completion event (e.g., BucketCreated)
|
|
369
|
+
- Table naming: `slice_<name>` prefix for slice-specific work queue tables
|
|
370
|
+
- The "bucketId" maps to "restaurantId" — both fields stored, one used as PK
|
|
371
|
+
|
|
372
|
+
### AUTOMATION Processor with External API
|
|
373
|
+
When an AUTOMATION slice's `backendPrompt` specifies an external operation (e.g., "create a bucket using the Supabase storage API"):
|
|
374
|
+
- Perform the external API call **before** firing the command
|
|
375
|
+
- Use `client.storage.createBucket(id)` for Supabase storage bucket creation
|
|
376
|
+
- Name the bucket after the entity identifier (e.g., restaurant ID)
|
|
377
|
+
- On failure, catch per-item and continue (don't stop the entire batch)
|
|
378
|
+
- The command acts as the **confirmation** that the external operation succeeded
|
|
379
|
+
|
|
380
|
+
### folder Field in slice.json
|
|
381
|
+
When a command/event has a `folder` field with `"mapping": "month"`:
|
|
382
|
+
- The `folder` field's value = the `month` field's value (same value, different semantic purpose)
|
|
383
|
+
- In storage uploads: use `${month}/` as the folder prefix in the file path (e.g., `${month}/${clerkId}-${month}.csv`)
|
|
384
|
+
- `backendPrompt: "use the month as a folder in the bucket"` = upload path `${month}/{filename}` not just `{filename}`
|
|
385
|
+
- Pass `folder` in command data and event data just like other fields
|
|
386
|
+
|
|
387
|
+
### Additional: Completing Partial Slices
|
|
388
|
+
- [ ] Verify all dependencies from slice.json implemented
|
|
389
|
+
- [ ] Compare canHandle with dependencies
|
|
390
|
+
- [ ] Add missing handlers and tests
|
|
391
|
+
|
|
392
|
+
### Event Structure Changes
|
|
393
|
+
- [ ] Event definition updated
|
|
394
|
+
- [ ] All command handlers updated
|
|
395
|
+
- [ ] All consuming slice tests updated
|
|
396
|
+
- [ ] Build passes, all tests pass
|
|
397
|
+
|
|
398
|
+
## Common Patterns by Domain
|
|
399
|
+
|
|
400
|
+
### Time Entry Domain
|
|
401
|
+
|
|
402
|
+
**Stream ID**: `time-entry-{clerkId}-{month}` (MMYYYY format)
|
|
403
|
+
|
|
404
|
+
**Key Events**: CheckedIn, CheckedOut, TimeEntryAdded, TimesheetSubmitted, SubmissionReverted, TimesheetApproved, TimesheetDeclined, CheckinCancelled
|
|
405
|
+
|
|
406
|
+
**Workflow**:
|
|
407
|
+
1. CheckIn → ActiveCheckins
|
|
408
|
+
2. CheckOut → TimeEntryCheckoutsToProcess (work queue)
|
|
409
|
+
3. ProcessTimeEntryCheckout (automation) → TimeEntryAdded
|
|
410
|
+
4. SubmitTimeEntry → ActiveCheckinsToCancel (mark for cancellation)
|
|
411
|
+
5. CancelCheckin (automation) → CheckinCancelled
|
|
412
|
+
6. SubmitTimesheet → TimesheetSubmitted → SubmissionDate, SubmissionStatus
|
|
413
|
+
7. ApproveTimesheet → TimesheetApproved → ApprovalDate
|
|
414
|
+
8. DeclineTimesheet → TimesheetDeclined (allows reapproval)
|
|
415
|
+
|
|
416
|
+
**Hours Calculation**: `(endDate - startDate) ms / (1000*60*60)`, rounded to 0.5
|
|
417
|
+
|
|
418
|
+
**Guard Patterns**:
|
|
419
|
+
- CheckOut: if CheckedOut in stream → throw "no active check"
|
|
420
|
+
- ProcessTimeEntryCheckout: if NO CheckedOut → throw "no confirmed checkout"
|
|
421
|
+
- SubmitTimesheet: if submitted → throw "cannot submit twice"
|
|
422
|
+
- Delete/Update: track Set<string> of entry IDs
|
|
423
|
+
- SubmitTimeEntry: if `approved && !declined` → throw "cannot submit time entry after approval"
|
|
424
|
+
- DeleteTimeentry: if `approved && !declined` → throw "cannot delete after approval"
|
|
425
|
+
- UpdateTimeEntry: if `submitted && !declined` → throw "cannot submit after submission"
|
|
426
|
+
|
|
427
|
+
**Cross-Slice State Rules (Time Entry Approval Guards)**:
|
|
428
|
+
- `TimesheetApproved` event: sets `approved=true, declined=false`
|
|
429
|
+
- `TimesheetDeclined` event: sets `approved=false, declined=true` (resets approval, re-enables submit/delete/update)
|
|
430
|
+
- `SubmissionReverted` event: sets `approved=false` (allows new submissions after revert)
|
|
431
|
+
- Multiple commands track the same approval state: SubmitTimeEntry, DeleteTimeentry, UpdateTimeEntry
|
|
432
|
+
- Decline AFTER approval is valid workflow (and allows subsequent operations)
|
|
433
|
+
|
|
434
|
+
### Event Assignment Domain
|
|
435
|
+
|
|
436
|
+
**Stream ID**: `{restaurantId}-event-{eventId}` via `eventStreamId()` helper
|
|
437
|
+
|
|
438
|
+
**Key Events**: EventCreated, EventCancelled, EventPlanned, EventReplanned, ClerkAssignedToEvent, ClerkUnassignedFromEvent
|
|
439
|
+
|
|
440
|
+
**Inverse Operations**:
|
|
441
|
+
- Assign/UnAssign share same stream ID
|
|
442
|
+
- Use Set<string> for tracking assigned clerks
|
|
443
|
+
- Assign guard: `if (has(clerkId)) throw "already.assigned"`
|
|
444
|
+
- UnAssign guard: `if (!has(clerkId)) throw "not.assigned"`
|
|
445
|
+
|
|
446
|
+
**Planning**: EventReplanned resets isPlanned to false (allows re-planning)
|
|
447
|
+
|
|
448
|
+
### Activation/Deactivation Pattern
|
|
449
|
+
|
|
450
|
+
**Complementary Commands**:
|
|
451
|
+
- Both track same boolean state, validate opposite conditions
|
|
452
|
+
- Activate: `if (active) throw "already.active"`
|
|
453
|
+
- Deactivate: `if (!active) throw "not.active"`
|
|
454
|
+
- Shared evolve handles BOTH events
|
|
455
|
+
|
|
456
|
+
**Implementation Order**: events → STATE_VIEW → Activate → Deactivate
|
|
457
|
+
|
|
458
|
+
**Examples**: OnlineReservation, Shift Publication
|
|
459
|
+
|
|
460
|
+
### Dashboard Read Models
|
|
461
|
+
|
|
462
|
+
**Characteristics**:
|
|
463
|
+
- Handle 4 events: Create, Cancel, Assign, Unassign
|
|
464
|
+
- JSONB assignees array
|
|
465
|
+
- PRIMARY KEY on entity_id
|
|
466
|
+
- Table naming: `active_{entity}_for_dashboard`
|
|
467
|
+
|
|
468
|
+
**Test Pattern**: Create → Assignment/unassignment → Delete
|
|
469
|
+
|
|
470
|
+
## Common Gotchas
|
|
471
|
+
|
|
472
|
+
### Hooks Rewrite Files
|
|
473
|
+
- Claude Code hooks extensively rewrite files after save
|
|
474
|
+
- Always read files AFTER hook execution
|
|
475
|
+
- Hooks add restaurantId, userId, enrich ContextEvents.ts
|
|
476
|
+
|
|
477
|
+
### Auto-Generation Issues
|
|
478
|
+
- Migration files may get auto-numbered incorrectly (V99) - renumber
|
|
479
|
+
- Projection `canHandle` may be incomplete - verify against slice.json
|
|
480
|
+
- Pre-generated routes may use wrong streamId
|
|
481
|
+
|
|
482
|
+
### TypeScript Issues
|
|
483
|
+
- Don't chain `.given()` directly on `DeciderSpecification.for()`
|
|
484
|
+
- macOS filesystem case-insensitive, TypeScript imports case-sensitive
|
|
485
|
+
- Use specific event union, NOT `ContextEvents` when handling subset
|
|
486
|
+
- Don't add explicit type arguments to `DeciderSpecification.for()`
|
|
487
|
+
|
|
488
|
+
### State Management
|
|
489
|
+
- Always use explicit `break` in switch statements
|
|
490
|
+
- Tests for state-dependent commands need prerequisite events in given
|
|
491
|
+
- Paired commands share same stream
|
|
492
|
+
- Use proper `initialState` function (not `{}`)
|
|
493
|
+
|
|
494
|
+
### Knex/Database
|
|
495
|
+
- `.insert({...}).del()` does NOT insert - it deletes
|
|
496
|
+
- Use `.insert().onConflict().merge()` for upserts
|
|
497
|
+
- Use `.where().update()` or `.where().delete()` for modifications
|
|
498
|
+
|
|
499
|
+
### Specifications
|
|
500
|
+
- Empty `specifications: []` → create tests covering event dependencies
|
|
501
|
+
- Dependency-only events → include in `canHandle` as no-op (return `[]`)
|
|
502
|
+
- Spec assertions may target different read model than slice's own
|
|
503
|
+
- Pre-existing test failures don't block new slice implementation
|
|
504
|
+
|
|
505
|
+
### Metadata Updates
|
|
506
|
+
- Specs may be added to slice.json AFTER tests implemented
|
|
507
|
+
- Verify executable tests exist before assuming new work needed
|
|
508
|
+
- If tests exist and pass → metadata sync → commit as "chore: sync specifications"
|
|
509
|
+
|
|
510
|
+
### Rate Limit Recovery
|
|
511
|
+
- Agent context resets after hitting API rate limits ("You've hit your limit")
|
|
512
|
+
- On resume, re-read slice.json and current implementation to verify state before continuing
|
|
513
|
+
- Do not re-implement already-completed files — check for existing files first
|
|
514
|
+
|
|
515
|
+
## Error Handling
|
|
516
|
+
|
|
517
|
+
- HTTP 409 for conflicts, HTTP 500 for server errors
|
|
518
|
+
- Business-friendly error codes (e.g., "already.active", "not.assigned")
|
|
519
|
+
- German error messages typical in this codebase
|
|
520
|
+
- Check errors most specific to most general (e.g., not_submitted → already_reverted → already_approved)
|
|
521
|
+
|
|
522
|
+
## UI Documentation (ui-prompt.md)
|
|
523
|
+
|
|
524
|
+
Each STATE_CHANGE slice should include `ui-prompt.md` with:
|
|
525
|
+
1. Endpoint URL + HTTP method
|
|
526
|
+
2. Payload example (realistic JSON)
|
|
527
|
+
3. Required headers (correlation_id, Authorization)
|
|
528
|
+
4. Response format (success/error)
|
|
529
|
+
5. Field descriptions + API client code + curl commands
|
|
530
|
+
|
|
531
|
+
For STATE_VIEW: include query endpoint docs + database table definition.
|
|
@@ -14,16 +14,17 @@ Build React + TypeScript UI components from slice JSON definitions using establi
|
|
|
14
14
|
7. A slice can define additional prompts as codegen/uiPrompt. any additional prompts defined in backend are hints for the implementation of the slice and have to be taken into account. If you use the additional prompt, add a line in progress.txt
|
|
15
15
|
8. Write a short progress one liner after each step to progress.txt
|
|
16
16
|
9. Analyze and Implement according to the Rest of the instructions in this file, make use of the skills in the skills directory, but also your previsously collected
|
|
17
|
-
knowledge. Make a list TODO list for what needs to be done. Also make sure to adjust the implementation according to the json definition.
|
|
17
|
+
knowledge. Make a list TODO list for what needs to be done. Also make sure to adjust the implementation according to the json definition.
|
|
18
18
|
10. The slice in the json is always true, the code follows what is defined in the json
|
|
19
19
|
11. the slice is only 'Done' if APIs are implemented.
|
|
20
20
|
12. make sure to read the ui-prompt.md in /backend/src/slices/<slice>
|
|
21
|
-
13.
|
|
22
|
-
|
|
23
|
-
16.
|
|
21
|
+
13. Place the component where it belongs. If you can´t find a place, add a new page with /debug/<page> to showcase the component.
|
|
22
|
+
14. Run quality checks ( npm run build, tsc ) - Attention - it´s enough to run the tests for the slice. Do not run all tests.
|
|
23
|
+
16. Update the Slice in the index.json to status 'Done' and remove assignment
|
|
24
|
+
17. If checks pass, commit ALL changes with message: `feat: [Slice Name]` and merge back to main as FF merge ( update
|
|
24
25
|
first )
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
18. Append your progress to `progress.txt` after each step in the iteration.
|
|
27
|
+
19. append your new learnings to frontend/AGENTS.md in a compressed form, reusable for future iterations. Only add learnings if they are not already there.
|
|
27
28
|
20. Finish the iteration.
|
|
28
29
|
|
|
29
30
|
---
|
|
@@ -208,7 +209,7 @@ export function CreateEventDialog({ open, onOpenChange }) {
|
|
|
208
209
|
if (!form.name) return toast.error("Name required");
|
|
209
210
|
try {
|
|
210
211
|
await createEvent.mutateAsync(form);
|
|
211
|
-
toast.success("
|
|
212
|
+
toast.success("Planned");
|
|
212
213
|
onOpenChange(false);
|
|
213
214
|
} catch (err) {
|
|
214
215
|
toast.error(`Error: ${err.message}`);
|
|
File without changes
|
|
@@ -23,7 +23,8 @@ const App = () => (
|
|
|
23
23
|
<Routes>
|
|
24
24
|
<Route path="/register" element={<Register/>}/>
|
|
25
25
|
<Route path="/auth" element={<Auth/>}/>
|
|
26
|
-
|
|
26
|
+
{/*<Route path="/" element={<ProtectedRoute><Dashboard/></ProtectedRoute>}/>*/}
|
|
27
|
+
<Route path="/" element={<Dashboard/>}/>
|
|
27
28
|
<Route path="*" element={<NotFound/>}/>
|
|
28
29
|
</Routes>
|
|
29
30
|
</BrowserRouter>
|
|
@@ -21,10 +21,10 @@ const AuthContext = createContext<AuthContextType>({
|
|
|
21
21
|
export const useAuth = () => useContext(AuthContext);
|
|
22
22
|
|
|
23
23
|
const fetchAssignedTenant = async (userId: string): Promise<string | null> => {
|
|
24
|
-
try {
|
|
24
|
+
/*try {
|
|
25
25
|
const { data, error } = await supabase
|
|
26
|
-
.from("
|
|
27
|
-
.select("
|
|
26
|
+
.from("tenant_to_user_mapping")
|
|
27
|
+
.select("tenant_id")
|
|
28
28
|
.eq("owner_id", userId)
|
|
29
29
|
.maybeSingle();
|
|
30
30
|
|
|
@@ -32,11 +32,12 @@ const fetchAssignedTenant = async (userId: string): Promise<string | null> => {
|
|
|
32
32
|
console.error("Error fetching assigned tenant:", error);
|
|
33
33
|
return null;
|
|
34
34
|
}
|
|
35
|
-
return data?.
|
|
35
|
+
return data?.tenant_id ?? null;
|
|
36
36
|
} catch (error) {
|
|
37
37
|
console.error("Error fetching assigned tenant:", error);
|
|
38
38
|
return null;
|
|
39
|
-
}
|
|
39
|
+
}*/
|
|
40
|
+
return null
|
|
40
41
|
};
|
|
41
42
|
|
|
42
43
|
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
package/templates/server.mjs
CHANGED
|
@@ -223,7 +223,7 @@ const server = createServer(async (req, res) => {
|
|
|
223
223
|
if (index == -1) {
|
|
224
224
|
sliceIndices.slices.push(sliceIndex);
|
|
225
225
|
} else {
|
|
226
|
-
sliceIndices.slices[index] = sliceIndex;
|
|
226
|
+
sliceIndices.slices[index] = {...sliceIndices.slices[index], ...sliceIndex};
|
|
227
227
|
}
|
|
228
228
|
});
|
|
229
229
|
writeFileSync(indexFile, JSON.stringify(sliceIndices, null, 2));
|
|
@@ -275,6 +275,7 @@ const server = createServer(async (req, res) => {
|
|
|
275
275
|
const allSlices = indexData.slices.map(s => ({
|
|
276
276
|
title: s.slice,
|
|
277
277
|
status: s.status,
|
|
278
|
+
assignee: s.assignee,
|
|
278
279
|
id: s.id,
|
|
279
280
|
specifications: specificationsMap.get(s.id)
|
|
280
281
|
}));
|