@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.
- package/index.js +2 -1
- package/package.json +1 -1
- package/template/CLAUDE.md +218 -0
- package/template/Dockerfile +32 -0
- package/template/Jenkinsfile +28 -0
- package/template/README.md +493 -16
- package/template/_env.example +14 -0
- package/template/backend/app.py +642 -49
- package/template/backend/db_config.py +16 -0
- package/template/backend/extensions.py +3 -0
- package/template/backend/init_db.py +75 -0
- package/template/backend/migrations/README +1 -0
- package/template/backend/migrations/alembic.ini +50 -0
- package/template/backend/migrations/env.py +113 -0
- package/template/backend/migrations/script.py.mako +24 -0
- package/template/backend/migrations/versions/001_add_favourite_organisations.py +31 -0
- package/template/backend/migrations/versions/002_add_org_daily_stats.py +36 -0
- package/template/backend/models.py +31 -0
- package/template/backend/requirements.txt +8 -2
- package/template/eslint.config.js +6 -2
- package/template/index.html +11 -8
- package/template/infra/docker-compose.yml +15 -0
- package/template/openapi.json +40357 -0
- package/template/package.json +8 -1
- package/template/scripts/create-ecr-repo.sh +42 -0
- package/template/src/App.jsx +30 -33
- package/template/src/AppConfigContext.jsx +45 -0
- package/template/src/hooks/useFavourites.js +44 -0
- package/template/src/i18n/index.js +3 -0
- package/template/src/i18n/messages.js +8 -14
- package/template/src/i18n/translations/de.js +72 -0
- package/template/src/i18n/translations/en.js +79 -0
- package/template/src/i18n/translations/es.js +72 -0
- package/template/src/i18n/translations/fr.js +72 -0
- package/template/src/i18n/translations/nl.js +72 -0
- package/template/src/main.jsx +2 -6
- package/template/src/pages/Home.jsx +170 -0
- package/template/src/pages/OrganisationDetail.jsx +263 -0
- package/template/src/pages/OrganisationList.jsx +457 -0
- package/template/src/pages/Overview.jsx +199 -0
- package/template/src/pages/WidgetShowcase.jsx +522 -0
- package/template/src/styles/app.css +543 -4
- package/template/start-backend.sh +4 -0
- 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]
|
|
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
|
@@ -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
|
+
}
|