@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
package/index.js CHANGED
@@ -39,7 +39,7 @@ async function substitute(dir) {
39
39
  for (const entry of await readdir(dir)) {
40
40
  const p = path.join(dir, entry)
41
41
  if ((await stat(p)).isDirectory()) { await substitute(p); continue }
42
- if (!/\.(json|js|jsx|html|md|py|txt|sh|css)$|^\.[a-z]+$/i.test(entry)) continue
42
+ if (!/\.(json|js|jsx|html|md|py|txt|sh|css|yml|yaml|ini|mako)$|^\.[a-z][a-z.]*$|^(Jenkinsfile|Dockerfile|Makefile)$/i.test(entry)) continue
43
43
  const content = await readFile(p, 'utf8')
44
44
  if (content.includes('{{APP_NAME}}')) {
45
45
  await writeFile(p, content.replaceAll('{{APP_NAME}}', appName))
@@ -53,6 +53,7 @@ Created ${appName}/
53
53
 
54
54
  Next steps:
55
55
  cd ${appName}
56
+ cp .env.example .env # then fill in MAPBOX_KEY + LOCATIONIQ_KEY (required for SntMap)
56
57
  npm install
57
58
  pip install -r backend/requirements.txt
58
59
  npm run dev # frontend on :3000
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sensolus/create-snt-agent-app",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Scaffold a new Sensolus agent app: React frontend wired to @sensolus/snt-agent-kit + Flask API proxy backend.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,218 @@
1
+ # CLAUDE.md
2
+
3
+ > **Widgets, theme, colors and the i18n framework come from `@sensolus/snt-agent-kit`** —
4
+ > import from the package, never copy widget source into this repo. ESLint enforces:
5
+ > no deep imports into the kit, no re-declaring `Snt*` components. App-owned translation
6
+ > keys live in `src/i18n/translations/` and are merged via `<LocaleProvider messages>`.
7
+ > Theme: `import '@sensolus/snt-agent-kit/theme.css'` (in `main.jsx`).
8
+
9
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
10
+
11
+ ## Build and Run Commands
12
+
13
+ ```bash
14
+ # Local development (frontend + backend)
15
+ npm install # Install frontend dependencies
16
+ npm run dev # Start Vite dev server on :3000 (proxies /api to Flask)
17
+ python backend/app.py # Start Flask backend on :5000 (in separate terminal)
18
+
19
+ # Production build
20
+ npm run build # Build frontend to dist/
21
+ python backend/app.py # Serves from dist/
22
+
23
+ # Docker build and run
24
+ docker build -t {{APP_NAME}} .
25
+ docker run -p 5000:5000 {{APP_NAME}}
26
+ ```
27
+
28
+ Development: http://localhost:3000 (Vite with HMR)
29
+ Production: http://localhost:5000 (Flask serves built frontend)
30
+
31
+ ## Architecture
32
+
33
+ This is a Flask + React (Vite) dashboard for querying the Sensolus public API.
34
+
35
+ ### Project Structure
36
+
37
+ ```
38
+ {{APP_NAME}}/
39
+ ├── src/ # React frontend source
40
+ │ ├── main.jsx # Entry point
41
+ │ ├── App.jsx # Main app component
42
+ │ ├── i18n/ # App translation keys (framework from kit)
43
+ │ └── styles/
44
+ │ └── app.css # App-specific styles
45
+ ├── backend/ # Flask backend
46
+ │ ├── app.py # Flask app (API proxy)
47
+ │ └── requirements.txt # Python dependencies
48
+ ├── index.html # Vite entry HTML
49
+ ├── vite.config.js # Vite configuration
50
+ ├── package.json # Node dependencies
51
+ └── openapi.json # Sensolus API spec (reference)
52
+ ```
53
+
54
+ The backend acts as a proxy to avoid CORS issues - the frontend calls `/api/organisations` which forwards to the Sensolus API.
55
+
56
+ ## Sensolus API Authentication
57
+
58
+ The backend supports two authentication methods for the Sensolus API:
59
+
60
+ ### 1. Session Cookie (Bearer Token)
61
+ If the user has a `prod-sensolus-token` cookie (from being logged into cloud.sensolus.com), it is passed as a Bearer token in the Authorization header:
62
+ ```
63
+ Authorization: Bearer <token>
64
+ ```
65
+
66
+ ### 2. API Key (Query Parameter)
67
+ API keys are passed as a query parameter to the Sensolus API:
68
+ ```
69
+ GET /rest/api/v2/organisations?apiKey=<key>
70
+ ```
71
+
72
+ **Priority:** Session cookie takes precedence over API key if both are present.
73
+
74
+ ## CI/CD
75
+
76
+ Jenkins pipeline (Jenkinsfile) builds the Docker image using multi-stage build (Node for frontend, Python for backend).
77
+
78
+ ## Sensolus Design System
79
+
80
+ This app uses the official Sensolus color palette and widgets. **Always use these when extending this app.**
81
+
82
+ ### Importing Widgets
83
+
84
+ ```jsx
85
+ import { SntButton, SntInput, SntBadge, SntTable } from '@sensolus/snt-agent-kit'
86
+ ```
87
+
88
+ ### Available Widgets
89
+
90
+ | Component | Description |
91
+ |-----------|-------------|
92
+ | `SntButton` | Primary button with variants (primary, secondary, success, danger, warning, info) |
93
+ | `SntInput` | Text input (onChange receives value directly, not event) |
94
+ | `SntBadge` | Status badge with color variants |
95
+ | `SntCard` | Card container with optional image, title, badge |
96
+ | `SntTable` | Sortable, paginated data table |
97
+ | `SntSpinner` | Loading spinner (sizes: small, medium, large) |
98
+ | `SntLoadingOverlay` | Centered spinner with optional message |
99
+ | `SntToolbar` | Horizontal toolbar for grouping actions |
100
+ | `SntButtonGroup` | Segmented control for exclusive selection |
101
+ | `SntColors` | JavaScript color constants |
102
+
103
+ ### Color Palette (CSS Variables)
104
+
105
+ ```css
106
+ /* Primary Brand Colors */
107
+ --snt-blue-darkest: #212851; /* Primary - headers, buttons */
108
+ --snt-blue: #0071A1; /* Links, focus states */
109
+
110
+ /* Greys */
111
+ --snt-grey: #535E6F; /* Secondary text */
112
+ --snt-grey-light: #B8BFCA; /* Borders */
113
+
114
+ /* Backgrounds */
115
+ --snt-bg-zebra: #F9FAFA; /* Alternating rows */
116
+ --snt-white: #FFFFFF;
117
+
118
+ /* Status Colors */
119
+ --snt-green: #39CB99; /* Success */
120
+ --snt-yellow: #FFCC66; /* Warning */
121
+ --snt-red: #E00000; /* Danger */
122
+ --snt-infra: #00A6ED; /* Info */
123
+ ```
124
+
125
+ ### Widget Examples
126
+
127
+ ```jsx
128
+ // Button
129
+ <SntButton variant="primary" onClick={handleClick}>Save</SntButton>
130
+
131
+ // Input
132
+ <SntInput value={query} onChange={setQuery} placeholder="Search..." />
133
+
134
+ // Badge
135
+ <SntBadge variant="success" text="ACTIVE" />
136
+
137
+ // Button Group (toggle)
138
+ <SntButtonGroup
139
+ options={[
140
+ { value: 'cards', label: 'Cards' },
141
+ { value: 'table', label: 'Table' }
142
+ ]}
143
+ value={viewMode}
144
+ onChange={setViewMode}
145
+ />
146
+
147
+ // Table
148
+ <SntTable
149
+ data={items}
150
+ columns={[
151
+ { key: 'name', header: 'Name' },
152
+ { key: 'status', header: 'Status', render: (row, val) => <SntBadge text={val} /> }
153
+ ]}
154
+ />
155
+
156
+ // Loading
157
+ <SntLoadingOverlay message="Loading data..." />
158
+ ```
159
+
160
+ ### Usage Guidelines
161
+
162
+ 1. **Always use CSS variables** for colors instead of hardcoded hex values
163
+ 2. **Import widgets** from `@sensolus/snt-agent-kit` - they're modular ES modules
164
+ 3. **Follow existing patterns** in `src/App.jsx` for examples
165
+ 4. **Reference the baseline** at `/sensolus/work/baseline/` for additional components not yet ported
166
+
167
+ ## Internationalization (i18n)
168
+
169
+ The app fetches user preferences (language, timezone, units) from `/api/loginInfo` on startup and provides them via React context.
170
+
171
+ ### Supported Languages
172
+
173
+ en, nl, fr, de, es — English is the fallback for any missing keys.
174
+
175
+ ### Using Translations in Components
176
+
177
+ ```jsx
178
+ import { useLocale, formatNumber, formatShortDate } from '../i18n'
179
+
180
+ function MyComponent() {
181
+ const { t, intlLocale, timezone } = useLocale()
182
+
183
+ return (
184
+ <div>
185
+ <h1>{t('orgList.title')}</h1>
186
+ <span>{formatNumber(1234, intlLocale)}</span>
187
+ <span>{formatShortDate('2026-03-23', intlLocale, timezone)}</span>
188
+ </div>
189
+ )
190
+ }
191
+ ```
192
+
193
+ ### Adding Translations
194
+
195
+ Translation files are in `src/i18n/translations/`. Each file exports a flat object with dot-namespaced keys:
196
+
197
+ ```js
198
+ // Simple string
199
+ 'common.cancel': 'Cancel',
200
+
201
+ // Interpolated string (use function)
202
+ 'table.showing': (from, to, total) => `Showing ${from} - ${to} of ${total} items`,
203
+ ```
204
+
205
+ ### Date/Number Formatting
206
+
207
+ Always use `intlLocale` and `timezone` from `useLocale()` — never hardcode locales:
208
+
209
+ ```jsx
210
+ import { formatShortDate, formatDateTime, formatNumber } from '../i18n'
211
+
212
+ formatShortDate('2026-03-23', intlLocale, timezone) // "23 mrt. 2026" (nl)
213
+ formatNumber(1234, intlLocale) // "1.234" (nl) vs "1,234" (en)
214
+ ```
215
+
216
+ ### Available Context Values
217
+
218
+ `useLocale()` provides: `language`, `intlLocale`, `timezone`, `firstDayOfWeek`, `unitDistance`, `unitTemperature`, `t()`, `loading`
@@ -0,0 +1,32 @@
1
+ # Build stage - build the React frontend
2
+ FROM node:20-slim AS builder
3
+
4
+ WORKDIR /app
5
+
6
+ # Install dependencies
7
+ COPY package.json package-lock.json ./
8
+ RUN npm ci
9
+
10
+ # Copy source and build
11
+ COPY src/ src/
12
+ COPY index.html vite.config.js ./
13
+ RUN npm run build
14
+
15
+ # Production stage - serve with Flask
16
+ FROM python:3.12-slim
17
+
18
+ WORKDIR /app
19
+
20
+ # Install Python dependencies
21
+ COPY backend/requirements.txt .
22
+ RUN pip install --no-cache-dir -r requirements.txt
23
+
24
+ # Copy Flask app and built frontend
25
+ COPY backend/ backend/
26
+ COPY --from=builder /app/dist dist/
27
+
28
+ EXPOSE 5000
29
+
30
+ # Run with socketio.run() for proper WebSocket support (not gunicorn)
31
+ ENV PYTHONUNBUFFERED=1
32
+ CMD ["python", "backend/app.py"]
@@ -0,0 +1,28 @@
1
+ pipeline {
2
+ agent any
3
+
4
+ environment {
5
+ AWS_REGION = 'eu-west-1'
6
+ ECR_REGISTRY = '331708581843.dkr.ecr.eu-west-1.amazonaws.com'
7
+ IMAGE_NAME = '{{APP_NAME}}'
8
+ IMAGE_TAG = "${env.BUILD_NUMBER}"
9
+ }
10
+
11
+ stages {
12
+ stage('Build') {
13
+ steps {
14
+ sh "docker build -t ${IMAGE_NAME}:${IMAGE_TAG} -t ${IMAGE_NAME}:latest ."
15
+ }
16
+ }
17
+
18
+ stage('Push to ECR') {
19
+ steps {
20
+ sh "aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${ECR_REGISTRY}"
21
+ sh "docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${ECR_REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}"
22
+ sh "docker tag ${IMAGE_NAME}:latest ${ECR_REGISTRY}/${IMAGE_NAME}:latest"
23
+ sh "docker push ${ECR_REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}"
24
+ sh "docker push ${ECR_REGISTRY}/${IMAGE_NAME}:latest"
25
+ }
26
+ }
27
+ }
28
+ }