@sensolus/create-snt-agent-app 0.1.1 → 0.2.1

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/README.md ADDED
@@ -0,0 +1,153 @@
1
+ # @sensolus/create-snt-agent-app
2
+
3
+ Scaffold a new Sensolus agent app: a React (Vite) frontend wired to
4
+ [`@sensolus/snt-agent-kit`](https://www.npmjs.com/package/@sensolus/snt-agent-kit)
5
+ plus a Flask backend that proxies the Sensolus public API, with PostgreSQL,
6
+ migrations, Docker, and Jenkins CI included.
7
+
8
+ ## Quick start — create an agent app
9
+
10
+ ```bash
11
+ npm create @sensolus/snt-agent-app my-app
12
+ cd my-app
13
+ cp .env.example .env # fill in MAPBOX_KEY + LOCATIONIQ_KEY (required for SntMap)
14
+ npm install
15
+ pip install -r backend/requirements.txt
16
+ npm run dev # frontend on :3000
17
+ python backend/app.py # backend on :5000 (separate terminal)
18
+ ```
19
+
20
+ Open http://localhost:3000. The Vite dev server proxies `/api/*` to Flask;
21
+ Flask serves runtime config (map keys) at `/api/config` and proxies the
22
+ Sensolus API with your session cookie or an API key entered in the app.
23
+
24
+ The generated README covers the rest: PostgreSQL setup, Docker build,
25
+ deployment, and environment variables.
26
+
27
+ ### Rules of the road
28
+
29
+ Widgets, theme, and the i18n framework come from `@sensolus/snt-agent-kit` —
30
+ **import them, don't copy them**. ESLint in the generated app enforces no deep
31
+ imports and no `Snt*` re-declarations. App-specific translation keys live in
32
+ your app (`src/i18n/messages.js`) and are merged via
33
+ `<LocaleProvider messages={...}>`.
34
+
35
+ ## The widget kit
36
+
37
+ The generated app comes pre-wired, but everything below is plain
38
+ `@sensolus/snt-agent-kit` API — useful when extending the app.
39
+
40
+ ### Default wiring (generated-app convention)
41
+
42
+ The app proxies the Sensolus backend at `/api/*`, so the kit's defaults just
43
+ work:
44
+
45
+ ```jsx
46
+ import { SntUiProvider, LocaleProvider, SntButton, SntCard } from '@sensolus/snt-agent-kit'
47
+ import '@sensolus/snt-agent-kit/theme.css'
48
+ import { messages } from './i18n' // your app's translation keys
49
+
50
+ function App() {
51
+ return (
52
+ <SntUiProvider>
53
+ <LocaleProvider messages={messages}>
54
+ <SntCard title="Hello">
55
+ <SntButton variant="primary">Click me</SntButton>
56
+ </SntCard>
57
+ </LocaleProvider>
58
+ </SntUiProvider>
59
+ )
60
+ }
61
+ ```
62
+
63
+ ### Custom wiring (non-`/api/*` backends)
64
+
65
+ `SntUiProvider` accepts overrides for any of the four backend calls and for
66
+ navigation. Anything you omit falls back to the same-origin `/api/*` default.
67
+
68
+ ```jsx
69
+ import { useNavigate } from 'react-router-dom'
70
+ import { SntUiProvider } from '@sensolus/snt-agent-kit'
71
+
72
+ function Shell({ children }) {
73
+ const navigate = useNavigate()
74
+ return (
75
+ <SntUiProvider
76
+ api={{
77
+ // Call the Sensolus public API directly with your own API key
78
+ fetchLoginInfo: () => mySensolus.getLoginInfo(),
79
+ fetchGeozones: (orgId) => mySensolus.getGeozones(orgId),
80
+ geocode: (q) => mySensolus.geocode(q),
81
+ reverseGeocode: (lat, lng) => mySensolus.reverseGeocode(lat, lng),
82
+ }}
83
+ navigate={navigate} // optional: wire to react-router, Next, etc.
84
+ >
85
+ {children}
86
+ </SntUiProvider>
87
+ )
88
+ }
89
+ ```
90
+
91
+ #### `api` contract
92
+
93
+ | Method | Signature | Used by |
94
+ |--------|-----------|---------|
95
+ | `fetchLoginInfo()` | `() => Promise<{ language, timezone, firstDayOfWeek, unitDistance, unitTemperature } \| null>` | `LocaleProvider` |
96
+ | `fetchGeozones(orgId)` | `(string) => Promise<Geozone[]>` | `SntMap` (when `orgId` prop set) |
97
+ | `geocode(query)` | `(string) => Promise<Array<{ name, lat, lng, boundingbox }>>` | `SntMap` (search box) |
98
+ | `reverseGeocode(lat, lng)` | `(number, number) => Promise<{ displayName, address, lat, lng } \| null>` | reserved for future widgets |
99
+
100
+ #### `navigate` contract
101
+
102
+ `(to: string \| number) => void` — strings are routes (`'/orgs/123'`), `-1`
103
+ means "back". Wire to `useNavigate()` from react-router-dom, the Next.js
104
+ router, or omit to fall back to `window.history`.
105
+
106
+ ### SntMap
107
+
108
+ Tile-provider keys are **required props** — the kit ships no keys. In a
109
+ generated app they come from the backend's `/api/config` at runtime (via
110
+ `AppConfigContext`), so one Docker image deploys across environments:
111
+
112
+ ```jsx
113
+ const config = useAppConfig()
114
+
115
+ <SntMap
116
+ mapboxKey={config.mapboxKey}
117
+ locationiqKey={config.locationiqKey}
118
+ devices={devices}
119
+ orgId={id} // triggers api.fetchGeozones(orgId)
120
+ />
121
+ ```
122
+
123
+ ### Components
124
+
125
+ | Component | Description |
126
+ |-----------|-------------|
127
+ | `SntUiProvider`, `useSntUi` | Inject backend `api` + `navigate` adapters |
128
+ | `LocaleProvider`, `useLocale` | i18n / locale context (reads `api.fetchLoginInfo`, merges app `messages`) |
129
+ | `formatShortDate`, `formatDateTime`, `formatNumber` | Locale-aware formatters |
130
+ | `SntButton` | Primary button with variants |
131
+ | `SntInput` | Text input (onChange receives value directly) |
132
+ | `SntBadge` | Status badge with color variants |
133
+ | `SntCard` | Card container |
134
+ | `SntTable` | Sortable, paginated data table |
135
+ | `SntSpinner`, `SntLoadingOverlay` | Loading indicators |
136
+ | `SntToolbar`, `SntToolbarSpacer` | Toolbar layout |
137
+ | `SntButtonGroup` | Segmented control |
138
+ | `SntSelect` | Native select with Sensolus styling |
139
+ | `SntPageHeader` | Page header (uses `navigate` from `SntUiProvider`) |
140
+ | `SntTabs`, `SntTabPanel` | Tabbed views |
141
+ | `SntDialog` | Modal dialog |
142
+ | `SntSidepanel`, `SntFilterSection` | Slide-out side panel |
143
+ | `SntSwitch` | Toggle switch |
144
+ | `SntGrid`, `SntGridItem` | Responsive grid |
145
+ | `SntSummaryStat` | Big-number summary tile |
146
+ | `SntHistogram` | Time-bucketed histogram |
147
+ | `SntCheckboxList` | Multi-select checkbox list |
148
+ | `SntDateRangePicker` | Date range picker (locale-aware) |
149
+ | `SntMap` | Leaflet map (Street/Satellite, geozones, device markers) |
150
+ | `SntColors` | JavaScript color constants |
151
+
152
+ The generated app includes a **Widgets** tab (`WidgetShowcase`) demonstrating
153
+ every component live.
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.1",
3
+ "version": "0.2.1",
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": {
@@ -11,6 +11,22 @@ A modern React + Flask dashboard for querying the Sensolus public API. This appl
11
11
 
