Skip to content

Commit 9f300d7

Browse files
authored
feat: 🔥 are we getting somewhere ?
1 parent f59a465 commit 9f300d7

12 files changed

Lines changed: 226 additions & 181 deletions

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ pki
3131
.claude/settings.local.json
3232

3333
reference/
34-
34+
integrations/

README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,72 @@ But you don't have to chose, both InfluxDB and Prometheus can replicate their da
1414

1515
Of course you can also use Sensapp as a standalone time-series database.
1616

17+
## Quickstart
18+
19+
The quickest way to run SensApp is with SQLite so no external database is required.
20+
21+
By default, SensApp listens on `127.0.0.1:3000`.
22+
23+
### Local run
24+
25+
Start SensApp with SQLite:
26+
27+
```bash
28+
SENSAPP_STORAGE_CONNECTION_STRING=sqlite://sensapp.db \
29+
cargo run --no-default-features --features sqlite
30+
```
31+
32+
Check that the server is ready:
33+
34+
```bash
35+
curl http://127.0.0.1:3000/health/ready
36+
```
37+
38+
Open the API documentation:
39+
40+
```bash
41+
curl http://127.0.0.1:3000/docs
42+
```
43+
44+
Ingest one sample:
45+
46+
```bash
47+
curl -X POST http://127.0.0.1:3000/publish \
48+
-H 'content-type: text/csv' \
49+
--data-raw $'datetime,sensor_name,value,unit\n2026-03-16T12:00:00Z,temperature,21.5,C'
50+
```
51+
52+
Query the stored data:
53+
54+
```bash
55+
curl 'http://127.0.0.1:3000/api/v1/query?query=temperature'
56+
curl 'http://127.0.0.1:3000/api/v1/query?query=temperature&format=csv'
57+
```
58+
59+
### Container run
60+
61+
Build the image:
62+
63+
```bash
64+
docker build -t sensapp:local .
65+
```
66+
67+
Run the container with SQLite:
68+
69+
```bash
70+
docker run --rm -p 3000:3000 \
71+
-e SENSAPP_STORAGE_CONNECTION_STRING=sqlite:///var/lib/sensapp/sensapp.db \
72+
sensapp:local
73+
```
74+
75+
Check that the server is ready:
76+
77+
```bash
78+
curl http://127.0.0.1:3000/health/ready
79+
```
80+
81+
If you run `cargo run` with the repository defaults, SensApp will try to connect to PostgreSQL on `localhost:5432`.
82+
1783
## Features
1884

