-- 0090_water_log_completion.sql -- -- Water Log feature completion. The original `0001_init.sql` shipped the -- core five tables (headgates, irrigators, sessions, log entries, alerts) -- with RLS, but the SaaS rebuild's actions and UIs depend on a wider -- surface that this migration adds: -- -- - `water_headgates.headgate_token` — opaque per-gate token for QR -- - `water_headgates.status` — open / closed / maintenance -- - `water_headgates.max_flow_gpm` — optional high-water marker -- - `water_headgates.notes` — free-form location notes -- - `water_headgates.last_used_at` — denormalized for display -- - `water_headgates.high_threshold` — optional alert ceiling -- - `water_headgates.low_threshold` — optional alert floor -- - `water_headgates.unit` — display unit (CFS/GPM/etc.) -- -- - `water_irrigators.role` — "irrigator" | "water_admin" -- - `water_irrigators.phone` — optional contact -- - `water_irrigators.notes` — free-form -- -- - `water_log_entries.method` — "manual" | "meter" | "estimate" | "qr" -- - `water_log_entries.total_gallons` — derived when computable -- - `water_log_entries.photo_url` — link to storage object -- - `water_log_entries.logged_date` — date-only (YYYY-MM-DD) for fast grouping -- - `water_log_entries.latitude`/`longitude` — optional GPS pin -- - `water_log_entries.brand_id` index upgrade -- -- - NEW `water_admin_settings` (per-brand admin PIN + alert config) -- - NEW `water_admin_sessions` (admin sign-in sessions, separate cookie) -- - NEW `water_audit_log` (who changed what, when) -- -- All new tables follow project conventions: -- - TIMESTAMPTZ for timestamps -- - UUID PKs via gen_random_uuid() -- - brand_id scoped, RLS enabled, FORCE'd -- - CREATE TABLE IF NOT EXISTS for re-runnability -- -- ============================================================================ -- ─── 1. Extend water_headgates ────────────────────────────────────────────── ALTER TABLE water_headgates ADD COLUMN IF NOT EXISTS headgate_token TEXT UNIQUE DEFAULT encode(gen_random_bytes(12), 'hex'), ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'closed', 'maintenance')), ADD COLUMN IF NOT EXISTS max_flow_gpm NUMERIC, ADD COLUMN IF NOT EXISTS notes TEXT, ADD COLUMN IF NOT EXISTS last_used_at TIMESTAMPTZ, ADD COLUMN IF NOT EXISTS high_threshold NUMERIC, ADD COLUMN IF NOT EXISTS low_threshold NUMERIC, ADD COLUMN IF NOT EXISTS unit TEXT NOT NULL DEFAULT 'CFS'; -- Backfill any existing rows that pre-date the default expression UPDATE water_headgates SET headgate_token = encode(gen_random_bytes(12), 'hex') WHERE headgate_token IS NULL; -- ─── 2. Extend water_irrigators ───────────────────────────────────────────── ALTER TABLE water_irrigators ADD COLUMN IF NOT EXISTS role TEXT NOT NULL DEFAULT 'irrigator' CHECK (role IN ('irrigator', 'water_admin')), ADD COLUMN IF NOT EXISTS phone TEXT, ADD COLUMN IF NOT EXISTS notes TEXT; -- ─── 3. Extend water_log_entries ──────────────────────────────────────────── ALTER TABLE water_log_entries ADD COLUMN IF NOT EXISTS method TEXT NOT NULL DEFAULT 'manual' CHECK (method IN ('manual', 'meter', 'estimate', 'qr')), ADD COLUMN IF NOT EXISTS total_gallons NUMERIC, ADD COLUMN IF NOT EXISTS photo_url TEXT, ADD COLUMN IF NOT EXISTS logged_date DATE, ADD COLUMN IF NOT EXISTS latitude DOUBLE PRECISION, ADD COLUMN IF NOT EXISTS longitude DOUBLE PRECISION; -- Backfill logged_date for any rows that pre-date the column UPDATE water_log_entries SET logged_date = (logged_at AT TIME ZONE 'UTC')::date WHERE logged_date IS NULL AND logged_at IS NOT NULL; -- Index for fast "today's entries" + "this week's entries" queries CREATE INDEX IF NOT EXISTS water_log_entries_brand_date_idx ON water_log_entries (brand_id, logged_date DESC); CREATE INDEX IF NOT EXISTS water_log_entries_irrigator_idx ON water_log_entries (irrigator_id, logged_at DESC); -- ─── 4. water_admin_settings (per brand) ─────────────────────────────────── CREATE TABLE IF NOT EXISTS water_admin_settings ( brand_id UUID PRIMARY KEY REFERENCES brands(id) ON DELETE CASCADE, enabled BOOLEAN NOT NULL DEFAULT true, session_duration_hours INTEGER NOT NULL DEFAULT 4 CHECK (session_duration_hours BETWEEN 1 AND 168), can_edit_entries BOOLEAN NOT NULL DEFAULT true, can_delete_entries BOOLEAN NOT NULL DEFAULT true, can_export_csv BOOLEAN NOT NULL DEFAULT true, alert_phone TEXT, alerts_enabled BOOLEAN NOT NULL DEFAULT false, updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_by UUID REFERENCES admin_users(id) ); DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_trigger t JOIN pg_class c ON t.tgrelid = c.oid JOIN pg_namespace n ON c.relnamespace = n.oid WHERE t.tgname = 'water_admin_settings_updated_at' AND n.nspname = current_schema() ) THEN CREATE TRIGGER water_admin_settings_updated_at BEFORE UPDATE ON water_admin_settings FOR EACH ROW EXECUTE FUNCTION set_updated_at(); END IF; END $$; -- ─── 5. water_admin_sessions (separate from irrigator sessions) ─────────── CREATE TABLE IF NOT EXISTS water_admin_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), brand_id UUID NOT NULL REFERENCES brands(id) ON DELETE CASCADE, admin_user_id UUID NOT NULL REFERENCES admin_users(id) ON DELETE CASCADE, pin_hash_used TEXT NOT NULL, expires_at TIMESTAMPTZ NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS water_admin_sessions_admin_idx ON water_admin_sessions (admin_user_id, expires_at DESC); -- ─── 6. water_audit_log (who changed what, when) ─────────────────────────── CREATE TABLE IF NOT EXISTS water_audit_log ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), brand_id UUID NOT NULL REFERENCES brands(id) ON DELETE CASCADE, actor_id UUID REFERENCES admin_users(id), actor_label TEXT NOT NULL, action TEXT NOT NULL, entity_type TEXT NOT NULL, entity_id UUID, details JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS water_audit_log_brand_recent_idx ON water_audit_log (brand_id, created_at DESC); -- ─── 7. RLS for new tables ──────────────────────────────────────────────── ALTER TABLE water_admin_settings ENABLE ROW LEVEL SECURITY; ALTER TABLE water_admin_sessions ENABLE ROW LEVEL SECURITY; ALTER TABLE water_audit_log ENABLE ROW LEVEL SECURITY; ALTER TABLE water_admin_settings FORCE ROW LEVEL SECURITY; ALTER TABLE water_admin_sessions FORCE ROW LEVEL SECURITY; ALTER TABLE water_audit_log FORCE ROW LEVEL SECURITY; DROP POLICY IF EXISTS tenant_isolation ON water_admin_settings; CREATE POLICY tenant_isolation ON water_admin_settings FOR ALL USING (brand_id = current_brand_id() OR is_platform_admin()) WITH CHECK (brand_id = current_brand_id() OR is_platform_admin()); DROP POLICY IF EXISTS tenant_isolation ON water_admin_sessions; CREATE POLICY tenant_isolation ON water_admin_sessions FOR ALL USING (brand_id = current_brand_id() OR is_platform_admin()) WITH CHECK (brand_id = current_brand_id() OR is_platform_admin()); DROP POLICY IF EXISTS tenant_isolation ON water_audit_log; CREATE POLICY tenant_isolation ON water_audit_log FOR ALL USING (brand_id = current_brand_id() OR is_platform_admin()) WITH CHECK (brand_id = current_brand_id() OR is_platform_admin());