Skip to content

Commit b869712

Browse files
committed
Start collecting heartbeat pings from weather station online
1 parent fcdd661 commit b869712

File tree

8 files changed

+212
-1
lines changed

8 files changed

+212
-1
lines changed

website/app/routes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ export default [
1010
"/upload-from-weather-station/:secret",
1111
"./routes/uploadFromWeatherStation.ts"
1212
),
13+
route(
14+
"/heartbeat-from-weather-station/:secret",
15+
"./routes/heartbeatFromWeatherStation.ts"
16+
),
1317
route("/status", "./routes/status.ts"),
1418
layout("./routes/layout.tsx", [
1519
route("/download", "./routes/download.tsx"),
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { sql } from "drizzle-orm";
2+
import { data } from "react-router";
3+
import { Heartbeats } from "../../database/schema.d";
4+
import type { Route } from "./+types/heartbeatFromWeatherStation";
5+
6+
export async function loader({ request, context, params }: Route.LoaderArgs) {
7+
if (request.method !== "HEAD") {
8+
return data(
9+
{
10+
error: "Invalid request method",
11+
},
12+
{ status: 400 }
13+
);
14+
}
15+
const uploadSecret = await context.cloudflare.env.KV.get(
16+
"UPLOAD_FROM_WEATHER_STATION_SECRET"
17+
);
18+
if (params.secret !== uploadSecret) {
19+
return data(
20+
{
21+
error: "Incorrect secret",
22+
},
23+
{ status: 401 }
24+
);
25+
}
26+
27+
context.cloudflare.ctx.waitUntil(
28+
context.db
29+
.insert(Heartbeats)
30+
.values({
31+
hourStartTimestamp: sql`unixepoch(strftime('%Y-%m-%d %H:00:00', 'now'))`,
32+
pingCount: 1,
33+
})
34+
.onConflictDoUpdate({
35+
target: Heartbeats.hourStartTimestamp,
36+
set: { pingCount: sql`${Heartbeats.pingCount} + 1` },
37+
})
38+
);
39+
40+
return new Response(null, { status: 204 }); // This is a HEAD request, so we need to return a 204 response
41+
}

website/app/routes/uploadFromWeatherStation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
} from "../../database/schema.d";
66
import type { Route } from "./+types/uploadFromWeatherStation";
77

8-
export async function action({ request, context, params }: Route.LoaderArgs) {
8+
export async function action({ request, context, params }: Route.ActionArgs) {
99
if (request.method !== "PUT" || !request.body) {
1010
return data(
1111
{
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
CREATE TABLE `heartbeats` (
2+
`hour_start_timestamp` integer PRIMARY KEY NOT NULL,
3+
`ping_count` integer DEFAULT 0 NOT NULL
4+
);
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
{
2+
"version": "6",
3+
"dialect": "sqlite",
4+
"id": "2f57c7cd-0a6e-4818-b5e9-f7e278994bff",
5+
"prevId": "782a665b-d3ce-44c1-9cb3-38e523dd78fe",
6+
"tables": {
7+
"disregarded_observations": {
8+
"name": "disregarded_observations",
9+
"columns": {
10+
"id": {
11+
"name": "id",
12+
"type": "integer",
13+
"primaryKey": true,
14+
"notNull": true,
15+
"autoincrement": true
16+
},
17+
"timestamp": {
18+
"name": "timestamp",
19+
"type": "integer",
20+
"primaryKey": false,
21+
"notNull": true,
22+
"autoincrement": false
23+
},
24+
"created_at": {
25+
"name": "created_at",
26+
"type": "integer",
27+
"primaryKey": false,
28+
"notNull": true,
29+
"autoincrement": false
30+
},
31+
"data": {
32+
"name": "data",
33+
"type": "text",
34+
"primaryKey": false,
35+
"notNull": false,
36+
"autoincrement": false
37+
},
38+
"disregardReasonFriendly": {
39+
"name": "disregardReasonFriendly",
40+
"type": "text",
41+
"primaryKey": false,
42+
"notNull": true,
43+
"autoincrement": false
44+
},
45+
"disregardReasonDetailed": {
46+
"name": "disregardReasonDetailed",
47+
"type": "text",
48+
"primaryKey": false,
49+
"notNull": true,
50+
"autoincrement": false
51+
}
52+
},
53+
"indexes": {},
54+
"foreignKeys": {},
55+
"compositePrimaryKeys": {},
56+
"uniqueConstraints": {},
57+
"checkConstraints": {}
58+
},
59+
"heartbeats": {
60+
"name": "heartbeats",
61+
"columns": {
62+
"hour_start_timestamp": {
63+
"name": "hour_start_timestamp",
64+
"type": "integer",
65+
"primaryKey": true,
66+
"notNull": true,
67+
"autoincrement": false
68+
},
69+
"ping_count": {
70+
"name": "ping_count",
71+
"type": "integer",
72+
"primaryKey": false,
73+
"notNull": true,
74+
"autoincrement": false,
75+
"default": 0
76+
}
77+
},
78+
"indexes": {},
79+
"foreignKeys": {},
80+
"compositePrimaryKeys": {},
81+
"uniqueConstraints": {},
82+
"checkConstraints": {}
83+
},
84+
"observations": {
85+
"name": "observations",
86+
"columns": {
87+
"id": {
88+
"name": "id",
89+
"type": "integer",
90+
"primaryKey": true,
91+
"notNull": true,
92+
"autoincrement": true
93+
},
94+
"timestamp": {
95+
"name": "timestamp",
96+
"type": "integer",
97+
"primaryKey": false,
98+
"notNull": true,
99+
"autoincrement": false
100+
},
101+
"created_at": {
102+
"name": "created_at",
103+
"type": "integer",
104+
"primaryKey": false,
105+
"notNull": true,
106+
"autoincrement": false
107+
},
108+
"data": {
109+
"name": "data",
110+
"type": "text",
111+
"primaryKey": false,
112+
"notNull": true,
113+
"autoincrement": false
114+
},
115+
"exported_to_r2": {
116+
"name": "exported_to_r2",
117+
"type": "integer",
118+
"primaryKey": false,
119+
"notNull": true,
120+
"autoincrement": false,
121+
"default": false
122+
}
123+
},
124+
"indexes": {},
125+
"foreignKeys": {},
126+
"compositePrimaryKeys": {},
127+
"uniqueConstraints": {},
128+
"checkConstraints": {}
129+
}
130+
},
131+
"views": {},
132+
"enums": {},
133+
"_meta": {
134+
"schemas": {},
135+
"tables": {},
136+
"columns": {}
137+
},
138+
"internal": {
139+
"indexes": {}
140+
}
141+
}

website/database/migrations/meta/_journal.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@
2222
"when": 1744533965718,
2323
"tag": "0002_tiresome_storm",
2424
"breakpoints": true
25+
},
26+
{
27+
"idx": 3,
28+
"version": "6",
29+
"when": 1754915033969,
30+
"tag": "0003_powerful_dagger",
31+
"breakpoints": true
2532
}
2633
]
2734
}

website/database/schema.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from "./schema/DisregardedObservations";
2+
export * from "./schema/Heartbeats";
23
export * from "./schema/Observations";
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { sql } from "drizzle-orm";
2+
import { integer, sqliteTable } from "drizzle-orm/sqlite-core";
3+
4+
export const Heartbeats = sqliteTable(
5+
"heartbeats",
6+
{
7+
hourStartTimestamp: integer("hour_start_timestamp", { mode: "timestamp" })
8+
.primaryKey()
9+
.$defaultFn(() => sql`(unixepoch(strftime('%Y-%m-%d %H:00:00', 'now')))`), // Time of the heartbeat
10+
pingCount: integer("ping_count").notNull().default(0),
11+
},
12+
(table) => []
13+
);

0 commit comments

Comments
 (0)