@sensolus/create-snt-agent-app 0.1.0 → 0.2.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.
Files changed (44) hide show
  1. package/index.js +2 -1
  2. package/package.json +1 -1
  3. package/template/CLAUDE.md +218 -0
  4. package/template/Dockerfile +32 -0
  5. package/template/Jenkinsfile +28 -0
  6. package/template/README.md +493 -16
  7. package/template/_env.example +14 -0
  8. package/template/backend/app.py +642 -49
  9. package/template/backend/db_config.py +16 -0
  10. package/template/backend/extensions.py +3 -0
  11. package/template/backend/init_db.py +75 -0
  12. package/template/backend/migrations/README +1 -0
  13. package/template/backend/migrations/alembic.ini +50 -0
  14. package/template/backend/migrations/env.py +113 -0
  15. package/template/backend/migrations/script.py.mako +24 -0
  16. package/template/backend/migrations/versions/001_add_favourite_organisations.py +31 -0
  17. package/template/backend/migrations/versions/002_add_org_daily_stats.py +36 -0
  18. package/template/backend/models.py +31 -0
  19. package/template/backend/requirements.txt +8 -2
  20. package/template/eslint.config.js +6 -2
  21. package/template/index.html +11 -8
  22. package/template/infra/docker-compose.yml +15 -0
  23. package/template/openapi.json +40357 -0
  24. package/template/package.json +8 -1
  25. package/template/scripts/create-ecr-repo.sh +42 -0
  26. package/template/src/App.jsx +30 -33
  27. package/template/src/AppConfigContext.jsx +45 -0
  28. package/template/src/hooks/useFavourites.js +44 -0
  29. package/template/src/i18n/index.js +3 -0
  30. package/template/src/i18n/messages.js +8 -14
  31. package/template/src/i18n/translations/de.js +72 -0
  32. package/template/src/i18n/translations/en.js +79 -0
  33. package/template/src/i18n/translations/es.js +72 -0
  34. package/template/src/i18n/translations/fr.js +72 -0
  35. package/template/src/i18n/translations/nl.js +72 -0
  36. package/template/src/main.jsx +2 -6
  37. package/template/src/pages/Home.jsx +170 -0
  38. package/template/src/pages/OrganisationDetail.jsx +263 -0
  39. package/template/src/pages/OrganisationList.jsx +457 -0
  40. package/template/src/pages/Overview.jsx +199 -0
  41. package/template/src/pages/WidgetShowcase.jsx +522 -0
  42. package/template/src/styles/app.css +543 -4
  43. package/template/start-backend.sh +4 -0
  44. package/template/start-frontend.sh +3 -0
@@ -1,26 +1,503 @@
1
- # {{APP_NAME}}
1
+ # Sample Micro App
2
2
 
3
- Sensolus agent app, generated by `@sensolus/create-snt-agent-app`.
3
+ A modern React + Flask dashboard for querying the Sensolus public API. This application demonstrates how to build micro applications that integrate with the Sensolus platform using the official design system.
4
4
 
5
- ## Run
5
+ ## Requirements
6
6
 
7
+ - **Node.js** 20+ (for frontend development)
8
+ - **Python** 3.12+ (for backend)
9
+ - **Docker** (for running infrastructure locally)
10
+ - **PostgreSQL** 17 with PostGIS (for data persistence)
11
+
12
+ ## Quick Start
13
+
14
+ ### Environment Variables (.env)
15
+
16
+ Copy `.env.example` to `.env` and fill in the map provider keys (ask your team
17
+ or check the vault). They are **required** for the map pages (`SntMap`):
18
+
19
+ ```env
20
+ MAPBOX_KEY=pk....
21
+ LOCATIONIQ_KEY=pk....
22
+ ```
23
+
24
+ The Flask backend loads `.env` at startup and serves the keys to the frontend
25
+ at runtime via `/api/config` (so they are not baked into the build, and one
26
+ Docker image can deploy across environments — in Docker, pass them with
27
+ `-e MAPBOX_KEY=... -e LOCATIONIQ_KEY=...`). The real `.env` is gitignored;
28
+ never commit keys.
29
+
30
+ ### Local Development (Recommended)
31
+
32
+ Run both the frontend dev server and Flask backend in separate terminals:
33
+
34
+ **Terminal 1 - Backend (Flask API proxy):**
35
+ ```bash
36
+ cd backend
37
+ python -m venv .venv
38
+ source .venv/bin/activate # On Windows: .venv\Scripts\activate
39
+ pip install -r requirements.txt
40
+ python app.py
41
+ ```
42
+
43
+ **Terminal 2 - Frontend (Vite dev server with HMR):**
7
44
  ```bash
8
45
  npm install
9
- pip install -r backend/requirements.txt
10
- npm run dev # frontend :3000 (proxies /api -> :5000)
11
- python backend/app.py # backend :5000, separate terminal
46
+ npm run dev
12
47
  ```
