From 9419bc1b2e426d79dd8341ed155f1f6e44af567d Mon Sep 17 00:00:00 2001 From: jona159 Date: Tue, 10 Mar 2026 14:09:02 +0100 Subject: [PATCH 1/9] feat: add archived at field --- app/schema/device.ts | 1 + drizzle/0034_mute_ultragirl.sql | 1 + drizzle/meta/0034_snapshot.json | 1336 +++++++++++++++++++++++++++++++ 3 files changed, 1338 insertions(+) create mode 100644 drizzle/0034_mute_ultragirl.sql create mode 100644 drizzle/meta/0034_snapshot.json diff --git a/app/schema/device.ts b/app/schema/device.ts index db58f8a6..20d5a6c5 100644 --- a/app/schema/device.ts +++ b/app/schema/device.ts @@ -46,6 +46,7 @@ export const device = pgTable('device', { public: boolean('public').default(false), createdAt: timestamp('created_at').defaultNow().notNull(), updatedAt: timestamp('updated_at').defaultNow().notNull(), + archivedAt: timestamp('archived_at'), expiresAt: date('expires_at', { mode: 'date' }), latitude: doublePrecision('latitude').notNull(), longitude: doublePrecision('longitude').notNull(), diff --git a/drizzle/0034_mute_ultragirl.sql b/drizzle/0034_mute_ultragirl.sql new file mode 100644 index 00000000..13e0b7f9 --- /dev/null +++ b/drizzle/0034_mute_ultragirl.sql @@ -0,0 +1 @@ +ALTER TABLE "device" ADD COLUMN "archived_at" timestamp; \ No newline at end of file diff --git a/drizzle/meta/0034_snapshot.json b/drizzle/meta/0034_snapshot.json new file mode 100644 index 00000000..9a6f889b --- /dev/null +++ b/drizzle/meta/0034_snapshot.json @@ -0,0 +1,1336 @@ +{ + "id": "70db0852-369c-47bf-9b12-f9b6f8295fbf", + "prevId": "2df45cfc-982c-4c7c-b2e7-6fe47039d121", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.device": { + "name": "device", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "ARRAY[]::text[]" + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "use_auth": { + "name": "use_auth", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "apiKey": { + "name": "apiKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "exposure": { + "name": "exposure", + "type": "exposure", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'inactive'" + }, + "model": { + "name": "model", + "type": "model", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'custom'" + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "latitude": { + "name": "latitude", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "longitude": { + "name": "longitude", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sensor_wiki_model": { + "name": "sensor_wiki_model", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_to_location": { + "name": "device_to_location", + "schema": "", + "columns": { + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "location_id": { + "name": "location_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "time": { + "name": "time", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "device_to_location_device_id_device_id_fk": { + "name": "device_to_location_device_id_device_id_fk", + "tableFrom": "device_to_location", + "tableTo": "device", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "device_to_location_location_id_location_id_fk": { + "name": "device_to_location_location_id_location_id_fk", + "tableFrom": "device_to_location", + "tableTo": "location", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "device_to_location_device_id_location_id_time_pk": { + "name": "device_to_location_device_id_location_id_time_pk", + "columns": [ + "device_id", + "location_id", + "time" + ] + } + }, + "uniqueConstraints": { + "device_to_location_device_id_location_id_time_unique": { + "name": "device_to_location_device_id_location_id_time_unique", + "nullsNotDistinct": false, + "columns": [ + "device_id", + "location_id", + "time" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.measurement": { + "name": "measurement", + "schema": "", + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "value": { + "name": "value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "location_id": { + "name": "location_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "measurement_location_id_location_id_fk": { + "name": "measurement_location_id_location_id_fk", + "tableFrom": "measurement", + "tableTo": "location", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "measurement_sensor_id_time_unique": { + "name": "measurement_sensor_id_time_unique", + "nullsNotDistinct": false, + "columns": [ + "sensor_id", + "time" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.password": { + "name": "password", + "schema": "", + "columns": { + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "password_user_id_user_id_fk": { + "name": "password_user_id_user_id_fk", + "tableFrom": "password", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.password_reset_request": { + "name": "password_reset_request", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "password_reset_request_user_id_user_id_fk": { + "name": "password_reset_request_user_id_user_id_fk", + "tableFrom": "password_reset_request", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "password_reset_request_user_id_unique": { + "name": "password_reset_request_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile": { + "name": "profile", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "profile_user_id_user_id_fk": { + "name": "profile_user_id_user_id_fk", + "tableFrom": "profile", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "profile_username_unique": { + "name": "profile_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile_image": { + "name": "profile_image", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "alt_text": { + "name": "alt_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "blob": { + "name": "blob", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "profile_image_profile_id_profile_id_fk": { + "name": "profile_image_profile_id_profile_id_fk", + "tableFrom": "profile_image", + "tableTo": "profile", + "columnsFrom": [ + "profile_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sensor": { + "name": "sensor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sensor_type": { + "name": "sensor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'inactive'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sensor_wiki_type": { + "name": "sensor_wiki_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sensor_wiki_phenomenon": { + "name": "sensor_wiki_phenomenon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sensor_wiki_unit": { + "name": "sensor_wiki_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lastMeasurement": { + "name": "lastMeasurement", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "sensor_device_id_device_id_fk": { + "name": "sensor_device_id_device_id_fk", + "tableFrom": "sensor", + "tableTo": "device", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "unconfirmed_email": { + "name": "unconfirmed_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'en_US'" + }, + "email_is_confirmed": { + "name": "email_is_confirmed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "email_confirmation_token": { + "name": "email_confirmation_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "user_unconfirmed_email_unique": { + "name": "user_unconfirmed_email_unique", + "nullsNotDistinct": false, + "columns": [ + "unconfirmed_email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.location": { + "name": "location", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "location": { + "name": "location", + "type": "geometry(point)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "location_index": { + "name": "location_index", + "columns": [ + { + "expression": "location", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gist", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "location_location_unique": { + "name": "location_location_unique", + "nullsNotDistinct": false, + "columns": [ + "location" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.log_entry": { + "name": "log_entry", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.refresh_token": { + "name": "refresh_token", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "refresh_token_user_id_user_id_fk": { + "name": "refresh_token_user_id_user_id_fk", + "tableFrom": "refresh_token", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.token_revocation": { + "name": "token_revocation", + "schema": "", + "columns": { + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.claim": { + "name": "claim", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "box_id": { + "name": "box_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "claim_expires_at_idx": { + "name": "claim_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "claim_box_id_device_id_fk": { + "name": "claim_box_id_device_id_fk", + "tableFrom": "claim", + "tableTo": "device", + "columnsFrom": [ + "box_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_box_id": { + "name": "unique_box_id", + "nullsNotDistinct": false, + "columns": [ + "box_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration": { + "name": "integration", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_url": { + "name": "service_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_key": { + "name": "service_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "integration_slug_unique": { + "name": "integration_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.exposure": { + "name": "exposure", + "schema": "public", + "values": [ + "indoor", + "outdoor", + "mobile", + "unknown" + ] + }, + "public.model": { + "name": "model", + "schema": "public", + "values": [ + "homeV2Lora", + "homeV2Ethernet", + "homeV2Wifi", + "homeEthernet", + "homeWifi", + "homeEthernetFeinstaub", + "homeWifiFeinstaub", + "luftdaten_sds011", + "luftdaten_sds011_dht11", + "luftdaten_sds011_dht22", + "luftdaten_sds011_bmp180", + "luftdaten_sds011_bme280", + "hackair_home_v2", + "senseBox:Edu", + "luftdaten.info", + "custom" + ] + }, + "public.status": { + "name": "status", + "schema": "public", + "values": [ + "active", + "inactive", + "old" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": { + "public.measurement_10min": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_10min", + "schema": "public", + "isExisting": true, + "materialized": true + }, + "public.measurement_1day": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_1day", + "schema": "public", + "isExisting": true, + "materialized": true + }, + "public.measurement_1hour": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_1hour", + "schema": "public", + "isExisting": true, + "materialized": true + }, + "public.measurement_1month": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_1month", + "schema": "public", + "isExisting": true, + "materialized": true + }, + "public.measurement_1year": { + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "name": "measurement_1year", + "schema": "public", + "isExisting": true, + "materialized": true + } + }, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file From ef5d3bd86015cde31fc1159936ad2e090d84ac35 Mon Sep 17 00:00:00 2001 From: jona159 Date: Tue, 10 Mar 2026 14:15:40 +0100 Subject: [PATCH 2/9] feat: add cron job to archive devices --- drizzle/0035_handy_rawhide_kid.sql | 23 + drizzle/meta/0035_snapshot.json | 1336 ++++++++++++++++++++++++++++ drizzle/meta/_journal.json | 14 + 3 files changed, 1373 insertions(+) create mode 100644 drizzle/0035_handy_rawhide_kid.sql create mode 100644 drizzle/meta/0035_snapshot.json diff --git a/drizzle/0035_handy_rawhide_kid.sql b/drizzle/0035_handy_rawhide_kid.sql new file mode 100644 index 00000000..b377b19b --- /dev/null +++ b/drizzle/0035_handy_rawhide_kid.sql @@ -0,0 +1,23 @@ +CREATE OR REPLACE PROCEDURE archive_inactive_devices() +LANGUAGE SQL +AS $$ + UPDATE device d + SET + archived_at = now(), + updated_at = now() + WHERE d.archived_at IS NULL + AND d.created_at < now() - interval '12 months' + AND NOT EXISTS ( + SELECT 1 + FROM sensor s + JOIN measurement m ON m.sensor_id = s.id + WHERE s.device_id = d.id + AND m.time >= now() - interval '12 months' + ); +$$; + +SELECT cron.schedule( + 'device-archive-inactive', + '0 2 * * *', + 'CALL archive_inactive_devices()' +); \ No newline at end of file diff --git a/drizzle/meta/0035_snapshot.json b/drizzle/meta/0035_snapshot.json new file mode 100644 index 00000000..52b7c1fc --- /dev/null +++ b/drizzle/meta/0035_snapshot.json @@ -0,0 +1,1336 @@ +{ + "id": "3a3aef01-9339-43fd-8bd4-330319cac9c9", + "prevId": "70db0852-369c-47bf-9b12-f9b6f8295fbf", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.device": { + "name": "device", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": false, + "default": "ARRAY[]::text[]" + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "use_auth": { + "name": "use_auth", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "apiKey": { + "name": "apiKey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "exposure": { + "name": "exposure", + "type": "exposure", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'inactive'" + }, + "model": { + "name": "model", + "type": "model", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'custom'" + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "latitude": { + "name": "latitude", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "longitude": { + "name": "longitude", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sensor_wiki_model": { + "name": "sensor_wiki_model", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_to_location": { + "name": "device_to_location", + "schema": "", + "columns": { + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "location_id": { + "name": "location_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "time": { + "name": "time", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "device_to_location_device_id_device_id_fk": { + "name": "device_to_location_device_id_device_id_fk", + "tableFrom": "device_to_location", + "columnsFrom": [ + "device_id" + ], + "tableTo": "device", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "cascade" + }, + "device_to_location_location_id_location_id_fk": { + "name": "device_to_location_location_id_location_id_fk", + "tableFrom": "device_to_location", + "columnsFrom": [ + "location_id" + ], + "tableTo": "location", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": { + "device_to_location_device_id_location_id_time_pk": { + "name": "device_to_location_device_id_location_id_time_pk", + "columns": [ + "device_id", + "location_id", + "time" + ] + } + }, + "uniqueConstraints": { + "device_to_location_device_id_location_id_time_unique": { + "name": "device_to_location_device_id_location_id_time_unique", + "columns": [ + "device_id", + "location_id", + "time" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.measurement": { + "name": "measurement", + "schema": "", + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "value": { + "name": "value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "location_id": { + "name": "location_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "measurement_location_id_location_id_fk": { + "name": "measurement_location_id_location_id_fk", + "tableFrom": "measurement", + "columnsFrom": [ + "location_id" + ], + "tableTo": "location", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "measurement_sensor_id_time_unique": { + "name": "measurement_sensor_id_time_unique", + "columns": [ + "sensor_id", + "time" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.password": { + "name": "password", + "schema": "", + "columns": { + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "password_user_id_user_id_fk": { + "name": "password_user_id_user_id_fk", + "tableFrom": "password", + "columnsFrom": [ + "user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.password_reset_request": { + "name": "password_reset_request", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "password_reset_request_user_id_user_id_fk": { + "name": "password_reset_request_user_id_user_id_fk", + "tableFrom": "password_reset_request", + "columnsFrom": [ + "user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "password_reset_request_user_id_unique": { + "name": "password_reset_request_user_id_unique", + "columns": [ + "user_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile": { + "name": "profile", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "profile_user_id_user_id_fk": { + "name": "profile_user_id_user_id_fk", + "tableFrom": "profile", + "columnsFrom": [ + "user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "profile_username_unique": { + "name": "profile_username_unique", + "columns": [ + "username" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile_image": { + "name": "profile_image", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "alt_text": { + "name": "alt_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "blob": { + "name": "blob", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "profile_id": { + "name": "profile_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "profile_image_profile_id_profile_id_fk": { + "name": "profile_image_profile_id_profile_id_fk", + "tableFrom": "profile_image", + "columnsFrom": [ + "profile_id" + ], + "tableTo": "profile", + "columnsTo": [ + "id" + ], + "onUpdate": "cascade", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sensor": { + "name": "sensor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sensor_type": { + "name": "sensor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'inactive'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sensor_wiki_type": { + "name": "sensor_wiki_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sensor_wiki_phenomenon": { + "name": "sensor_wiki_phenomenon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sensor_wiki_unit": { + "name": "sensor_wiki_unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lastMeasurement": { + "name": "lastMeasurement", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "sensor_device_id_device_id_fk": { + "name": "sensor_device_id_device_id_fk", + "tableFrom": "sensor", + "columnsFrom": [ + "device_id" + ], + "tableTo": "device", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "unconfirmed_email": { + "name": "unconfirmed_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'en_US'" + }, + "email_is_confirmed": { + "name": "email_is_confirmed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "email_confirmation_token": { + "name": "email_confirmation_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "nullsNotDistinct": false + }, + "user_unconfirmed_email_unique": { + "name": "user_unconfirmed_email_unique", + "columns": [ + "unconfirmed_email" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.location": { + "name": "location", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "location": { + "name": "location", + "type": "geometry(point)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "location_index": { + "name": "location_index", + "columns": [ + { + "expression": "location", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "gist", + "concurrently": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "location_location_unique": { + "name": "location_location_unique", + "columns": [ + "location" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.log_entry": { + "name": "log_entry", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.refresh_token": { + "name": "refresh_token", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "refresh_token_user_id_user_id_fk": { + "name": "refresh_token_user_id_user_id_fk", + "tableFrom": "refresh_token", + "columnsFrom": [ + "user_id" + ], + "tableTo": "user", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.token_revocation": { + "name": "token_revocation", + "schema": "", + "columns": { + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.claim": { + "name": "claim", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "box_id": { + "name": "box_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "claim_expires_at_idx": { + "name": "claim_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "with": {}, + "method": "btree", + "concurrently": false + } + }, + "foreignKeys": { + "claim_box_id_device_id_fk": { + "name": "claim_box_id_device_id_fk", + "tableFrom": "claim", + "columnsFrom": [ + "box_id" + ], + "tableTo": "device", + "columnsTo": [ + "id" + ], + "onUpdate": "no action", + "onDelete": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_box_id": { + "name": "unique_box_id", + "columns": [ + "box_id" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration": { + "name": "integration", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_url": { + "name": "service_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_key": { + "name": "service_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "integration_slug_unique": { + "name": "integration_slug_unique", + "columns": [ + "slug" + ], + "nullsNotDistinct": false + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.exposure": { + "name": "exposure", + "schema": "public", + "values": [ + "indoor", + "outdoor", + "mobile", + "unknown" + ] + }, + "public.model": { + "name": "model", + "schema": "public", + "values": [ + "homeV2Lora", + "homeV2Ethernet", + "homeV2Wifi", + "homeEthernet", + "homeWifi", + "homeEthernetFeinstaub", + "homeWifiFeinstaub", + "luftdaten_sds011", + "luftdaten_sds011_dht11", + "luftdaten_sds011_dht22", + "luftdaten_sds011_bmp180", + "luftdaten_sds011_bme280", + "hackair_home_v2", + "senseBox:Edu", + "luftdaten.info", + "custom" + ] + }, + "public.status": { + "name": "status", + "schema": "public", + "values": [ + "active", + "inactive", + "old" + ] + } + }, + "schemas": {}, + "views": { + "public.measurement_10min": { + "name": "measurement_10min", + "schema": "public", + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "materialized": true, + "isExisting": true + }, + "public.measurement_1day": { + "name": "measurement_1day", + "schema": "public", + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "materialized": true, + "isExisting": true + }, + "public.measurement_1hour": { + "name": "measurement_1hour", + "schema": "public", + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "materialized": true, + "isExisting": true + }, + "public.measurement_1month": { + "name": "measurement_1month", + "schema": "public", + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "materialized": true, + "isExisting": true + }, + "public.measurement_1year": { + "name": "measurement_1year", + "schema": "public", + "columns": { + "sensor_id": { + "name": "sensor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time": { + "name": "time", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "avg_value": { + "name": "avg_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "total_values": { + "name": "total_values", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "min_value": { + "name": "min_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "max_value": { + "name": "max_value", + "type": "double precision", + "primaryKey": false, + "notNull": false + } + }, + "materialized": true, + "isExisting": true + } + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 07a2283d..6053429e 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -239,6 +239,20 @@ "when": 1772009671134, "tag": "0033_caag-retention-policies", "breakpoints": true + }, + { + "idx": 34, + "version": "7", + "when": 1773145800241, + "tag": "0034_mute_ultragirl", + "breakpoints": true + }, + { + "idx": 35, + "version": "7", + "when": 1773145838146, + "tag": "0035_handy_rawhide_kid", + "breakpoints": true } ] } \ No newline at end of file From c649633c4cc9216ce9b10103fd10e95327bfd60d Mon Sep 17 00:00:00 2001 From: jona159 Date: Tue, 10 Mar 2026 16:08:36 +0100 Subject: [PATCH 3/9] feat: exclude archived devices by default --- app/models/device.server.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/models/device.server.ts b/app/models/device.server.ts index 42039ed7..23aa8b07 100644 --- a/app/models/device.server.ts +++ b/app/models/device.server.ts @@ -7,6 +7,7 @@ import { arrayContains, and, between, + isNull, type ExtractTablesWithRelations, } from 'drizzle-orm' import { type PgTransaction } from 'drizzle-orm/pg-core' @@ -433,6 +434,7 @@ export async function getDevices( export async function getDevices(format: DevicesFormat = 'json') { const devices = await drizzleClient.query.device.findMany({ + where: (device, { isNull }) => isNull(device.archivedAt), columns: { id: true, name: true, @@ -476,6 +478,8 @@ export async function getDevicesWithSensors() { }) .from(device) .leftJoin(sensor, eq(sensor.deviceId, device.id)) + .where(isNull(device.archivedAt)) + const geojson: GeoJSON.FeatureCollection = { type: 'FeatureCollection', features: [], From 8a62d563570114891ec58ccf172ab475e2936d78 Mon Sep 17 00:00:00 2001 From: jona159 Date: Tue, 10 Mar 2026 16:10:58 +0100 Subject: [PATCH 4/9] feat: get archived devices method --- app/models/device.server.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/app/models/device.server.ts b/app/models/device.server.ts index 23aa8b07..2ff614de 100644 --- a/app/models/device.server.ts +++ b/app/models/device.server.ts @@ -9,6 +9,7 @@ import { between, isNull, type ExtractTablesWithRelations, + isNotNull, } from 'drizzle-orm' import { type PgTransaction } from 'drizzle-orm/pg-core' import { type PostgresJsQueryResultHKT } from 'drizzle-orm/postgres-js' @@ -434,7 +435,7 @@ export async function getDevices( export async function getDevices(format: DevicesFormat = 'json') { const devices = await drizzleClient.query.device.findMany({ - where: (device, { isNull }) => isNull(device.archivedAt), + where: (device) => isNull(device.archivedAt), columns: { id: true, name: true, @@ -465,6 +466,24 @@ export async function getDevices(format: DevicesFormat = 'json') { return devices } +export async function getArchivedDevices(){ + const devices = await drizzleClient.query.device.findMany({ + where: (device) => isNotNull(device.archivedAt), + columns: { + id: true, + name: true, + latitude: true, + longitude: true, + exposure: true, + status: true, + createdAt: true, + tags: true, + archivedAt: true + }, + }) + return devices +} + export async function getDevicesWithSensors() { const rows = await drizzleClient .select({ From 888f7a6cc61f0ed11c2e3b3caa4164ad50815078 Mon Sep 17 00:00:00 2001 From: jona159 Date: Tue, 10 Mar 2026 16:55:06 +0100 Subject: [PATCH 5/9] feat: adjust datatable for archived devices --- app/components/landing/header/header.tsx | 2 +- app/components/mydevices/dt/columns.tsx | 124 +++++++++++++-------- app/components/mydevices/dt/data-table.tsx | 31 +++--- app/routes/profile.$username.tsx | 5 + 4 files changed, 102 insertions(+), 60 deletions(-) diff --git a/app/components/landing/header/header.tsx b/app/components/landing/header/header.tsx index 79efc9cd..dd97b47f 100644 --- a/app/components/landing/header/header.tsx +++ b/app/components/landing/header/header.tsx @@ -1,8 +1,8 @@ import { useState } from 'react' +import { useTranslation } from 'react-i18next' import { Link } from 'react-router' // import { ModeToggle } from "../../mode-toggle"; import LanguageSelector from './language-selector' -import { useTranslation } from 'react-i18next' const links = [ { diff --git a/app/components/mydevices/dt/columns.tsx b/app/components/mydevices/dt/columns.tsx index c6442fd6..85d06e12 100644 --- a/app/components/mydevices/dt/columns.tsx +++ b/app/components/mydevices/dt/columns.tsx @@ -19,6 +19,7 @@ export type SenseBox = { name: string exposure: Device['exposure'] createdAt: Date + archivedAt: Date | null // model: string; } @@ -31,20 +32,37 @@ export function getColumns( const { t } = useTranslation const isOwner = opts?.isOwner ?? false return [ - { + { accessorKey: 'name', - header: ({ column }) => { - return ( - - ) - }, + header: ({ column }) => { + return ( + + ) + }, + cell: ({ row }) => { + const senseBox = row.original + const isArchived = !!senseBox.archivedAt + + return ( +
+ + {senseBox.name} + + {isArchived ? ( + + {t('archived')} + + ) : null} +
+ ) + }, }, { accessorKey: 'createdAt', @@ -124,7 +142,8 @@ export function getColumns(
{t('actions')}
), cell: ({ row }) => { - const senseBox = row.original + const device = row.original + const isArchived = !!device.archivedAt return ( @@ -134,49 +153,66 @@ export function getColumns( + - Actions + {t('actions')} + - {t('overview')} + {t('overview')} - - {t('show_on_map')} + + + {t('show_on_map')} + {isOwner ? ( <> - - {t('edit')} - - - - {t('data_upload')} - - - - - {t('support')} - - - navigator.clipboard.writeText(senseBox?.id)} - className="cursor-pointer" - > - {t('copy_id')} - - - ): null } + + {isArchived ? ( + {t('edit')} + ) : ( + + {t('edit')} + + )} + + + + {isArchived ? ( + {t('data_upload')} + ) : ( + + {t('data_upload')} + + )} + + + + + {t('support')} + + + + navigator.clipboard.writeText(device.id)} + className="cursor-pointer" + > + {t('copy_id')} + + + ) : null} ) }, - }, + } ] } diff --git a/app/components/mydevices/dt/data-table.tsx b/app/components/mydevices/dt/data-table.tsx index 51711a66..189f4101 100644 --- a/app/components/mydevices/dt/data-table.tsx +++ b/app/components/mydevices/dt/data-table.tsx @@ -40,11 +40,13 @@ import { interface DataTableProps { columns: ColumnDef[] data: TData[] + getRowClassName?: (row: TData) => string } export function DataTable({ columns, data, + getRowClassName, }: DataTableProps) { const [sorting, setSorting] = React.useState([ { id: 'createdAt', desc: true }, @@ -72,8 +74,8 @@ export function DataTable({ }, }, }) - const tableColsWidth = [30, 30, 30, 40] + const tableColsWidth = [30, 30, 30, 40] const { t } = useTranslation('data-table') return ( @@ -94,27 +96,27 @@ export function DataTable({ {table.getHeaderGroups().map((headerGroup) => ( - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ) - })} + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ))} ))} + {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell, index) => ( ({ onValueChange={(value) => { table.setPageSize(Number(value)) }} - // disabled={isPending} > @@ -218,4 +219,4 @@ export function DataTable({ ) -} +} \ No newline at end of file diff --git a/app/routes/profile.$username.tsx b/app/routes/profile.$username.tsx index 38d41758..6de31374 100644 --- a/app/routes/profile.$username.tsx +++ b/app/routes/profile.$username.tsx @@ -210,6 +210,11 @@ export default function () { + device.archivedAt + ? 'opacity-60 bg-slate-100 dark:bg-slate-900/40' + : '' + } /> )} From 9fd4552c8af9d392162b72b5d2ad8ba01107fc1c Mon Sep 17 00:00:00 2001 From: jona159 Date: Wed, 11 Mar 2026 08:05:52 +0100 Subject: [PATCH 6/9] feat: archive device email template --- emails/archived-device.tsx | 346 +++++++++++++++++++++++++++++++++++++ 1 file changed, 346 insertions(+) create mode 100644 emails/archived-device.tsx diff --git a/emails/archived-device.tsx b/emails/archived-device.tsx new file mode 100644 index 00000000..1f412e1a --- /dev/null +++ b/emails/archived-device.tsx @@ -0,0 +1,346 @@ +import { createIntl } from '@formatjs/intl' +import { + Html, + Head, + Heading, + Preview, + Body, + Container, + Text, + Link, + Hr, + Section, +} from '@react-email/components' + +export interface DeviceArchivedEmailProps { + user: { + name: string + } + device: { + id: string + name: string + lastActivity?: string // ISO date string + } + language: 'de' | 'en' + content: typeof messages +} + +export const messages = { + en: { + preview: 'Your device has been archived due to inactivity', + heading: 'Device Archived', + subheading: 'Due to inactivity', + greeting: 'Hello', + mainText: + 'your device "{ deviceName }" has been archived after 12 months of inactivity. Archived devices are no longer publicly visible on openSenseMap, but all your data is safely preserved.', + whatThisMeans: 'What does this mean?', + point1: 'Your device is no longer publicly visible on the map.', + point2: 'All historical sensor data remains stored and is not deleted.', + point3: 'You can reactivate your device at any time from your dashboard.', + deviceId: 'Device ID', + lastSeen: 'Last activity', + reactivateText: 'Want to continue contributing? Reactivate your device:', + reactivateCta: 'Reactivate Device', + support: + 'Questions? Reach us on GitHub or via email: ', + closing: 'Thank you for your past contributions to openSenseMap.', + teamSignoff: 'The openSenseMap Team', + }, + de: { + preview: 'Dein Gerät wurde wegen Inaktivität archiviert', + heading: 'Gerät archiviert', + subheading: 'Aufgrund von Inaktivität', + greeting: 'Hallo', + mainText: + 'dein Gerät "{ deviceName }" wurde nach 12 Monaten Inaktivität archiviert. Archivierte Geräte sind auf der openSenseMap nicht mehr öffentlich sichtbar, aber alle deine Daten sind sicher gespeichert.', + whatThisMeans: 'Was bedeutet das?', + point1: 'Dein Gerät ist auf der Karte nicht mehr öffentlich sichtbar.', + point2: 'Alle historischen Sensordaten bleiben gespeichert und werden nicht gelöscht.', + point3: 'Du kannst dein Gerät jederzeit über dein Dashboard reaktivieren.', + deviceId: 'Geräte-ID', + lastSeen: 'Letzte Aktivität', + reactivateText: 'Möchtest du weiter mitmachen? Reaktiviere dein Gerät:', + reactivateCta: 'Gerät reaktivieren', + support: + 'Fragen? Kontaktiere uns über GitHub oder schreib eine Mail an: ', + closing: 'Danke für deine bisherigen Beiträge zur openSenseMap.', + teamSignoff: 'Dein openSenseMap Team', + }, +} + +const baseUrl = process.env.OSEM_URL + ? `https://${process.env.OSEM_URL}` + : 'https://opensensemap.org' + +const styles = { + body: { + backgroundColor: '#f5f4f0', + fontFamily: + "'Georgia', 'Times New Roman', Times, serif", + margin: '0', + padding: '0', + }, + outerContainer: { + maxWidth: '560px', + margin: '0 auto', + padding: '40px 16px', + }, + card: { + backgroundColor: '#ffffff', + borderRadius: '2px', + overflow: 'hidden' as const, + boxShadow: '0 1px 4px rgba(0,0,0,0.08)', + }, + statusBanner: { + backgroundColor: '#c97d2e', + padding: '6px 28px', + display: 'block' as const, + }, + statusBannerText: { + color: '#fff8ee', + fontSize: '11px', + fontFamily: "'Courier New', Courier, monospace", + letterSpacing: '2px', + textTransform: 'uppercase' as const, + margin: '0', + }, + headerSection: { + padding: '32px 28px 24px', + borderBottom: '1px solid #ede9e0', + }, + heading: { + color: '#1a1a18', + fontSize: '28px', + fontWeight: 'bold', + margin: '0 0 4px 0', + padding: '0', + lineHeight: '1.2', + letterSpacing: '-0.5px', + }, + subheading: { + color: '#8a7f6e', + fontSize: '13px', + fontFamily: "'Courier New', Courier, monospace", + letterSpacing: '1.5px', + textTransform: 'uppercase' as const, + margin: '0', + }, + bodySection: { + padding: '24px 28px', + }, + text: { + color: '#3d3930', + fontSize: '15px', + lineHeight: '1.7', + margin: '0 0 16px 0', + }, + infoBox: { + backgroundColor: '#faf8f3', + border: '1px solid #e8e2d4', + borderLeft: '3px solid #c97d2e', + borderRadius: '2px', + padding: '16px 18px', + margin: '20px 0', + }, + infoBoxHeading: { + color: '#1a1a18', + fontSize: '12px', + fontFamily: "'Courier New', Courier, monospace", + letterSpacing: '1.5px', + textTransform: 'uppercase' as const, + margin: '0 0 12px 0', + fontWeight: 'bold', + }, + bulletPoint: { + color: '#3d3930', + fontSize: '14px', + lineHeight: '1.6', + margin: '0 0 6px 0', + paddingLeft: '4px', + }, + metaBox: { + backgroundColor: '#f5f4f0', + borderRadius: '2px', + padding: '14px 18px', + margin: '20px 0', + }, + metaLabel: { + color: '#8a7f6e', + fontSize: '10px', + fontFamily: "'Courier New', Courier, monospace", + letterSpacing: '1.5px', + textTransform: 'uppercase' as const, + margin: '0 0 2px 0', + }, + metaValue: { + color: '#1a1a18', + fontSize: '13px', + fontFamily: "'Courier New', Courier, monospace", + margin: '0 0 12px 0', + fontWeight: 'bold', + }, + ctaSection: { + padding: '0 28px 24px', + textAlign: 'center' as const, + }, + ctaText: { + color: '#8a7f6e', + fontSize: '13px', + margin: '0 0 14px 0', + }, + ctaButton: { + display: 'inline-block' as const, + backgroundColor: '#c97d2e', + color: '#ffffff', + fontSize: '13px', + fontFamily: "'Courier New', Courier, monospace", + letterSpacing: '1.5px', + textTransform: 'uppercase' as const, + textDecoration: 'none', + padding: '12px 28px', + borderRadius: '2px', + }, + hr: { + borderColor: '#ede9e0', + margin: '0', + }, + footer: { + padding: '20px 28px', + }, + footerText: { + color: '#8a7f6e', + fontSize: '13px', + lineHeight: '1.6', + margin: '0 0 8px 0', + }, + footerLink: { + color: '#c97d2e', + textDecoration: 'underline', + }, + teamSignoff: { + color: '#3d3930', + fontSize: '13px', + fontFamily: "'Courier New', Courier, monospace", + margin: '16px 0 0 0', + }, +} + +export const DeviceArchivedEmail = ({ + user = { name: 'Erika Mustermann' }, + device = { id: '12345678', name: 'My Weather Station', lastActivity: '2024-02-15T08:00:00Z' }, + language = 'en', + content = messages, +}: DeviceArchivedEmailProps) => { + const i = createIntl({ + locale: language, + messages: content[language], + }) + + const formattedDate = device.lastActivity + ? new Date(device.lastActivity).toLocaleDateString(language === 'de' ? 'de-DE' : 'en-GB', { + year: 'numeric', + month: 'long', + day: 'numeric', + }) + : '—' + + return ( + + + {i.formatMessage({ id: 'preview' })} + + +
+ {/* Status banner */} +
+ + openSenseMap · {i.formatMessage({ id: 'subheading' })} + +
+ + {/* Header */} +
+ + {i.formatMessage({ id: 'heading' })} + +
+ + {/* Body */} +
+ + {i.formatMessage({ id: 'greeting' })} {user.name}, + + + {i.formatMessage({ id: 'mainText' }, { deviceName: device.name })} + + + {/* What this means */} +
+ + {i.formatMessage({ id: 'whatThisMeans' })} + + + → {i.formatMessage({ id: 'point1' })} + + + → {i.formatMessage({ id: 'point2' })} + + + → {i.formatMessage({ id: 'point3' })} + +
+ + {/* Device meta */} +
+ {i.formatMessage({ id: 'deviceId' })} + {device.id} + + {i.formatMessage({ id: 'lastSeen' })} + + + {formattedDate} + +
+
+ + {/* CTA */} +
+ + {i.formatMessage({ id: 'reactivateText' })} + + + {i.formatMessage({ id: 'reactivateCta' })} + +
+ +
+ + {/* Footer */} +
+ + {i.formatMessage({ id: 'support' })} + + info@opensenselab.org + + + + {i.formatMessage({ id: 'closing' })} + + + — {i.formatMessage({ id: 'teamSignoff' })} + +
+
+
+ + + ) +} + +export default DeviceArchivedEmail \ No newline at end of file From 9a054b356041fcd3bafa6589f829c3326ffc60b6 Mon Sep 17 00:00:00 2001 From: jona159 Date: Fri, 13 Mar 2026 13:36:18 +0100 Subject: [PATCH 7/9] fix: rm email template --- emails/archived-device.tsx | 346 ------------------------------------- 1 file changed, 346 deletions(-) delete mode 100644 emails/archived-device.tsx diff --git a/emails/archived-device.tsx b/emails/archived-device.tsx deleted file mode 100644 index 1f412e1a..00000000 --- a/emails/archived-device.tsx +++ /dev/null @@ -1,346 +0,0 @@ -import { createIntl } from '@formatjs/intl' -import { - Html, - Head, - Heading, - Preview, - Body, - Container, - Text, - Link, - Hr, - Section, -} from '@react-email/components' - -export interface DeviceArchivedEmailProps { - user: { - name: string - } - device: { - id: string - name: string - lastActivity?: string // ISO date string - } - language: 'de' | 'en' - content: typeof messages -} - -export const messages = { - en: { - preview: 'Your device has been archived due to inactivity', - heading: 'Device Archived', - subheading: 'Due to inactivity', - greeting: 'Hello', - mainText: - 'your device "{ deviceName }" has been archived after 12 months of inactivity. Archived devices are no longer publicly visible on openSenseMap, but all your data is safely preserved.', - whatThisMeans: 'What does this mean?', - point1: 'Your device is no longer publicly visible on the map.', - point2: 'All historical sensor data remains stored and is not deleted.', - point3: 'You can reactivate your device at any time from your dashboard.', - deviceId: 'Device ID', - lastSeen: 'Last activity', - reactivateText: 'Want to continue contributing? Reactivate your device:', - reactivateCta: 'Reactivate Device', - support: - 'Questions? Reach us on GitHub or via email: ', - closing: 'Thank you for your past contributions to openSenseMap.', - teamSignoff: 'The openSenseMap Team', - }, - de: { - preview: 'Dein Gerät wurde wegen Inaktivität archiviert', - heading: 'Gerät archiviert', - subheading: 'Aufgrund von Inaktivität', - greeting: 'Hallo', - mainText: - 'dein Gerät "{ deviceName }" wurde nach 12 Monaten Inaktivität archiviert. Archivierte Geräte sind auf der openSenseMap nicht mehr öffentlich sichtbar, aber alle deine Daten sind sicher gespeichert.', - whatThisMeans: 'Was bedeutet das?', - point1: 'Dein Gerät ist auf der Karte nicht mehr öffentlich sichtbar.', - point2: 'Alle historischen Sensordaten bleiben gespeichert und werden nicht gelöscht.', - point3: 'Du kannst dein Gerät jederzeit über dein Dashboard reaktivieren.', - deviceId: 'Geräte-ID', - lastSeen: 'Letzte Aktivität', - reactivateText: 'Möchtest du weiter mitmachen? Reaktiviere dein Gerät:', - reactivateCta: 'Gerät reaktivieren', - support: - 'Fragen? Kontaktiere uns über GitHub oder schreib eine Mail an: ', - closing: 'Danke für deine bisherigen Beiträge zur openSenseMap.', - teamSignoff: 'Dein openSenseMap Team', - }, -} - -const baseUrl = process.env.OSEM_URL - ? `https://${process.env.OSEM_URL}` - : 'https://opensensemap.org' - -const styles = { - body: { - backgroundColor: '#f5f4f0', - fontFamily: - "'Georgia', 'Times New Roman', Times, serif", - margin: '0', - padding: '0', - }, - outerContainer: { - maxWidth: '560px', - margin: '0 auto', - padding: '40px 16px', - }, - card: { - backgroundColor: '#ffffff', - borderRadius: '2px', - overflow: 'hidden' as const, - boxShadow: '0 1px 4px rgba(0,0,0,0.08)', - }, - statusBanner: { - backgroundColor: '#c97d2e', - padding: '6px 28px', - display: 'block' as const, - }, - statusBannerText: { - color: '#fff8ee', - fontSize: '11px', - fontFamily: "'Courier New', Courier, monospace", - letterSpacing: '2px', - textTransform: 'uppercase' as const, - margin: '0', - }, - headerSection: { - padding: '32px 28px 24px', - borderBottom: '1px solid #ede9e0', - }, - heading: { - color: '#1a1a18', - fontSize: '28px', - fontWeight: 'bold', - margin: '0 0 4px 0', - padding: '0', - lineHeight: '1.2', - letterSpacing: '-0.5px', - }, - subheading: { - color: '#8a7f6e', - fontSize: '13px', - fontFamily: "'Courier New', Courier, monospace", - letterSpacing: '1.5px', - textTransform: 'uppercase' as const, - margin: '0', - }, - bodySection: { - padding: '24px 28px', - }, - text: { - color: '#3d3930', - fontSize: '15px', - lineHeight: '1.7', - margin: '0 0 16px 0', - }, - infoBox: { - backgroundColor: '#faf8f3', - border: '1px solid #e8e2d4', - borderLeft: '3px solid #c97d2e', - borderRadius: '2px', - padding: '16px 18px', - margin: '20px 0', - }, - infoBoxHeading: { - color: '#1a1a18', - fontSize: '12px', - fontFamily: "'Courier New', Courier, monospace", - letterSpacing: '1.5px', - textTransform: 'uppercase' as const, - margin: '0 0 12px 0', - fontWeight: 'bold', - }, - bulletPoint: { - color: '#3d3930', - fontSize: '14px', - lineHeight: '1.6', - margin: '0 0 6px 0', - paddingLeft: '4px', - }, - metaBox: { - backgroundColor: '#f5f4f0', - borderRadius: '2px', - padding: '14px 18px', - margin: '20px 0', - }, - metaLabel: { - color: '#8a7f6e', - fontSize: '10px', - fontFamily: "'Courier New', Courier, monospace", - letterSpacing: '1.5px', - textTransform: 'uppercase' as const, - margin: '0 0 2px 0', - }, - metaValue: { - color: '#1a1a18', - fontSize: '13px', - fontFamily: "'Courier New', Courier, monospace", - margin: '0 0 12px 0', - fontWeight: 'bold', - }, - ctaSection: { - padding: '0 28px 24px', - textAlign: 'center' as const, - }, - ctaText: { - color: '#8a7f6e', - fontSize: '13px', - margin: '0 0 14px 0', - }, - ctaButton: { - display: 'inline-block' as const, - backgroundColor: '#c97d2e', - color: '#ffffff', - fontSize: '13px', - fontFamily: "'Courier New', Courier, monospace", - letterSpacing: '1.5px', - textTransform: 'uppercase' as const, - textDecoration: 'none', - padding: '12px 28px', - borderRadius: '2px', - }, - hr: { - borderColor: '#ede9e0', - margin: '0', - }, - footer: { - padding: '20px 28px', - }, - footerText: { - color: '#8a7f6e', - fontSize: '13px', - lineHeight: '1.6', - margin: '0 0 8px 0', - }, - footerLink: { - color: '#c97d2e', - textDecoration: 'underline', - }, - teamSignoff: { - color: '#3d3930', - fontSize: '13px', - fontFamily: "'Courier New', Courier, monospace", - margin: '16px 0 0 0', - }, -} - -export const DeviceArchivedEmail = ({ - user = { name: 'Erika Mustermann' }, - device = { id: '12345678', name: 'My Weather Station', lastActivity: '2024-02-15T08:00:00Z' }, - language = 'en', - content = messages, -}: DeviceArchivedEmailProps) => { - const i = createIntl({ - locale: language, - messages: content[language], - }) - - const formattedDate = device.lastActivity - ? new Date(device.lastActivity).toLocaleDateString(language === 'de' ? 'de-DE' : 'en-GB', { - year: 'numeric', - month: 'long', - day: 'numeric', - }) - : '—' - - return ( - - - {i.formatMessage({ id: 'preview' })} - - -
- {/* Status banner */} -
- - openSenseMap · {i.formatMessage({ id: 'subheading' })} - -
- - {/* Header */} -
- - {i.formatMessage({ id: 'heading' })} - -
- - {/* Body */} -
- - {i.formatMessage({ id: 'greeting' })} {user.name}, - - - {i.formatMessage({ id: 'mainText' }, { deviceName: device.name })} - - - {/* What this means */} -
- - {i.formatMessage({ id: 'whatThisMeans' })} - - - → {i.formatMessage({ id: 'point1' })} - - - → {i.formatMessage({ id: 'point2' })} - - - → {i.formatMessage({ id: 'point3' })} - -
- - {/* Device meta */} -
- {i.formatMessage({ id: 'deviceId' })} - {device.id} - - {i.formatMessage({ id: 'lastSeen' })} - - - {formattedDate} - -
-
- - {/* CTA */} -
- - {i.formatMessage({ id: 'reactivateText' })} - - - {i.formatMessage({ id: 'reactivateCta' })} - -
- -
- - {/* Footer */} -
- - {i.formatMessage({ id: 'support' })} - - info@opensenselab.org - - - - {i.formatMessage({ id: 'closing' })} - - - — {i.formatMessage({ id: 'teamSignoff' })} - -
-
-
- - - ) -} - -export default DeviceArchivedEmail \ No newline at end of file From c8d9de5c90a37987fcbe84d87b19107dd54d6c96 Mon Sep 17 00:00:00 2001 From: jona159 Date: Fri, 13 Mar 2026 13:37:48 +0100 Subject: [PATCH 8/9] fix: naming --- app/components/mydevices/dt/columns.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/app/components/mydevices/dt/columns.tsx b/app/components/mydevices/dt/columns.tsx index 85d06e12..19a9b35b 100644 --- a/app/components/mydevices/dt/columns.tsx +++ b/app/components/mydevices/dt/columns.tsx @@ -47,13 +47,13 @@ export function getColumns( ) }, cell: ({ row }) => { - const senseBox = row.original - const isArchived = !!senseBox.archivedAt + const device = row.original + const isArchived = !!device.archivedAt return (
- {senseBox.name} + {device.name} {isArchived ? ( @@ -120,16 +120,15 @@ export function getColumns(
{t('sensebox_id')}
), cell: ({ row }) => { - const senseBox = row.original + const device = row.original return ( - //
- {senseBox?.id} + {device?.id} navigator.clipboard.writeText(senseBox?.id)} + onClick={() => navigator.clipboard.writeText(device?.id)} className="ml-[6px] mr-1 inline-block h-4 w-4 cursor-pointer align-text-bottom text-[#818a91] dark:text-white" />
From aa04780cae333c7faa0f634a0b3ad934b3ea9e82 Mon Sep 17 00:00:00 2001 From: jona159 Date: Fri, 13 Mar 2026 13:44:30 +0100 Subject: [PATCH 9/9] fix: include archivedAt in base device columns --- app/models/device.server.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/device.server.ts b/app/models/device.server.ts index 2ff614de..f5f2ceb6 100644 --- a/app/models/device.server.ts +++ b/app/models/device.server.ts @@ -49,6 +49,7 @@ const BASE_DEVICE_COLUMNS = { status: true, createdAt: true, updatedAt: true, + archivedAt: true, expiresAt: true, useAuth: true, apiKey: true,