Tie points to locator serial numbers and store receiver telemetry

Field setups often relay through a phone/gateway that owns the MQTT
connection, so the publishing credential and the instrument are now
separate concepts:

- Points messages carry a "serial" identifying the locator receiver;
  unknown serials are auto-registered as devices in the publisher's
  org (name "Locator <serial>"). Device.mqttUsername is now optional
  and serialNumber is unique per org.
- LocatePoint gains standard receiver telemetry: frequencyHz,
  currentMa, signalDb, gainDb, locateMode (PEAK/NULL/BROAD_PEAK/SONDE),
  phaseDeg, compassDeg, distortionPct, plus GPS quality columns
  vAccuracy, satellites, hdop. All optional; raw payload still kept.
- Migration hand-edited to preserve the generated geom column and its
  GIST index (Prisma diff wanted to drop both).
- REST create/point DTOs, map tooltips, job-detail latest-point
  readout, devices settings form, seed, and the sample publisher all
  carry the new fields.
- Added .dockerignore for backend/web: COPY . . was clobbering the
  image's freshly generated Prisma client with the host's stale
  node_modules, breaking image builds after schema changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
ulhub
2026-07-14 14:58:12 +00:00
parent eae4075265
commit 21f5a04133
16 changed files with 380 additions and 68 deletions

View File

@@ -0,0 +1,25 @@
-- Locator receiver telemetry + device identity by serial number.
-- NOTE: hand-edited — Prisma's diff wanted to drop the geom GIST index and the
-- generated-column expression on locate_points.geom; those statements were removed.
-- CreateEnum
CREATE TYPE "LocateMode" AS ENUM ('PEAK', 'NULL', 'BROAD_PEAK', 'SONDE');
-- AlterTable
ALTER TABLE "devices" ALTER COLUMN "mqttUsername" DROP NOT NULL;
-- AlterTable
ALTER TABLE "locate_points" ADD COLUMN "compassDeg" DECIMAL(5,2),
ADD COLUMN "currentMa" DECIMAL(9,3),
ADD COLUMN "distortionPct" DECIMAL(5,2),
ADD COLUMN "frequencyHz" INTEGER,
ADD COLUMN "gainDb" DECIMAL(6,2),
ADD COLUMN "hdop" DECIMAL(4,2),
ADD COLUMN "locateMode" "LocateMode",
ADD COLUMN "phaseDeg" DECIMAL(6,2),
ADD COLUMN "satellites" INTEGER,
ADD COLUMN "signalDb" DECIMAL(6,2),
ADD COLUMN "vAccuracy" DECIMAL(7,3);
-- CreateIndex
CREATE UNIQUE INDEX "devices_orgId_serialNumber_key" ON "devices"("orgId", "serialNumber");

View File

@@ -45,6 +45,14 @@ enum GpsFixType {
FIXED_RTK
}
// EM locator receiver antenna mode used when the point was captured
enum LocateMode {
PEAK
NULL
BROAD_PEAK
SONDE
}
model Organization {
id String @id @default(cuid())
name String
@@ -116,12 +124,16 @@ model Job {
@@map("jobs")
}
// A device is either a locator receiver (identified by serialNumber, usually
// auto-registered from incoming data) or an MQTT publisher (app/gateway with
// broker credentials, identified by mqttUsername) — or both, when a locator
// connects to the broker directly.
model Device {
id String @id @default(cuid())
orgId String
name String
serialNumber String?
mqttUsername String @unique
mqttUsername String? @unique
isActive Boolean @default(true)
lastSeenAt DateTime? @db.Timestamptz(6)
createdAt DateTime @default(now()) @db.Timestamptz(6)
@@ -130,6 +142,7 @@ model Device {
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
points LocatePoint[]
@@unique([orgId, serialNumber])
@@map("devices")
}
@@ -140,15 +153,31 @@ model LocatePoint {
lat Decimal @db.Decimal(10, 8)
lng Decimal @db.Decimal(11, 8)
altitude Decimal? @db.Decimal(8, 3)
fixType GpsFixType @default(NONE)
hAccuracy Decimal? @db.Decimal(7, 3)
depth Decimal? @db.Decimal(6, 3)
utilityType UtilityType @default(UNKNOWN)
sequence Int?
recordedAt DateTime @db.Timestamptz(6)
receivedAt DateTime @default(now()) @db.Timestamptz(6)
raw Json?
geom Unsupported("geometry(Point, 4326)")?
// GPS quality
fixType GpsFixType @default(NONE)
hAccuracy Decimal? @db.Decimal(7, 3) // meters
vAccuracy Decimal? @db.Decimal(7, 3) // meters
satellites Int?
hdop Decimal? @db.Decimal(4, 2)
// Locator receiver telemetry
depth Decimal? @db.Decimal(6, 3) // meters below grade
frequencyHz Int? // active/passive locate frequency
currentMa Decimal? @db.Decimal(9, 3) // signal current on the line
signalDb Decimal? @db.Decimal(6, 2) // signal strength
gainDb Decimal? @db.Decimal(6, 2) // receiver gain
locateMode LocateMode?
phaseDeg Decimal? @db.Decimal(6, 2)
compassDeg Decimal? @db.Decimal(5, 2) // line direction, 0-360
distortionPct Decimal? @db.Decimal(5, 2)
recordedAt DateTime @db.Timestamptz(6)
receivedAt DateTime @default(now()) @db.Timestamptz(6)
raw Json?
geom Unsupported("geometry(Point, 4326)")?
job Job @relation(fields: [jobId], references: [id], onDelete: Cascade)
device Device? @relation(fields: [deviceId], references: [id], onDelete: SetNull)

View File

@@ -1,4 +1,4 @@
import { PrismaClient, GpsFixType, JobStatus, UtilityType } from '@prisma/client';
import { PrismaClient, GpsFixType, JobStatus, LocateMode, UtilityType } from '@prisma/client';
import * as bcrypt from 'bcryptjs';
const prisma = new PrismaClient();
@@ -68,7 +68,17 @@ async function main() {
altitude: 187.4 + i * 0.02,
fixType: GpsFixType.FIXED_RTK,
hAccuracy: 0.014,
vAccuracy: 0.021,
satellites: 22,
hdop: 0.7,
depth: 1.2,
frequencyHz: 33000,
currentMa: 48.5 - i * 0.4,
signalDb: 62.1 - i * 0.3,
gainDb: 40,
locateMode: LocateMode.PEAK,
compassDeg: 17.5,
distortionPct: 4.2,
utilityType: UtilityType.GAS,
sequence: i + 1,
recordedAt: new Date(Date.now() - (12 - i) * 5000),