12
12
  ## Quick Start
13
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
+
14
30
  ### Local Development (Recommended)
15
31
 
16
32
  Run both the frontend dev server and Flask backend in separate terminals:
@@ -1,4 +1,14 @@
1
- # Copy to .env required for SntMap tile layers (get keys from your team vault).
2
- # Keys are loaded at app build time; never commit the real .env.
3
- VITE_LOCATIONIQ_KEY=
4
- VITE_MAPBOX_KEY=
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
@@ -401,6 +401,18 @@ def get_devices_by_filter():
401
401
 
402
402
 
403
403
  LOCATIONIQ_KEY = os.environ.get('LOCATIONIQ_KEY', '')
404
+ MAPBOX_KEY = os.environ.get('MAPBOX_KEY', '')
405
+
406
+
407
+ @app.route('/api/config')
408
+ def app_config():
409
+ """Runtime config served to the frontend. Lets one Docker image deploy
410
+ across dev/demo/prod by reading keys from the container's env at request
411
+ time instead of baking them into the bundle at build time."""
412
+ return jsonify({
413
+ 'mapboxKey': MAPBOX_KEY,
414
+ 'locationiqKey': LOCATIONIQ_KEY,
415
+ }), 200
404
416
 
405
417
 
406
418
  @app.route('/api/geocode')