13
48
 
14
- ## Rules
49
+ Open http://localhost:3000 in your browser. The Vite dev server proxies `/api/*` requests to Flask on port 5000.
50
+
51
+ ### VS Code Tasks (Recommended)
52
+
53
+ Use VS Code's built-in task runner to start both servers in side-by-side terminals:
54
+
55
+ 1. Press `Ctrl+Shift+B` (or `Cmd+Shift+B` on Mac)
56
+ 2. Select **"Start Dev Environment"**
57
+
58
+ This runs the Flask backend and Vite frontend in parallel. Both terminals appear in the terminal panel.
59
+
60
+ **Available tasks:**
61
+ | Task | Description |
62
+ |------|-------------|
63
+ | **Start Dev Environment** | Starts both backend and frontend (default build task) |
64
+ | Backend: Flask | Start Flask server only |
65
+ | Frontend: Vite | Start Vite dev server only |
66
+ | Build Production | Build frontend to `dist/` |
67
+ | Docker Build | Build Docker image |
68
+ | Docker Run | Run Docker container |
69
+
70
+ ## PostgreSQL Setup
71
+
72
+ The app uses PostgreSQL for data persistence. The recommended image is `postgis/postgis:17-3.5` (PostgreSQL 17 with PostGIS extensions).
73
+
74
+ ### Option 1: Docker Compose (Recommended)
75
+
76
+ A compose stack is provided in `infra/`:
77
+
78
+ ```bash
79
+ cd infra
80
+ docker compose up -d
81
+ ```
82
+
83
+ To stop:
84
+ ```bash
85
+ docker compose down
86
+ ```
87
+
88
+ To also remove the database volume (wipes all data):
89
+ ```bash
90
+ docker compose down -v
91
+ ```
92
+
93
+ ### Option 2: Standalone PostgreSQL via Docker
94
+
95
+ If you're already running PostgreSQL (e.g. from the shared `minimal-infra-stack.yml`), you can use that instance. Otherwise, start one manually:
96
+
97
+ ```bash
98
+ docker run -d \
99
+ --name {{APP_NAME}}_db \
100
+ -p 5432:5432 \
101
+ -e POSTGRES_USER=snt \
102
+ -e POSTGRES_PASSWORD=snt \
103
+ -e POSTGRES_DB={{APP_NAME}} \
104
+ -v {{APP_NAME}}_pgdata:/var/lib/postgresql/data \
105
+ postgis/postgis:17-3.5
106
+ ```
107
+
108
+ ### Database Configuration
109
+
110
+ The backend reads database settings from environment variables (or a `.env` file in the project root):
111
+
112
+ | Variable | Default | Description |
113
+ |----------|---------|-------------|
114
+ | `DB_HOST` | `localhost` | PostgreSQL host |
115
+ | `DB_PORT` | `5432` | PostgreSQL port |
116
+ | `DB_NAME` | `{{APP_NAME}}` | Database name |
117
+ | `DB_USER` | `snt` | Database user |
118
+ | `DB_PASSWORD` | `snt` | Database password |
119
+
120
+ Example `.env` file:
121
+ ```env
122
+ DB_HOST=localhost
123
+ DB_PORT=5432
124
+ DB_NAME={{APP_NAME}}
125
+ DB_USER=snt
126
+ DB_PASSWORD=snt
127
+ ```
128
+
129
+ ## Architecture
130
+
131
+ ```
132
+ {{APP_NAME}}/
133
+ ├── src/ # React frontend (Vite)
134
+ │ ├── main.jsx # Entry point
135
+ │ ├── App.jsx # Router setup
136
+ │ ├── pages/ # Page components
137
+ │ │ ├── OrganisationList.jsx
138
+ │ │ └── OrganisationDetail.jsx
139
+ │ ├── i18n/ # App translation keys (framework from kit)
140
+ │ └── styles/
141
+ │ └── app.css # App-specific styles
142
+ ├── backend/ # Flask backend
143
+ │ ├── app.py # API proxy server
144
+ │ └── requirements.txt # Python deps (flask, requests)
145
+ ├── index.html # Vite entry HTML
146
+ ├── vite.config.js # Vite config with proxy
147
+ ├── package.json # Node dependencies
148
+ ├── Dockerfile # Multi-stage build
149
+ └── openapi.json # Sensolus API spec (reference)
150
+ ```
151
+
152
+ ### How It Works
153
+
154
+ 1. **Frontend** (React + Vite): Single-page app with React Router for navigation
155
+ 2. **Backend** (Flask): Acts as an API proxy to avoid CORS issues when calling the Sensolus API
156
+ 3. **Development**: Vite serves the frontend on `:3000` and proxies `/api/*` to Flask on `:5000`
157
+ 4. **Production**: Flask serves the built frontend from `dist/` and handles API requests
158
+
159
+ ### API Proxy Flow
160
+
161
+ ```
162
+ Browser → localhost:3000/api/organisations
163
+ → Vite proxy
164
+ → localhost:5000/api/organisations
165
+ → Flask backend
166
+ → cloud.sensolus.com/rest/api/v2/organisations
167
+ ```
168
+
169
+ ## Sensolus API Authentication
170
+
171
+ The backend supports two authentication methods:
172
+
173
+ ### 1. Session Cookie (Bearer Token)
174
+ If logged into cloud.sensolus.com, the `prod-sensolus-token` cookie is passed as a Bearer token:
175
+ ```
176
+ Authorization: Bearer <token>
177
+ ```
178
+
179
+ ### 2. API Key (Query Parameter)
180
+ API keys are passed as a query parameter:
181
+ ```
182
+ GET /api/organisations?apiKey=<your-key>
183
+ ```
184
+
185
+ **Priority:** Session cookie takes precedence over API key if both are present.
186
+
187
+ ---
188
+
189
+ ## Sensolus Design System
190
+
191
+ This app uses the official Sensolus color palette and widget library. **Always use these components when extending the app.**
192
+
193
+ ### Importing Widgets
194
+
195
+ ```jsx
196
+ import {
197
+ SntButton,
198
+ SntInput,
199
+ SntTable,
200
+ SntCard,
201
+ SntBadge,
202
+ SntDialog,
203
+ // ... see full list below
204
+ } from '@sensolus/snt-agent-kit'
205
+ ```
206
+
207
+ ### Available Widgets
208
+
209
+ | Component | Description |
210
+ |-----------|-------------|
211
+ | **SntButton** | Primary button with variants (primary, secondary, success, danger, warning, info) |
212
+ | **SntInput** | Text input (onChange receives value directly, not event) |
213
+ | **SntSelect** | Dropdown select (onChange receives value directly) |
214
+ | **SntBadge** | Status badge with color variants |
215
+ | **SntCard** | Card container with optional image, title, badge |
216
+ | **SntTable** | Sortable, paginated data table |
217
+ | **SntSpinner** | Loading spinner (sizes: small, medium, large) |
218
+ | **SntLoadingOverlay** | Centered spinner with optional message |
219
+ | **SntToolbar** | Horizontal toolbar for grouping actions |
220
+ | **SntToolbarSpacer** | Spacer element for toolbars |
221
+ | **SntButtonGroup** | Segmented control for exclusive selection |
222
+ | **SntProgressBar** | Inline progress bar for percentages |
223
+ | **SntPageHeader** | Page header with title, back button, actions |
224
+ | **SntTabs** | Tab navigation |
225
+ | **SntTabPanel** | Content panel for tabs |
226
+ | **SntDialog** | Modal dialog (small, medium, large sizes) |
227
+ | **SntSidepanel** | Collapsible filter side panel |
228
+ | **SntFilterSection** | Labeled section within sidepanel |
229
+ | **SntSwitch** | Toggle switch |
230
+ | **SntGrid** | Responsive grid layout |
231
+ | **SntGridItem** | Grid item |
232
+ | **SntSummaryStat** | Summary statistic display |
233
+ | **SntHistogram** | Microchart histogram |
234
+ | **SntCheckboxList** | Multi-select checkbox list |
235
+ | **SntDateRangePicker** | Date range picker with presets |
236
+ | **SntColors** | JavaScript color constants |
237
+
238
+ ---
239
+
240
+ ## Widget API Reference
241
+
242
+ ### SntButton
243
+
244
+ ```jsx
245
+ <SntButton
246
+ variant="primary" // 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info'
247
+ onClick={handleClick}
248
+ icon={<Icon />} // Optional icon element
249
+ text="Save" // Button text (or use children)
250
+ title="Tooltip" // Tooltip text
251
+ disabled={false}
252
+ className=""
253
+ >
254
+ Children also work
255
+ </SntButton>
256
+ ```
257
+
258
+ ### SntInput
259
+
260
+ ```jsx
261
+ <SntInput
262
+ value={query}
263
+ onChange={setQuery} // Receives value directly, NOT event
264
+ placeholder="Search..."
265
+ disabled={false}
266
+ readOnly={false}
267
+ />
268
+ ```
269
+
270
+ ### SntSelect
271
+
272
+ ```jsx
273
+ <SntSelect
274
+ options={[
275
+ { value: 'opt1', label: 'Option 1' },
276
+ { value: 'opt2', label: 'Option 2' },
277
+ ]}
278
+ value={selected}
279
+ onChange={setSelected} // Receives value directly, NOT event
280
+ placeholder="Choose..."
281
+ disabled={false}
282
+ />
283
+ ```
284
+
285
+ ### SntBadge
286
+
287
+ ```jsx
288
+ <SntBadge
289
+ text="ACTIVE"
290
+ variant="success" // 'primary' | 'secondary' | 'success' | 'warning' | 'danger' |
291
+ // 'info' | 'light' | 'dark' | 'orange' | 'salmon' | 'purple' | 'emerald'
292
+ compact={false} // Smaller size
293
+ />
294
+ ```
295
+
296
+ ### SntCard
297
+
298
+ ```jsx
299
+ <SntCard
300
+ image="/path/to/image.jpg"
301
+ title="Card Title"
302
+ titleIcon="/icon.svg"
303
+ badge={{ text: 'NEW', variant: 'success' }}
304
+ titleButton={<SntButton text="Action" />}
305
+ onClick={handleClick} // Makes card clickable with hover effect
306
+ >
307
+ <p>Card body content</p>
308
+ </SntCard>
309
+ ```
310
+
311
+ ### SntTable
312
+
313
+ ```jsx
314
+ <SntTable
315
+ data={items}
316
+ columns={[
317
+ { key: 'name', header: 'Name' },
318
+ { key: 'status', header: 'Status', render: (row, val) => <SntBadge text={val} /> },
319
+ { key: 'count', header: 'Count', sortable: false },
320
+ { key: 'date', header: 'Date', getValue: (row) => new Date(row.date).getTime() },
321
+ ]}
322
+ rowKey="id" // Key to use as unique row identifier
323
+ defaultPageSize={25}
324
+ pageSizeOptions={[25, 50, 100]}
325
+ emptyMessage="No data"
326
+ />
327
+ ```
328
+
329
+ **Column options:**
330
+ - `key` - Data property name
331
+ - `header` - Column header text
332
+ - `sortable` - Enable sorting (default: true)
333
+ - `sortKey` - Alternative key for sorting
334
+ - `getValue` - Custom value getter for sorting: `(row) => value`
335
+ - `render` - Custom cell renderer: `(row, value) => ReactNode`
336
+
337
+ ### SntDialog
338
+
339
+ ```jsx
340
+ <SntDialog
341
+ open={isOpen}
342
+ onClose={() => setIsOpen(false)}
343
+ title="Dialog Title"
344
+ size="medium" // 'small' (400px) | 'medium' (600px) | 'large' (1100px)
345
+ >
346
+ <p>Dialog content</p>
347
+ </SntDialog>
348
+ ```
349
+
350
+ ### SntTabs
351
+
352
+ ```jsx
353
+ const [activeTab, setActiveTab] = useState('tab1')
354
+
355
+ <SntTabs
356
+ tabs={[
357
+ { key: 'tab1', label: 'First Tab' },
358
+ { key: 'tab2', label: 'Second Tab' },
359
+ ]}
360
+ activeTab={activeTab}
361
+ onChange={setActiveTab}
362
+ >
363
+ <SntTabPanel tabKey="tab1" activeTab={activeTab}>
364
+ <p>First tab content</p>
365
+ </SntTabPanel>
366
+ <SntTabPanel tabKey="tab2" activeTab={activeTab}>
367
+ <p>Second tab content</p>
368
+ </SntTabPanel>
369
+ </SntTabs>
370
+ ```
371
+
372
+ ### SntSidepanel
373
+
374
+ ```jsx
375
+ const [filtersOpen, setFiltersOpen] = useState(true)
376
+
377
+ <SntSidepanel
378
+ title="Filters"
379
+ open={filtersOpen}
380
+ onToggle={() => setFiltersOpen(!filtersOpen)}
381
+ width={280}
382
+ >
383
+ <SntFilterSection label="Status">
384
+ <SntCheckboxList ... />
385
+ </SntFilterSection>
386
+ <SntFilterSection label="Date Range">
387
+ <SntDateRangePicker ... />
388
+ </SntFilterSection>
389
+ </SntSidepanel>
390
+ ```
391
+
392
+ ### SntSpinner / SntLoadingOverlay
393
+
394
+ ```jsx
395
+ <SntSpinner size="medium" /> // 'small' | 'medium' | 'large'
396
+
397
+ <SntLoadingOverlay message="Loading data..." />
398
+ ```
399
+
400
+ ### SntButtonGroup
401
+
402
+ ```jsx
403
+ <SntButtonGroup
404
+ options={[
405
+ { value: 'cards', label: 'Cards' },
406
+ { value: 'table', label: 'Table' },
407
+ ]}
408
+ value={viewMode}
409
+ onChange={setViewMode}
410
+ />
411
+ ```
412
+
413
+ ### SntProgressBar
414
+
415
+ ```jsx
416
+ <SntProgressBar
417
+ value={75}
418
+ variant="success" // 'primary' | 'success' | 'warning' | 'danger' | 'info'
419
+ size="medium" // 'small' | 'medium'
420
+ showLabel={true}
421
+ />
422
+ ```
423
+
424
+ ### SntPageHeader
425
+
426
+ ```jsx
427
+ <SntPageHeader
428
+ title="Page Title"
429
+ onBack={() => navigate(-1)} // Shows back button
430
+ actions={
431
+ <>
432
+ <SntButton text="Export" />
433
+ <SntButton variant="primary" text="Create" />
434
+ </>
435
+ }
436
+ />
437
+ ```
438
+
439
+ ---
440
+
441
+ ## Color Palette (CSS Variables)
442
+
443
+ Use CSS custom properties for consistent styling:
444
+
445
+ ```css
446
+ /* Primary Brand Colors */
447
+ --snt-blue-darkest: #212851; /* Headers, primary buttons */
448
+ --snt-blue: #0071A1; /* Links, focus states */
449
+ --snt-blue-light: #A0C3D8;
450
+
451
+ /* Greys */
452
+ --snt-grey: #535E6F; /* Secondary text */
453
+ --snt-grey-light: #B8BFCA; /* Borders */
454
+ --snt-grey-lightest: #DEE4E6;
455
+
456
+ /* Backgrounds */
457
+ --snt-bg-lightblue: #EFF3F4; /* Page background */
458
+ --snt-bg-zebra: #F9FAFA; /* Alternating rows */
459
+ --snt-white: #FFFFFF;
460
+
461
+ /* Status Colors */
462
+ --snt-green: #39CB99; /* Success */
463
+ --snt-yellow: #FFCC66; /* Warning */
464
+ --snt-red: #E00000; /* Danger */
465
+ --snt-infra: #00A6ED; /* Info */
466
+
467
+ /* Semantic Aliases (preferred) */
468
+ --snt-color-primary: var(--snt-blue-darkest);
469
+ --snt-color-success: var(--snt-green);
470
+ --snt-color-warning: var(--snt-yellow);
471
+ --snt-color-danger: var(--snt-red);
472
+ --snt-color-text-primary: var(--snt-blue-darkest);
473
+ --snt-color-text-secondary: var(--snt-grey);
474
+ --snt-color-border: var(--snt-grey-light);
475
+ ```
476
+
477
+ ### JavaScript Colors
478
+
479
+ ```jsx
480
+ import { SntColors } from '@sensolus/snt-agent-kit'
481
+
482
+ // SntColors.blueDarkest, SntColors.green, etc.
483
+ ```
484
+
485
+ ---
486
+
487
+ ## Usage Guidelines
488
+
489
+ 1. **Always use CSS variables** for colors instead of hardcoded hex values
490
+ 2. **Import widgets** from `@sensolus/snt-agent-kit` - they're modular ES modules
491
+ 3. **Follow existing patterns** in `src/pages/` for page structure
492
+ 4. **Use semantic aliases** (`--snt-color-primary`) over raw colors when appropriate
493
+ 5. **Test both dev and production** modes before deploying
494
+
495
+ ## CI/CD
15
496
 
