-- Network Infrastructure Map — V1 schema. -- Adds 7 tables (sites, locations, devices, ports, links, vlans, imports) -- plus a private Storage bucket for uploaded source documents. -- RLS: read for admin/it_staff/dispatcher/programmer; write for admin/it_staff only. -- See plan: C:\Users\marcr\.claude\plans\i-m-thinking-of-nested-lagoon.md -- ============================================================================= -- TABLES -- ============================================================================= -- Sites: top-level physical locations (campus, building treated standalone) CREATE TABLE IF NOT EXISTS public.network_sites ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL, address text, notes text, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), created_by uuid REFERENCES public.profiles(id) ); -- Locations: nested physical hierarchy within a site CREATE TABLE IF NOT EXISTS public.network_locations ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), site_id uuid NOT NULL REFERENCES public.network_sites(id) ON DELETE CASCADE, parent_id uuid REFERENCES public.network_locations(id) ON DELETE CASCADE, name text NOT NULL, kind text NOT NULL CHECK (kind IN ('building','floor','room','rack','wall_jack','other')), position_label text, notes text, created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_network_locations_site ON public.network_locations(site_id); CREATE INDEX IF NOT EXISTS idx_network_locations_parent ON public.network_locations(parent_id); -- Devices: switches, routers, APs, endpoints, patch panels CREATE TABLE IF NOT EXISTS public.network_devices ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL, kind text NOT NULL CHECK (kind IN ('router','switch','ap','firewall','server','endpoint','patch_panel','other')), role text CHECK (role IN ('core','distribution','access','edge','endpoint')), vendor text, model text, serial text, mgmt_ip inet, mac macaddr, location_id uuid REFERENCES public.network_locations(id) ON DELETE SET NULL, import_source text NOT NULL DEFAULT 'manual' CHECK (import_source IN ('manual','ai_import','agent')), notes text, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_network_devices_location ON public.network_devices(location_id); CREATE INDEX IF NOT EXISTS idx_network_devices_role ON public.network_devices(role); CREATE INDEX IF NOT EXISTS idx_network_devices_kind ON public.network_devices(kind); -- Ports: physical/logical ports on a device CREATE TABLE IF NOT EXISTS public.network_ports ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), device_id uuid NOT NULL REFERENCES public.network_devices(id) ON DELETE CASCADE, port_number text NOT NULL, port_kind text CHECK (port_kind IN ('rj45','sfp','sfp_plus','console','power','wireless','virtual','other')), speed_mbps integer, access_vlan integer, is_trunk boolean NOT NULL DEFAULT false, trunk_vlans integer[], notes text, UNIQUE (device_id, port_number) ); CREATE INDEX IF NOT EXISTS idx_network_ports_device ON public.network_ports(device_id); -- Links: port-to-port edges. Canonical ordering (port_a < port_b) prevents A↔B/B↔A dupes. CREATE TABLE IF NOT EXISTS public.network_links ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), port_a uuid NOT NULL REFERENCES public.network_ports(id) ON DELETE CASCADE, port_b uuid NOT NULL REFERENCES public.network_ports(id) ON DELETE CASCADE, link_kind text CHECK (link_kind IN ('copper','fiber','wireless','virtual','unknown')), cable_label text, notes text, created_at timestamptz NOT NULL DEFAULT now(), CHECK (port_a < port_b), UNIQUE (port_a, port_b) ); -- VLANs (org-scoped, named) CREATE TABLE IF NOT EXISTS public.network_vlans ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), vlan_id integer NOT NULL UNIQUE, name text NOT NULL, description text, color text ); -- Import sessions: each upload + extraction state + raw AI result for audit/replay CREATE TABLE IF NOT EXISTS public.network_imports ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), storage_path text NOT NULL, file_kind text NOT NULL CHECK (file_kind IN ('pdf','image','vsdx','csv','xlsx','docx','other')), status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','extracting','review','applied','discarded','failed')), ai_result jsonb, error_message text, applied_device_ids uuid[], applied_link_ids uuid[], applied_at timestamptz, created_by uuid NOT NULL REFERENCES public.profiles(id), created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_network_imports_status ON public.network_imports(status); CREATE INDEX IF NOT EXISTS idx_network_imports_created_by ON public.network_imports(created_by); -- ============================================================================= -- updated_at TRIGGERS -- ============================================================================= -- Reuses common pattern from prior Tasq migrations. CREATE OR REPLACE FUNCTION public.network_map_set_updated_at() RETURNS trigger AS $$ BEGIN NEW.updated_at = now(); RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS trg_network_sites_updated_at ON public.network_sites; CREATE TRIGGER trg_network_sites_updated_at BEFORE UPDATE ON public.network_sites FOR EACH ROW EXECUTE FUNCTION public.network_map_set_updated_at(); DROP TRIGGER IF EXISTS trg_network_devices_updated_at ON public.network_devices; CREATE TRIGGER trg_network_devices_updated_at BEFORE UPDATE ON public.network_devices FOR EACH ROW EXECUTE FUNCTION public.network_map_set_updated_at(); -- ============================================================================= -- ROW LEVEL SECURITY -- ============================================================================= -- Pattern matches supabase/migrations/20260223090000_services_read_only_for_standard_it_dispatcher.sql -- READ: admin + it_staff + dispatcher + programmer -- WRITE: admin + it_staff ALTER TABLE public.network_sites ENABLE ROW LEVEL SECURITY; ALTER TABLE public.network_locations ENABLE ROW LEVEL SECURITY; ALTER TABLE public.network_devices ENABLE ROW LEVEL SECURITY; ALTER TABLE public.network_ports ENABLE ROW LEVEL SECURITY; ALTER TABLE public.network_links ENABLE ROW LEVEL SECURITY; ALTER TABLE public.network_vlans ENABLE ROW LEVEL SECURITY; ALTER TABLE public.network_imports ENABLE ROW LEVEL SECURITY; -- ----- network_sites ----- DROP POLICY IF EXISTS "Network sites: select" ON public.network_sites; CREATE POLICY "Network sites: select" ON public.network_sites FOR SELECT USING ( EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff','dispatcher','programmer')) ); DROP POLICY IF EXISTS "Network sites: write" ON public.network_sites; CREATE POLICY "Network sites: write" ON public.network_sites FOR ALL USING ( EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff')) ) WITH CHECK ( EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff')) ); -- ----- network_locations ----- DROP POLICY IF EXISTS "Network locations: select" ON public.network_locations; CREATE POLICY "Network locations: select" ON public.network_locations FOR SELECT USING ( EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff','dispatcher','programmer')) ); DROP POLICY IF EXISTS "Network locations: write" ON public.network_locations; CREATE POLICY "Network locations: write" ON public.network_locations FOR ALL USING ( EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff')) ) WITH CHECK ( EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff')) ); -- ----- network_devices ----- DROP POLICY IF EXISTS "Network devices: select" ON public.network_devices; CREATE POLICY "Network devices: select" ON public.network_devices FOR SELECT USING ( EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff','dispatcher','programmer')) ); DROP POLICY IF EXISTS "Network devices: write" ON public.network_devices; CREATE POLICY "Network devices: write" ON public.network_devices FOR ALL USING ( EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff')) ) WITH CHECK ( EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff')) ); -- ----- network_ports ----- DROP POLICY IF EXISTS "Network ports: select" ON public.network_ports; CREATE POLICY "Network ports: select" ON public.network_ports FOR SELECT USING ( EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff','dispatcher','programmer')) ); DROP POLICY IF EXISTS "Network ports: write" ON public.network_ports; CREATE POLICY "Network ports: write" ON public.network_ports FOR ALL USING ( EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff')) ) WITH CHECK ( EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff')) ); -- ----- network_links ----- DROP POLICY IF EXISTS "Network links: select" ON public.network_links; CREATE POLICY "Network links: select" ON public.network_links FOR SELECT USING ( EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff','dispatcher','programmer')) ); DROP POLICY IF EXISTS "Network links: write" ON public.network_links; CREATE POLICY "Network links: write" ON public.network_links FOR ALL USING ( EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff')) ) WITH CHECK ( EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff')) ); -- ----- network_vlans ----- DROP POLICY IF EXISTS "Network vlans: select" ON public.network_vlans; CREATE POLICY "Network vlans: select" ON public.network_vlans FOR SELECT USING ( EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff','dispatcher','programmer')) ); DROP POLICY IF EXISTS "Network vlans: write" ON public.network_vlans; CREATE POLICY "Network vlans: write" ON public.network_vlans FOR ALL USING ( EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff')) ) WITH CHECK ( EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff')) ); -- ----- network_imports ----- DROP POLICY IF EXISTS "Network imports: select" ON public.network_imports; CREATE POLICY "Network imports: select" ON public.network_imports FOR SELECT USING ( EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff','dispatcher','programmer')) ); DROP POLICY IF EXISTS "Network imports: write" ON public.network_imports; CREATE POLICY "Network imports: write" ON public.network_imports FOR ALL USING ( EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff')) ) WITH CHECK ( EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff')) ); -- ============================================================================= -- STORAGE BUCKET (private) for uploaded source documents -- ============================================================================= -- The Edge Function `extract_topology` uses the service role to read from this -- bucket. Authenticated admin/it_staff clients upload via signed URLs or with -- their RLS-bound JWT. INSERT INTO storage.buckets (id, name, public) VALUES ('network-imports', 'network-imports', false) ON CONFLICT (id) DO NOTHING; -- Storage policies: only admin/it_staff can upload; same group + dispatcher/programmer can read. DROP POLICY IF EXISTS "Network imports bucket: select" ON storage.objects; CREATE POLICY "Network imports bucket: select" ON storage.objects FOR SELECT USING ( bucket_id = 'network-imports' AND EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff','dispatcher','programmer')) ); DROP POLICY IF EXISTS "Network imports bucket: write" ON storage.objects; CREATE POLICY "Network imports bucket: write" ON storage.objects FOR INSERT WITH CHECK ( bucket_id = 'network-imports' AND EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff')) ); DROP POLICY IF EXISTS "Network imports bucket: update" ON storage.objects; CREATE POLICY "Network imports bucket: update" ON storage.objects FOR UPDATE USING ( bucket_id = 'network-imports' AND EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff')) ); DROP POLICY IF EXISTS "Network imports bucket: delete" ON storage.objects; CREATE POLICY "Network imports bucket: delete" ON storage.objects FOR DELETE USING ( bucket_id = 'network-imports' AND EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','it_staff')) );