1985
- **HTTP REST API**
@@ -67,6 +133,7 @@ export SENSAPP_JWT_SECRET="my-super-secret-key-at-least-32-characters-long"
67133
```
68134

69135
When enabled:
136+
70137
- **Public endpoints** (health checks, docs, `/prometheus/metrics`) remain open.
71138
- **Read endpoints** (`/metrics`, `/series`, queries) require a token with `read` scope.
72139
- **Write endpoints** (`/publish`, InfluxDB/Prometheus write) require a token with `write` scope.

frontend/src/App.test.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,23 +117,23 @@ describe('App integration', () => {
117117
expect(useSelectionStore.getState().selectedMetric).toBe('temperature');
118118
});
119119

120-
it('shows the series section after a metric is selected', async () => {
120+
it('shows the series panel after a metric is selected', async () => {
121121
const user = userEvent.setup();
122122
render(<App />, { wrapper: createWrapper() });
123123

124124
// Select the metric
125125
const tempRow = await screen.findByText('temperature');
126126
await user.click(tempRow);
127127

128-
// Series section header should appear
129-
expect(await screen.findByText('Series for')).toBeInTheDocument();
128+
// Series panel should show the metric name
129+
expect(await screen.findByText((_, el) => el?.tagName === 'CODE' && el?.textContent === 'temperature')).toBeInTheDocument();
130130
});
131131

132-
it('shows empty guidance when no metric is selected', async () => {
132+
it('shows chart placeholder when no series selected', async () => {
133133
render(<App />, { wrapper: createWrapper() });
134134
await screen.findByText('temperature'); // wait for load
135135

136-
expect(screen.getByText(/Select a metric above/)).toBeInTheDocument();
136+
expect(screen.getByText(/Select a metric, then choose series to chart/)).toBeInTheDocument();
137137
});
138138

139139
it('renders footer', () => {

frontend/src/components/Layout.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { HealthBadge } from './HealthBadge';
33

44
export function Layout() {
55
return (
6-
<div className="min-h-screen flex flex-col bg-base-200">
6+
<div className="h-screen flex flex-col bg-base-200 overflow-hidden">
77
<header className="bg-base-100 border-b border-base-300 sticky top-0 z-50">
88
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
99
<div className="flex items-center justify-between h-14">
@@ -36,17 +36,9 @@ export function Layout() {
3636
</div>
3737
</header>
3838

39-
<main className="flex-1 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-6">
39+
<main className="flex-1 min-h-0 max-w-7xl w-full mx-auto px-3 sm:px-4 lg:px-6 py-3">
4040
<Outlet />
4141
</main>
42-
43-
<footer className="border-t border-base-300 bg-base-100">
44-
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-3">
45-
<p className="text-xs text-center text-base-content/40">
46-
SensApp &mdash; Sensor Data Platform by SINTEF
47-
</p>
48-
</div>
49-
</footer>
5042
</div>
5143
);
5244
}

frontend/src/components/MetricsTable.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ describe('MetricsTable', () => {
7373
it('shows loading state while fetching metrics', () => {
7474
mockListMetrics.mockReturnValue(new Promise(() => {})); // never resolves
7575
render(<MetricsTable />, { wrapper: createWrapper() });
76-
expect(screen.getByText('Loading metrics...')).toBeInTheDocument();
76+
expect(screen.getByText('Loading...')).toBeInTheDocument();
7777
});
7878

7979
it('renders metrics with type badges and series counts', async () => {

frontend/src/components/MetricsTable.tsx

Lines changed: 33 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -55,47 +55,38 @@ export function MetricsTable() {
5555
}
5656

5757
return (
58-
<div className="space-y-3">
59-
<div className="flex flex-wrap gap-3 items-end">
60-
<div className="form-control flex-1 min-w-48 max-w-xs">
61-
<label className="label pb-1">
62-
<span className="label-text text-xs font-medium text-base-content/60">Search metrics</span>
63-
</label>
64-
<input
65-
type="text"
66-
placeholder="Filter by name..."
67-
className="input input-bordered input-sm"
68-
value={nameFilter}
69-
onChange={(e) => setNameFilter(e.target.value)}
70-
/>
71-
</div>
72-
<div className="form-control">
73-
<label className="label pb-1">
74-
<span className="label-text text-xs font-medium text-base-content/60">Type</span>
75-
</label>
76-
<select
77-
className="select select-bordered select-sm"
78-
value={typeFilter}
79-
onChange={(e) => setTypeFilter(e.target.value)}
80-
>
81-
{SENSOR_TYPES.map((t) => (
82-
<option key={t} value={t}>
83-
{t || 'All types'}
84-
</option>
85-
))}
86-
</select>
87-
</div>
58+
<div className="flex flex-col h-full gap-2">
59+
<div className="flex flex-wrap gap-2 items-center shrink-0">
60+
<input
61+
type="text"
62+
placeholder="Filter by name..."
63+
className="input input-bordered input-xs text-xs flex-1 min-w-36 max-w-xs h-7"
64+
value={nameFilter}
65+
onChange={(e) => setNameFilter(e.target.value)}
66+
/>
67+
<select
68+
className="select select-bordered select-xs text-xs h-7"
69+
value={typeFilter}
70+
onChange={(e) => setTypeFilter(e.target.value)}
71+
>
72+
{SENSOR_TYPES.map((t) => (
73+
<option key={t} value={t}>
74+
{t || 'All types'}
75+
</option>
76+
))}
77+
</select>
8878
{!isLoading && !error && (
89-
<span className="text-xs text-base-content/40 pb-2">
79+
<span className="text-xs text-base-content/40">
9080
{metrics.length} metric{metrics.length !== 1 ? 's' : ''}
9181
</span>
9282
)}
9383
</div>
9484

85+
<div className="flex-1 min-h-0 overflow-y-auto overflow-x-auto">
9586
{isLoading && (
96-
<div className="flex items-center justify-center gap-2 py-12">
97-
<span className="loading loading-spinner loading-sm text-primary" />
98-
<span className="text-sm text-base-content/50">Loading metrics...</span>
87+
<div className="flex items-center justify-center gap-2 py-6">
88+
<span className="loading loading-spinner loading-xs text-primary" />
89+
<span className="text-xs text-base-content/50">Loading...</span>
9990
</div>
10091
)}
10192

@@ -109,8 +100,8 @@ export function MetricsTable() {
109100
)}
110101

111102
{!isLoading && !error && metrics.length === 0 && (
112-
<div className="text-center py-12">
113-
<p className="text-sm text-base-content/40">No metrics found</p>
103+
<div className="text-center py-6">
104+
<p className="text-xs text-base-content/40">No metrics found</p>
114105
{(nameFilter || typeFilter) && (
115106
<button
116107
className="btn btn-ghost btn-xs mt-2"
@@ -123,8 +114,8 @@ export function MetricsTable() {
123114
)}
124115

125116
{!isLoading && !error && metrics.length > 0 && (
126-
<div className="overflow-x-auto -mx-1">
127-
<table className="table table-sm w-full">
117+
<div>
118+
<table className="table table-xs w-full">
128119
<thead>
129120
<tr className="text-xs text-base-content/50">
130121
<th className="font-medium">Metric Name</th>
@@ -150,17 +141,17 @@ export function MetricsTable() {
150141
onClick={() => handleSelectMetric(metric)}
151142
>
152143
<td>
153-
<div className="font-mono text-sm font-medium">{name}</div>
144+
<span className="font-mono text-xs font-medium">{name}</span>
154145
{metric['sensor:unit'] && (
155-
<div className="text-xs text-base-content/40 mt-0.5">Unit: {metric['sensor:unit']}</div>
146+
<span className="text-xs text-base-content/40 ml-1">({metric['sensor:unit']})</span>
156147
)}
157148
</td>
158149
<td>
159150
<span className={`badge badge-sm ${sensorTypeBadgeClass(metric['sensor:type'])}`}>
160151
{metric['sensor:type']}
161152
</span>
162153
</td>
163-
<td className="tabular-nums text-sm">
154+
<td className="tabular-nums text-xs">
164155
{seriesCount ?? '—'}
165156
</td>
166157
<td className="hidden sm:table-cell">
@@ -182,6 +173,7 @@ export function MetricsTable() {
182173
</table>
183174
</div>
184175
)}
176+
</div>
185177
</div>
186178
);
187179
}

frontend/src/components/SeriesTable.tsx

Lines changed: 26 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -64,45 +64,34 @@ export function SeriesTable() {
6464
}
6565

6666
return (
67-
<div className="space-y-3">
68-
<div className="flex flex-wrap gap-3 items-end">
69-
<div className="form-control flex-1 min-w-56 max-w-sm">
70-
<label className="label pb-1">
71-
<span className="label-text text-xs font-medium text-base-content/60">
72-
PromQL Label Selector
73-
</span>
74-
</label>
75-
<input
76-
type="text"
77-
placeholder='{env="prod", region=~"us.*"}'
78-
className="input input-bordered input-sm font-mono text-xs"
79-
value={selectorInput}
80-
onChange={(e) => setSelectorInput(e.target.value)}
81-
/>
82-
</div>
83-
<div className="form-control min-w-36 max-w-48">
84-
<label className="label pb-1">
85-
<span className="label-text text-xs font-medium text-base-content/60">Quick filter</span>
86-
</label>
87-
<input
88-
type="text"
89-
placeholder="Filter labels..."
90-
className="input input-bordered input-sm"
91-
value={labelFilter}
92-
onChange={(e) => setLabelFilter(e.target.value)}
93-
/>
94-
</div>
67+
<div className="flex flex-col h-full gap-2">
68+
<div className="flex flex-wrap gap-2 items-center shrink-0">
69+
<input
70+
type="text"
71+
placeholder='{env="prod", region=~"us.*"}'
72+
className="input input-bordered input-xs font-mono text-xs flex-1 min-w-44 max-w-xs h-7"
73+
value={selectorInput}
74+
onChange={(e) => setSelectorInput(e.target.value)}
75+
/>
76+
<input
77+
type="text"
78+
placeholder="Quick filter..."
79+
className="input input-bordered input-xs text-xs min-w-28 max-w-40 h-7"
80+
value={labelFilter}
81+
onChange={(e) => setLabelFilter(e.target.value)}
82+
/>
9583
{selectedSeries.length > 0 && (
96-
<span className="text-xs text-primary font-medium pb-2">
84+
<span className="text-xs text-primary font-medium">
9785
{selectedSeries.length} selected
9886
</span>
9987
)}
10088
</div>
10189

90+
<div className="flex-1 min-h-0 overflow-y-auto overflow-x-auto">
10291
{isLoading && (
103-
<div className="flex items-center justify-center gap-2 py-12">
104-
<span className="loading loading-spinner loading-sm text-primary" />
105-
<span className="text-sm text-base-content/50">Loading series...</span>
92+
<div className="flex items-center justify-center gap-2 py-6">
93+
<span className="loading loading-spinner loading-xs text-primary" />
94+
<span className="text-xs text-base-content/50">Loading...</span>
10695
</div>
10796
)}
10897

@@ -116,16 +105,16 @@ export function SeriesTable() {
116105
)}
117106

118107
{!isLoading && !error && filteredSeries.length === 0 && (
119-
<div className="text-center py-8">
120-
<p className="text-sm text-base-content/40">
108+
<div className="text-center py-6">
109+
<p className="text-xs text-base-content/40">
121110
{series.length === 0 ? 'No series found for this metric' : 'No series match the current filter'}
122111
</p>
123112
</div>
124113
)}
125114

126115
{!isLoading && !error && filteredSeries.length > 0 && (
127-
<div className="overflow-x-auto -mx-1">
128-
<table className="table table-sm w-full">
116+
<div>
117+
<table className="table table-xs w-full">
129118
<thead>
130119
<tr className="text-xs text-base-content/50">
131120
<th className="w-8 font-medium">
@@ -195,6 +184,7 @@ export function SeriesTable() {
195184
</div>
196185
</div>
197186
)}
187+
</div>
198188
</div>
199189
);
200190
}

frontend/src/components/TimeRangeSelector.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ describe('TimeRangeSelector', () => {
4444
expect(diffMs).toBeCloseTo(24 * 60 * 60 * 1000, -3);
4545
});
4646

47-
it('renders From and To datetime inputs', () => {
47+
it('renders from and to datetime inputs', () => {
4848
render(<TimeRangeSelector />);
49-
expect(screen.getByText('From')).toBeInTheDocument();
50-
expect(screen.getByText('To')).toBeInTheDocument();
49+
expect(screen.getByText('from')).toBeInTheDocument();
50+
expect(screen.getByText('to')).toBeInTheDocument();
5151
});
5252
});

0 commit comments

Comments
 (0)