@@ -15,7 +15,7 @@
15
15
  "url": "git+ssh://git@bitbucket.org/sensolus/{{APP_NAME}}.git"
16
16
  },
17
17
  "dependencies": {
18
- "@sensolus/snt-agent-kit": "^0.1.1",
18
+ "@sensolus/snt-agent-kit": "^0.2.0",
19
19
  "react": "^19.2.3",
20
20
  "react-dom": "^19.2.3",
21
21
  "react-router-dom": "^7.1.1"
@@ -1,14 +1,33 @@
1
- import { BrowserRouter, Routes, Route } from 'react-router-dom'
1
+ import { BrowserRouter, Routes, Route, useNavigate } from 'react-router-dom'
2
+ import { SntUiProvider } from '@sensolus/snt-agent-kit'
3
+ import { LocaleProvider, messages } from './i18n'
4
+ import { AppConfigProvider } from './AppConfigContext'
2
5
  import { Home } from './pages/Home'
3
6
  import { OrganisationDetail } from './pages/OrganisationDetail'
4
7
 
8
+ // SntUiProvider decouples kit widgets from the router and backend:
9
+ // pass `navigate` (and optionally `api`) so widgets like SntPageHeader work.
10
+ // LocaleProvider sits inside it so it fetches login info via the api adapter.
11
+ function AppShell() {
12
+ const navigate = useNavigate()
13
+ return (
14
+ <SntUiProvider navigate={navigate}>
15
+ <LocaleProvider messages={messages}>
16
+ <AppConfigProvider>
17
+ <Routes>
18
+ <Route path="/" element={<Home />} />
19
+ <Route path="/organisation/:id" element={<OrganisationDetail />} />
20
+ </Routes>
21
+ </AppConfigProvider>
22
+ </LocaleProvider>
23
+ </SntUiProvider>
24
+ )
25
+ }
26
+
5
27
  function App() {
6
28
  return (
7
29
  <BrowserRouter>
8
- <Routes>
9
- <Route path="/" element={<Home />} />
10
- <Route path="/organisation/:id" element={<OrganisationDetail />} />
11
- </Routes>
30
+ <AppShell />
12
31
  </BrowserRouter>
13
32
  )
14
33
  }
@@ -0,0 +1,45 @@
1
+ import { createContext, useContext, useEffect, useState } from 'react'
2
+ import { SntLoadingOverlay } from '@sensolus/snt-agent-kit'
3
+
4
+ /**
5
+ * Fetches runtime config (map tile keys, etc.) from the Flask backend's
6
+ * /api/config endpoint and gates the app on it. Lets one Docker image
7
+ * deploy across environments — keys come from the container's env vars
8
+ * at runtime instead of being baked into the bundle at `vite build` time.
9
+ */
10
+ const AppConfigContext = createContext(null)
11
+
12
+ export function AppConfigProvider({ children }) {
13
+ const [config, setConfig] = useState(null)
14
+ const [error, setError] = useState(null)
15
+
16
+ useEffect(() => {
17
+ let cancelled = false
18
+ fetch('/api/config')
19
+ .then(r => r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`)))
20
+ .then(data => { if (!cancelled) setConfig(data) })
21
+ .catch(err => { if (!cancelled) setError(err) })
22
+ return () => { cancelled = true }
23
+ }, [])
24
+
25
+ if (error) {
26
+ return (
27
+ <div style={{ padding: 40, color: 'var(--snt-red)' }}>
28
+ Failed to load app config from /api/config: {error.message}
29
+ </div>
30
+ )
31
+ }
32
+ if (!config) return <SntLoadingOverlay message="Loading config..." />
33
+
34
+ return (
35
+ <AppConfigContext.Provider value={config}>
36
+ {children}
37
+ </AppConfigContext.Provider>
38
+ )
39
+ }
40
+
41
+ export function useAppConfig() {
42
+ const ctx = useContext(AppConfigContext)
43
+ if (!ctx) throw new Error('useAppConfig must be used inside AppConfigProvider')
44
+ return ctx
45
+ }
@@ -17,34 +17,10 @@ export default {
17
17
  // Table widget
18
18
 
19
19
  // CheckboxList widget
20
- 'checkboxList.selectAll': 'Alle auswählen',
21
- 'checkboxList.deselectAll': 'Alle abwählen',
22
20
 
23
21
  // Sidepanel
24
- 'sidepanel.collapse': 'Panel einklappen',
25
- 'sidepanel.expand': 'Panel ausklappen',
26
22
 
27
23
  // DateRangePicker presets
28
- 'dateRange.period': 'Zeitraum:',
29
- 'dateRange.from': 'Von',
30
- 'dateRange.to': 'Bis',
31
- 'dateRange.preset.this_month': 'Dieser Monat',
32
- 'dateRange.preset.last_month': 'Letzter Monat',
33
- 'dateRange.preset.last_3_months': 'Letzte 3 Monate',
34
- 'dateRange.preset.last_6_months': 'Letzte 6 Monate',
35
- 'dateRange.preset.last_12_months': 'Letzte 12 Monate',
36
- 'dateRange.preset.last_24_months': 'Letzte 24 Monate',
37
- 'dateRange.preset.last_36_months': 'Letzte 36 Monate',
38
- 'dateRange.preset.this_year': 'Dieses Jahr',
39
- 'dateRange.preset.last_year': 'Letztes Jahr',
40
- 'dateRange.preset.all_time': 'Gesamter Zeitraum',
41
- 'dateRange.preset.custom': 'Benutzerdefiniert',
42
- 'dateRange.preset.next_3_months': 'Nächste 3 Monate',
43
- 'dateRange.preset.next_6_months': 'Nächste 6 Monate',
44
- 'dateRange.preset.next_12_months': 'Nächste 12 Monate',
45
- 'dateRange.preset.this_quarter': 'Dieses Quartal',
46
- 'dateRange.preset.next_quarter': 'Nächstes Quartal',
47
- 'dateRange.preset.rolling_12_months': 'Rollende 12 Monate',
48
24
 
49
25
  // OrganisationList page
50
26
  'orgList.title': 'Organisationen',
@@ -17,34 +17,10 @@ export default {
17
17
  // Table widget
18
18
 
19
19
  // CheckboxList widget
20
- 'checkboxList.selectAll': 'Select all',
21
- 'checkboxList.deselectAll': 'Deselect all',
22
20
 
23
21
  // Sidepanel
24
- 'sidepanel.collapse': 'Collapse panel',
25
- 'sidepanel.expand': 'Expand panel',
26
22
 
27
23
  // DateRangePicker presets
28
- 'dateRange.period': 'Period:',
29
- 'dateRange.from': 'From',
30
- 'dateRange.to': 'To',
31
- 'dateRange.preset.this_month': 'This month',
32
- 'dateRange.preset.last_month': 'Last month',
33
- 'dateRange.preset.last_3_months': 'Last 3 months',
34
- 'dateRange.preset.last_6_months': 'Last 6 months',
35
- 'dateRange.preset.last_12_months': 'Last 12 months',
36
- 'dateRange.preset.last_24_months': 'Last 24 months',
37
- 'dateRange.preset.last_36_months': 'Last 36 months',
38
- 'dateRange.preset.this_year': 'This year',
39
- 'dateRange.preset.last_year': 'Last year',
40
- 'dateRange.preset.all_time': 'All time',
41
- 'dateRange.preset.custom': 'Custom',
42
- 'dateRange.preset.next_3_months': 'Next 3 months',
43
- 'dateRange.preset.next_6_months': 'Next 6 months',
44
- 'dateRange.preset.next_12_months': 'Next 12 months',
45
- 'dateRange.preset.this_quarter': 'This quarter',
46
- 'dateRange.preset.next_quarter': 'Next quarter',
47
- 'dateRange.preset.rolling_12_months': 'Rolling 12 months',
48
24
 
49
25
  // Auth dialog
50
26
  'auth.dialog.title': 'Sensolus API key required',
@@ -17,34 +17,10 @@ export default {
17
17
  // Table widget
18
18
 
19
19
  // CheckboxList widget
20
- 'checkboxList.selectAll': 'Seleccionar todo',
21
- 'checkboxList.deselectAll': 'Deseleccionar todo',
22
20
 
23
21
  // Sidepanel
24
- 'sidepanel.collapse': 'Contraer panel',
25
- 'sidepanel.expand': 'Expandir panel',
26
22
 
27
23
  // DateRangePicker presets
28
- 'dateRange.period': 'Período:',
29
- 'dateRange.from': 'Desde',
30
- 'dateRange.to': 'Hasta',
31
- 'dateRange.preset.this_month': 'Este mes',
32
- 'dateRange.preset.last_month': 'Mes pasado',
33
- 'dateRange.preset.last_3_months': 'Últimos 3 meses',
34
- 'dateRange.preset.last_6_months': 'Últimos 6 meses',
35
- 'dateRange.preset.last_12_months': 'Últimos 12 meses',
36
- 'dateRange.preset.last_24_months': 'Últimos 24 meses',
37
- 'dateRange.preset.last_36_months': 'Últimos 36 meses',
38
- 'dateRange.preset.this_year': 'Este año',
39
- 'dateRange.preset.last_year': 'Año pasado',
40
- 'dateRange.preset.all_time': 'Todo el período',
41
- 'dateRange.preset.custom': 'Personalizado',
42
- 'dateRange.preset.next_3_months': 'Próximos 3 meses',
43
- 'dateRange.preset.next_6_months': 'Próximos 6 meses',
44
- 'dateRange.preset.next_12_months': 'Próximos 12 meses',
45
- 'dateRange.preset.this_quarter': 'Este trimestre',
46
- 'dateRange.preset.next_quarter': 'Próximo trimestre',
47
- 'dateRange.preset.rolling_12_months': '12 meses continuos',
48
24
 
49
25
  // OrganisationList page
50
26
  'orgList.title': 'Organizaciones',
@@ -17,34 +17,10 @@ export default {
17
17
  // Table widget
18
18
 
19
19
  // CheckboxList widget
20
- 'checkboxList.selectAll': 'Tout sélectionner',
21
- 'checkboxList.deselectAll': 'Tout désélectionner',
22
20
 
23
21
  // Sidepanel
24
- 'sidepanel.collapse': 'Réduire le panneau',
25
- 'sidepanel.expand': 'Développer le panneau',
26
22
 
27
23
  // DateRangePicker presets
28
- 'dateRange.period': 'Période :',
29
- 'dateRange.from': 'Du',
30
- 'dateRange.to': 'Au',
31
- 'dateRange.preset.this_month': 'Ce mois-ci',
32
- 'dateRange.preset.last_month': 'Le mois dernier',
33
- 'dateRange.preset.last_3_months': '3 derniers mois',
34
- 'dateRange.preset.last_6_months': '6 derniers mois',
35
- 'dateRange.preset.last_12_months': '12 derniers mois',
36
- 'dateRange.preset.last_24_months': '24 derniers mois',
37
- 'dateRange.preset.last_36_months': '36 derniers mois',
38
- 'dateRange.preset.this_year': 'Cette année',
39
- 'dateRange.preset.last_year': 'L\'année dernière',
40
- 'dateRange.preset.all_time': 'Tout',
41
- 'dateRange.preset.custom': 'Personnalisé',
42
- 'dateRange.preset.next_3_months': '3 prochains mois',
43
- 'dateRange.preset.next_6_months': '6 prochains mois',
44
- 'dateRange.preset.next_12_months': '12 prochains mois',
45
- 'dateRange.preset.this_quarter': 'Ce trimestre',
46
- 'dateRange.preset.next_quarter': 'Trimestre prochain',
47
- 'dateRange.preset.rolling_12_months': '12 mois glissants',
48
24
 
49
25
  // OrganisationList page
50
26
  'orgList.title': 'Organisations',
@@ -17,34 +17,10 @@ export default {
17
17
  // Table widget
18
18
 
19
19
  // CheckboxList widget
20
- 'checkboxList.selectAll': 'Alles selecteren',
21
- 'checkboxList.deselectAll': 'Alles deselecteren',
22
20
 
23
21
  // Sidepanel
24
- 'sidepanel.collapse': 'Paneel inklappen',
25
- 'sidepanel.expand': 'Paneel uitklappen',
26
22
 
27
23
  // DateRangePicker presets
28
- 'dateRange.period': 'Periode:',
29
- 'dateRange.from': 'Van',
30
- 'dateRange.to': 'Tot',
31
- 'dateRange.preset.this_month': 'Deze maand',
32
- 'dateRange.preset.last_month': 'Vorige maand',
33
- 'dateRange.preset.last_3_months': 'Laatste 3 maanden',
34
- 'dateRange.preset.last_6_months': 'Laatste 6 maanden',
35
- 'dateRange.preset.last_12_months': 'Laatste 12 maanden',
36
- 'dateRange.preset.last_24_months': 'Laatste 24 maanden',
37
- 'dateRange.preset.last_36_months': 'Laatste 36 maanden',
38
- 'dateRange.preset.this_year': 'Dit jaar',
39
- 'dateRange.preset.last_year': 'Vorig jaar',
40
- 'dateRange.preset.all_time': 'Alles',
41
- 'dateRange.preset.custom': 'Aangepast',
42
- 'dateRange.preset.next_3_months': 'Volgende 3 maanden',
43
- 'dateRange.preset.next_6_months': 'Volgende 6 maanden',
44
- 'dateRange.preset.next_12_months': 'Volgende 12 maanden',
45
- 'dateRange.preset.this_quarter': 'Dit kwartaal',
46
- 'dateRange.preset.next_quarter': 'Volgend kwartaal',
47
- 'dateRange.preset.rolling_12_months': 'Rollende 12 maanden',
48
24
 
49
25
  // OrganisationList page
50
26
  'orgList.title': 'Organisaties',
@@ -1,14 +1,11 @@
1
1
  import React from 'react'
2
2
  import ReactDOM from 'react-dom/client'
3
3
  import App from './App'
4
- import { LocaleProvider, messages } from './i18n'
5
4
  import '@sensolus/snt-agent-kit/theme.css'
6
5
  import './styles/app.css'
7
6
 
8
7
  ReactDOM.createRoot(document.getElementById('root')).render(
9
8
  <React.StrictMode>
10
- <LocaleProvider messages={messages}>
11
- <App />
12
- </LocaleProvider>
9
+ <App />
13
10
  </React.StrictMode>
14
11
  )
@@ -9,10 +9,12 @@ import {
9
9
  SntSpinner,
10
10
  } from '@sensolus/snt-agent-kit'
11
11
  import { useLocale, formatNumber } from '../i18n'
12
+ import { useAppConfig } from '../AppConfigContext'
12
13
 
13
14
  export function OrganisationDetail() {
14
15
  const { id } = useParams()
15
16
  const { t, intlLocale } = useLocale()
17
+ const config = useAppConfig()
16
18
 
17
19
  // Try to get org data from sessionStorage
18
20
  const organisation = useMemo(() => {
@@ -213,6 +215,8 @@ export function OrganisationDetail() {
213
215
  </div>
214
216
  ) : (
215
217
  <SntMap
218
+ mapboxKey={config.mapboxKey}
219
+ locationiqKey={config.locationiqKey}
216
220
  height="500px"
217
221
  devices={devices}
218
222
  orgId={id}
@@ -28,6 +28,7 @@ import {
28
28
  SntToolbarSpacer,
29
29
  getDefaultDateRange,
30
30
  } from '@sensolus/snt-agent-kit'
31
+ import { useAppConfig } from '../AppConfigContext'
31
32
 
32
33
  function Section({ title, description, children }) {
33
34
  return (
@@ -88,12 +89,13 @@ const TAB_DEFS = [
88
89
  ]
89
90
 
90
91
  export function WidgetShowcase() {
92
+ const config = useAppConfig()
91
93
  const [inputValue, setInputValue] = useState('Hello world')
92
94
  const [selectValue, setSelectValue] = useState('eu')
93
95
  const [groupValue, setGroupValue] = useState('cards')
94
96
  const [switchOn, setSwitchOn] = useState(true)
95
97
  const [checkboxSelected, setCheckboxSelected] = useState(['Trackers', 'Geozones'])
96
- const [dateRange, setDateRange] = useState(() => getDefaultDateRange('last_3_months'))
98
+ const [dateRange, setDateRange] = useState(() => getDefaultDateRange('week'))
97
99
  const [dialogOpen, setDialogOpen] = useState(false)
98
100
  const [showOverlay, setShowOverlay] = useState(false)
99
101
  const [sidepanelOpen, setSidepanelOpen] = useState(true)
@@ -245,14 +247,10 @@ export function WidgetShowcase() {
245
247
  {/* ------------------------------------------------------------------ */}
246
248
  <Section
247
249
  title="SntDateRangePicker"
248
- description="Range selection with locale-aware presets. Receives ISO yyyy-MM-dd."
250
+ description="Range selection with day / week / month / custom modes. Value is { viewMode, start, end } as JS Dates."
249
251
  >
250
- <Example label={`Current: ${dateRange.from} → ${dateRange.to}`}>
251
- <SntDateRangePicker
252
- from={dateRange.from}
253
- to={dateRange.to}
254
- onChange={setDateRange}
255
- />
252
+ <Example label={`Current: ${dateRange.start.toDateString()} → ${dateRange.end.toDateString()} (${dateRange.viewMode})`}>
253
+ <SntDateRangePicker value={dateRange} onChange={setDateRange} />
256
254
  </Example>
257
255
  </Section>
258
256
 
@@ -487,6 +485,8 @@ export function WidgetShowcase() {
487
485
  description="Leaflet map with Street/Satellite layer toggle. Optionally renders geozones (by orgId or array) and device markers."
488
486
  >
489
487
  <SntMap
488
+ mapboxKey={config.mapboxKey}
489
+ locationiqKey={config.locationiqKey}
490
490
  height="320px"
491
491
  center={[50.85, 4.35]}
492
492
  zoom={6}
@@ -283,7 +283,7 @@ body {
283
283
  }
284
284
 
285
285
  .summary-stats-row .summary-stat-info .summary-value {
286
- color: var(--snt-infra);
286
+ color: var(--snt-ui-selected);
287
287
  }
288
288
 
289
289
  .summary-stats-row .summary-stat-success .summary-value {