BEGIN; -- Cameras, owned by head office rather than by the PC they run on. -- -- Until now a camera existed only in `cameras.json` on one shop's disk, added -- through the desktop app by somebody standing in that shop. That is fine for -- the shop and impossible for the tenant: an owner onboarding a new store, or -- fixing a camera in a branch they are not standing in, had no way to do it. -- -- The shop PC stays the thing that CONNECTS to the camera - it is on the same -- LAN, and nothing else can be - so this table is desired state that the agent -- pulls and applies. The engine's own store remains the running config; these -- two are reconciled, not merged. CREATE TABLE site_cameras ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), client_id uuid NOT NULL REFERENCES clients(id) ON DELETE CASCADE, site_id uuid NOT NULL REFERENCES sites(id) ON DELETE CASCADE, -- The id the ENGINE knows this camera by, and the one that lands in -- visits.camera_id. Stable for the life of the camera: renaming it would -- orphan every visit already recorded against the old name. camera_id text NOT NULL, label text NOT NULL DEFAULT '', -- Connection, split into parts rather than stored as one URL. The engine -- builds the RTSP URL itself with percent-encoded credentials, because a -- password containing '@' in a hand-assembled URL is the exact bug the -- original project shipped. host text NOT NULL DEFAULT '', port integer NOT NULL DEFAULT 554, path text NOT NULL DEFAULT '/', username text NOT NULL DEFAULT '', -- Encrypted, never hashed: the agent has to be able to USE it. Same -- treatment as a site's broker password, and for the same reason. -- -- This is a real widening of what the server holds. An RTSP credential is -- a live path into the camera itself, and until now it lived only on the -- shop PC under DPAPI. Putting it here is the price of onboarding a camera -- from head office, and it is sealed with the site id as additional data so -- a row copied between sites will not decrypt. password_enc bytea, max_width integer NOT NULL DEFAULT 1280, tuning jsonb NOT NULL DEFAULT '{}'::jsonb, enabled boolean NOT NULL DEFAULT true, -- Bumped on every edit. The agent compares it to what it last applied, so -- a reconcile is cheap when nothing has changed - which is almost always. revision bigint NOT NULL DEFAULT 1, -- Observed state, written by the agent. Kept beside desired state on -- purpose: "this camera is configured" and "this camera is working" are -- the two halves of the only question anyone asks about a camera, and -- splitting them across tables makes the screen that answers it a join -- nobody remembers to write. connected boolean, last_seen_at timestamptz, -- The most recent frame, as an object key. NOT a URL, for the same reason -- face images are keys: a stored URL is permanent and unrevocable. snapshot_key text NOT NULL DEFAULT '', snapshot_at timestamptz, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), -- A tombstone, not a delete. The agent ADOPTS cameras it finds configured -- locally, so a hard delete would be undone on the next sync by the very -- camera the operator just removed. deleted_at timestamptz, UNIQUE (site_id, camera_id) ); CREATE INDEX site_cameras_site_idx ON site_cameras (site_id) WHERE deleted_at IS NULL; CREATE INDEX site_cameras_client_idx ON site_cameras (client_id) WHERE deleted_at IS NULL; COMMIT;