16
- - UI widgets, theme, colors and i18n framework come from **`@sensolus/snt-agent-kit`**
17
- import them, never copy their source into this repo.
18
- - `npm run lint` enforces: no deep imports into the kit, no re-declaring `Snt*` components.
19
- - Need widget customization? Use slots/render props; if none fits, PR the
20
- `snt-agent-kit` repo. There is no eject path.
21
- - Kit updates: `npm update @sensolus/snt-agent-kit`.
497
+ Jenkins pipeline (`Jenkinsfile`) builds the Docker image using a multi-stage build:
498
+ 1. Node.js stage builds the React frontend
499
+ 2. Python stage serves with Flask
22
500
 
23
- ## i18n
501
+ ## License
24
502
 
25
- App keys live in `src/i18n/messages.js`, merged via `<LocaleProvider messages>`.
26
- Common strings (`common.*`, `table.*`) ship with the kit.
503
+ Proprietary - Sensolus
@@ -0,0 +1,14 @@
1
+ # Copy to .env and fill in — never commit the real .env (it is gitignored).
2
+ #
3
+ # Map provider keys (get them from your team vault). Required for SntMap:
4
+ # the Flask backend reads them at request time and serves them to the
5
+ # frontend via /api/config, so one Docker image works across environments.
6
+ MAPBOX_KEY=
7
+ LOCATIONIQ_KEY=
8
+
9
+ # PostgreSQL connection (defaults shown; see README "Database Configuration")
10
+ #DB_HOST=localhost
11
+ #DB_PORT=5432
12
+ #DB_NAME={{APP_NAME}}
13
+ #DB_USER=snt
14
+ #DB_PASSWORD=snt