diff --git a/.env.example b/.env.example index d61bb11..14afced 100644 --- a/.env.example +++ b/.env.example @@ -80,5 +80,5 @@ SENTRY_DSN= # GOTIFY_TOKEN= # Analytics (Umami) -NEXT_PUBLIC_UMAMI_WEBSITE_ID= -NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js +UMAMI_WEBSITE_ID= +UMAMI_API_ENDPOINT=https://analytics.infra.mintel.me diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index f33e80d..20d3e39 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -195,6 +195,7 @@ jobs: --build-arg NEXT_PUBLIC_BASE_URL=${{ needs.prepare.outputs.next_public_base_url }} \ --build-arg NEXT_PUBLIC_TARGET=${{ needs.prepare.outputs.target }} \ --build-arg DIRECTUS_URL=${{ needs.prepare.outputs.directus_url }} \ + --build-arg UMAMI_API_ENDPOINT=${{ secrets.UMAMI_API_ENDPOINT || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }} \ -t registry.infra.mintel.me/mintel/mb-grid-solutions:${{ needs.prepare.outputs.image_tag }} \ --push . @@ -263,8 +264,8 @@ jobs: SENTRY_DSN=${{ secrets.SENTRY_DSN || vars.SENTRY_DSN }} GOTIFY_URL=${{ secrets.GOTIFY_URL || vars.GOTIFY_URL }} GOTIFY_TOKEN=${{ secrets.GOTIFY_TOKEN || vars.GOTIFY_TOKEN }} - NEXT_PUBLIC_UMAMI_WEBSITE_ID=${{ secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }} - NEXT_PUBLIC_UMAMI_SCRIPT_URL=${{ secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || vars.NEXT_PUBLIC_UMAMI_SCRIPT_URL }} + UMAMI_WEBSITE_ID=${{ secrets.UMAMI_WEBSITE_ID || secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID || vars.NEXT_PUBLIC_UMAMI_WEBSITE_ID }} + UMAMI_API_ENDPOINT=${{ secrets.UMAMI_API_ENDPOINT || secrets.NEXT_PUBLIC_UMAMI_SCRIPT_URL || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }} # Project PROJECT_COLOR=${{ secrets.PROJECT_COLOR || vars.PROJECT_COLOR || '#82ed20' }} diff --git a/Dockerfile b/Dockerfile index cb60cc9..8421437 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,15 +8,13 @@ RUN rm -rf packages apps pnpm-workspace.yaml 2>/dev/null || true # Build-time environment variables for Next.js ARG NEXT_PUBLIC_BASE_URL -ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID -ARG NEXT_PUBLIC_UMAMI_SCRIPT_URL +ARG UMAMI_API_ENDPOINT ARG NEXT_PUBLIC_TARGET ARG DIRECTUS_URL ARG NPM_TOKEN ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL -ENV NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID -ENV NEXT_PUBLIC_UMAMI_SCRIPT_URL=$NEXT_PUBLIC_UMAMI_SCRIPT_URL +ENV UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET ENV DIRECTUS_URL=$DIRECTUS_URL ENV NPM_TOKEN=$NPM_TOKEN diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx index e4afc97..d151f25 100644 --- a/app/[locale]/layout.tsx +++ b/app/[locale]/layout.tsx @@ -5,6 +5,8 @@ import "../globals.css"; import { NextIntlClientProvider } from "next-intl"; import { getMessages } from "next-intl/server"; import { notFound } from "next/navigation"; +import AnalyticsProvider from "@/components/analytics/AnalyticsProvider"; +import { config } from "@/lib/config"; const inter = Inter({ subsets: ["latin"], @@ -105,6 +107,13 @@ export default async function RootLayout({ }, }; + // Track pageview on the server + // This is safe to call here because layout is a Server Component + const services = ( + await import("@/lib/services/create-services.server") + ).getServerAppServices(); + services.analytics.trackPageview(); + return ( @@ -115,6 +124,7 @@ export default async function RootLayout({ + {children} diff --git a/components/analytics/AnalyticsProvider.tsx b/components/analytics/AnalyticsProvider.tsx new file mode 100644 index 0000000..ddadb41 --- /dev/null +++ b/components/analytics/AnalyticsProvider.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { useEffect } from "react"; +import { usePathname, useSearchParams } from "next/navigation"; +import { getAppServices } from "@/lib/services/create-services"; + +/** + * AnalyticsProvider Component + * + * Automatically tracks pageviews on client-side route changes. + * This component should be placed inside your layout to handle navigation events. + * + * @param {Object} props - Component props + * @param {string} [props.websiteId] - The Umami website ID (passed from server config) + * + * @example + * ```tsx + * // In your layout.tsx + * const { websiteId } = config.analytics.umami; + * + * ``` + */ +export default function AnalyticsProvider({ + websiteId, +}: { + websiteId?: string; +}) { + const pathname = usePathname(); + const searchParams = useSearchParams(); + + useEffect(() => { + if (!pathname) return; + + const services = getAppServices(); + const url = `${pathname}${searchParams?.size ? `?${searchParams.toString()}` : ""}`; + + // Track pageview with the full URL + services.analytics.trackPageview(url); + + if (process.env.NODE_ENV === "development") { + console.log("[Umami] Tracked pageview:", url); + } + }, [pathname, searchParams]); + + if (!websiteId) return null; + + return null; +} diff --git a/dump.sql b/dump.sql new file mode 100644 index 0000000..a4cf2e7 --- /dev/null +++ b/dump.sql @@ -0,0 +1,2017 @@ +-- +-- PostgreSQL database dump +-- + +\restrict CbprhGcTL0byLeqVbuTsFhBReWMe8OyOY53RONJHhoY17zx1fVVjxl30zrOpqe6 + +-- Dumped from database version 15.15 +-- Dumped by pg_dump version 15.15 + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +ALTER TABLE IF EXISTS ONLY public.directus_versions DROP CONSTRAINT IF EXISTS directus_versions_user_updated_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_versions DROP CONSTRAINT IF EXISTS directus_versions_user_created_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_versions DROP CONSTRAINT IF EXISTS directus_versions_collection_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_users DROP CONSTRAINT IF EXISTS directus_users_role_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_shares DROP CONSTRAINT IF EXISTS directus_shares_user_created_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_shares DROP CONSTRAINT IF EXISTS directus_shares_role_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_shares DROP CONSTRAINT IF EXISTS directus_shares_collection_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_settings DROP CONSTRAINT IF EXISTS directus_settings_storage_default_folder_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_settings DROP CONSTRAINT IF EXISTS directus_settings_public_registration_role_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_settings DROP CONSTRAINT IF EXISTS directus_settings_public_foreground_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_settings DROP CONSTRAINT IF EXISTS directus_settings_public_favicon_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_settings DROP CONSTRAINT IF EXISTS directus_settings_public_background_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_settings DROP CONSTRAINT IF EXISTS directus_settings_project_logo_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_sessions DROP CONSTRAINT IF EXISTS directus_sessions_user_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_sessions DROP CONSTRAINT IF EXISTS directus_sessions_share_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_roles DROP CONSTRAINT IF EXISTS directus_roles_parent_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_revisions DROP CONSTRAINT IF EXISTS directus_revisions_version_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_revisions DROP CONSTRAINT IF EXISTS directus_revisions_parent_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_revisions DROP CONSTRAINT IF EXISTS directus_revisions_activity_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_presets DROP CONSTRAINT IF EXISTS directus_presets_user_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_presets DROP CONSTRAINT IF EXISTS directus_presets_role_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_permissions DROP CONSTRAINT IF EXISTS directus_permissions_policy_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_panels DROP CONSTRAINT IF EXISTS directus_panels_user_created_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_panels DROP CONSTRAINT IF EXISTS directus_panels_dashboard_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_operations DROP CONSTRAINT IF EXISTS directus_operations_user_created_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_operations DROP CONSTRAINT IF EXISTS directus_operations_resolve_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_operations DROP CONSTRAINT IF EXISTS directus_operations_reject_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_operations DROP CONSTRAINT IF EXISTS directus_operations_flow_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_notifications DROP CONSTRAINT IF EXISTS directus_notifications_sender_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_notifications DROP CONSTRAINT IF EXISTS directus_notifications_recipient_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_folders DROP CONSTRAINT IF EXISTS directus_folders_parent_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_flows DROP CONSTRAINT IF EXISTS directus_flows_user_created_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_files DROP CONSTRAINT IF EXISTS directus_files_uploaded_by_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_files DROP CONSTRAINT IF EXISTS directus_files_modified_by_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_files DROP CONSTRAINT IF EXISTS directus_files_folder_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_dashboards DROP CONSTRAINT IF EXISTS directus_dashboards_user_created_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_comments DROP CONSTRAINT IF EXISTS directus_comments_user_updated_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_comments DROP CONSTRAINT IF EXISTS directus_comments_user_created_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_collections DROP CONSTRAINT IF EXISTS directus_collections_group_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_access DROP CONSTRAINT IF EXISTS directus_access_user_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_access DROP CONSTRAINT IF EXISTS directus_access_role_foreign; +ALTER TABLE IF EXISTS ONLY public.directus_access DROP CONSTRAINT IF EXISTS directus_access_policy_foreign; +DROP INDEX IF EXISTS public.directus_revisions_parent_index; +DROP INDEX IF EXISTS public.directus_revisions_activity_index; +DROP INDEX IF EXISTS public.directus_activity_timestamp_index; +ALTER TABLE IF EXISTS ONLY public.directus_versions DROP CONSTRAINT IF EXISTS directus_versions_pkey; +ALTER TABLE IF EXISTS ONLY public.directus_users DROP CONSTRAINT IF EXISTS directus_users_token_unique; +ALTER TABLE IF EXISTS ONLY public.directus_users DROP CONSTRAINT IF EXISTS directus_users_pkey; +ALTER TABLE IF EXISTS ONLY public.directus_users DROP CONSTRAINT IF EXISTS directus_users_external_identifier_unique; +ALTER TABLE IF EXISTS ONLY public.directus_users DROP CONSTRAINT IF EXISTS directus_users_email_unique; +ALTER TABLE IF EXISTS ONLY public.directus_translations DROP CONSTRAINT IF EXISTS directus_translations_pkey; +ALTER TABLE IF EXISTS ONLY public.directus_shares DROP CONSTRAINT IF EXISTS directus_shares_pkey; +ALTER TABLE IF EXISTS ONLY public.directus_settings DROP CONSTRAINT IF EXISTS directus_settings_pkey; +ALTER TABLE IF EXISTS ONLY public.directus_sessions DROP CONSTRAINT IF EXISTS directus_sessions_pkey; +ALTER TABLE IF EXISTS ONLY public.directus_roles DROP CONSTRAINT IF EXISTS directus_roles_pkey; +ALTER TABLE IF EXISTS ONLY public.directus_revisions DROP CONSTRAINT IF EXISTS directus_revisions_pkey; +ALTER TABLE IF EXISTS ONLY public.directus_relations DROP CONSTRAINT IF EXISTS directus_relations_pkey; +ALTER TABLE IF EXISTS ONLY public.directus_presets DROP CONSTRAINT IF EXISTS directus_presets_pkey; +ALTER TABLE IF EXISTS ONLY public.directus_policies DROP CONSTRAINT IF EXISTS directus_policies_pkey; +ALTER TABLE IF EXISTS ONLY public.directus_permissions DROP CONSTRAINT IF EXISTS directus_permissions_pkey; +ALTER TABLE IF EXISTS ONLY public.directus_panels DROP CONSTRAINT IF EXISTS directus_panels_pkey; +ALTER TABLE IF EXISTS ONLY public.directus_operations DROP CONSTRAINT IF EXISTS directus_operations_resolve_unique; +ALTER TABLE IF EXISTS ONLY public.directus_operations DROP CONSTRAINT IF EXISTS directus_operations_reject_unique; +ALTER TABLE IF EXISTS ONLY public.directus_operations DROP CONSTRAINT IF EXISTS directus_operations_pkey; +ALTER TABLE IF EXISTS ONLY public.directus_notifications DROP CONSTRAINT IF EXISTS directus_notifications_pkey; +ALTER TABLE IF EXISTS ONLY public.directus_migrations DROP CONSTRAINT IF EXISTS directus_migrations_pkey; +ALTER TABLE IF EXISTS ONLY public.directus_folders DROP CONSTRAINT IF EXISTS directus_folders_pkey; +ALTER TABLE IF EXISTS ONLY public.directus_flows DROP CONSTRAINT IF EXISTS directus_flows_pkey; +ALTER TABLE IF EXISTS ONLY public.directus_flows DROP CONSTRAINT IF EXISTS directus_flows_operation_unique; +ALTER TABLE IF EXISTS ONLY public.directus_files DROP CONSTRAINT IF EXISTS directus_files_pkey; +ALTER TABLE IF EXISTS ONLY public.directus_fields DROP CONSTRAINT IF EXISTS directus_fields_pkey; +ALTER TABLE IF EXISTS ONLY public.directus_extensions DROP CONSTRAINT IF EXISTS directus_extensions_pkey; +ALTER TABLE IF EXISTS ONLY public.directus_dashboards DROP CONSTRAINT IF EXISTS directus_dashboards_pkey; +ALTER TABLE IF EXISTS ONLY public.directus_comments DROP CONSTRAINT IF EXISTS directus_comments_pkey; +ALTER TABLE IF EXISTS ONLY public.directus_collections DROP CONSTRAINT IF EXISTS directus_collections_pkey; +ALTER TABLE IF EXISTS ONLY public.directus_activity DROP CONSTRAINT IF EXISTS directus_activity_pkey; +ALTER TABLE IF EXISTS ONLY public.directus_access DROP CONSTRAINT IF EXISTS directus_access_pkey; +ALTER TABLE IF EXISTS ONLY public.contact_submissions DROP CONSTRAINT IF EXISTS contact_submissions_pkey; +ALTER TABLE IF EXISTS public.directus_settings ALTER COLUMN id DROP DEFAULT; +ALTER TABLE IF EXISTS public.directus_revisions ALTER COLUMN id DROP DEFAULT; +ALTER TABLE IF EXISTS public.directus_relations ALTER COLUMN id DROP DEFAULT; +ALTER TABLE IF EXISTS public.directus_presets ALTER COLUMN id DROP DEFAULT; +ALTER TABLE IF EXISTS public.directus_permissions ALTER COLUMN id DROP DEFAULT; +ALTER TABLE IF EXISTS public.directus_notifications ALTER COLUMN id DROP DEFAULT; +ALTER TABLE IF EXISTS public.directus_fields ALTER COLUMN id DROP DEFAULT; +ALTER TABLE IF EXISTS public.directus_activity ALTER COLUMN id DROP DEFAULT; +ALTER TABLE IF EXISTS public.contact_submissions ALTER COLUMN id DROP DEFAULT; +DROP TABLE IF EXISTS public.directus_versions; +DROP TABLE IF EXISTS public.directus_users; +DROP TABLE IF EXISTS public.directus_translations; +DROP TABLE IF EXISTS public.directus_shares; +DROP SEQUENCE IF EXISTS public.directus_settings_id_seq; +DROP TABLE IF EXISTS public.directus_settings; +DROP TABLE IF EXISTS public.directus_sessions; +DROP TABLE IF EXISTS public.directus_roles; +DROP SEQUENCE IF EXISTS public.directus_revisions_id_seq; +DROP TABLE IF EXISTS public.directus_revisions; +DROP SEQUENCE IF EXISTS public.directus_relations_id_seq; +DROP TABLE IF EXISTS public.directus_relations; +DROP SEQUENCE IF EXISTS public.directus_presets_id_seq; +DROP TABLE IF EXISTS public.directus_presets; +DROP TABLE IF EXISTS public.directus_policies; +DROP SEQUENCE IF EXISTS public.directus_permissions_id_seq; +DROP TABLE IF EXISTS public.directus_permissions; +DROP TABLE IF EXISTS public.directus_panels; +DROP TABLE IF EXISTS public.directus_operations; +DROP SEQUENCE IF EXISTS public.directus_notifications_id_seq; +DROP TABLE IF EXISTS public.directus_notifications; +DROP TABLE IF EXISTS public.directus_migrations; +DROP TABLE IF EXISTS public.directus_folders; +DROP TABLE IF EXISTS public.directus_flows; +DROP TABLE IF EXISTS public.directus_files; +DROP SEQUENCE IF EXISTS public.directus_fields_id_seq; +DROP TABLE IF EXISTS public.directus_fields; +DROP TABLE IF EXISTS public.directus_extensions; +DROP TABLE IF EXISTS public.directus_dashboards; +DROP TABLE IF EXISTS public.directus_comments; +DROP TABLE IF EXISTS public.directus_collections; +DROP SEQUENCE IF EXISTS public.directus_activity_id_seq; +DROP TABLE IF EXISTS public.directus_activity; +DROP TABLE IF EXISTS public.directus_access; +DROP SEQUENCE IF EXISTS public.contact_submissions_id_seq; +DROP TABLE IF EXISTS public.contact_submissions; +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: contact_submissions; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.contact_submissions ( + id integer NOT NULL, + name character varying(255), + email character varying(255), + company character varying(255), + message text, + date_created timestamp with time zone +); + + +-- +-- Name: contact_submissions_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.contact_submissions_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: contact_submissions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.contact_submissions_id_seq OWNED BY public.contact_submissions.id; + + +-- +-- Name: directus_access; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.directus_access ( + id uuid NOT NULL, + role uuid, + "user" uuid, + policy uuid NOT NULL, + sort integer +); + + +-- +-- Name: directus_activity; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.directus_activity ( + id integer NOT NULL, + action character varying(45) NOT NULL, + "user" uuid, + "timestamp" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + ip character varying(50), + user_agent text, + collection character varying(64) NOT NULL, + item character varying(255) NOT NULL, + origin character varying(255) +); + + +-- +-- Name: directus_activity_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.directus_activity_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: directus_activity_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.directus_activity_id_seq OWNED BY public.directus_activity.id; + + +-- +-- Name: directus_collections; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.directus_collections ( + collection character varying(64) NOT NULL, + icon character varying(64), + note text, + display_template character varying(255), + hidden boolean DEFAULT false NOT NULL, + singleton boolean DEFAULT false NOT NULL, + translations json, + archive_field character varying(64), + archive_app_filter boolean DEFAULT true NOT NULL, + archive_value character varying(255), + unarchive_value character varying(255), + sort_field character varying(64), + accountability character varying(255) DEFAULT 'all'::character varying, + color character varying(255), + item_duplication_fields json, + sort integer, + "group" character varying(64), + collapse character varying(255) DEFAULT 'open'::character varying NOT NULL, + preview_url character varying(255), + versioning boolean DEFAULT false NOT NULL +); + + +-- +-- Name: directus_comments; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.directus_comments ( + id uuid NOT NULL, + collection character varying(64) NOT NULL, + item character varying(255) NOT NULL, + comment text NOT NULL, + date_created timestamp with time zone DEFAULT CURRENT_TIMESTAMP, + date_updated timestamp with time zone DEFAULT CURRENT_TIMESTAMP, + user_created uuid, + user_updated uuid +); + + +-- +-- Name: directus_dashboards; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.directus_dashboards ( + id uuid NOT NULL, + name character varying(255) NOT NULL, + icon character varying(64) DEFAULT 'dashboard'::character varying NOT NULL, + note text, + date_created timestamp with time zone DEFAULT CURRENT_TIMESTAMP, + user_created uuid, + color character varying(255) +); + + +-- +-- Name: directus_extensions; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.directus_extensions ( + enabled boolean DEFAULT true NOT NULL, + id uuid NOT NULL, + folder character varying(255) NOT NULL, + source character varying(255) NOT NULL, + bundle uuid +); + + +-- +-- Name: directus_fields; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.directus_fields ( + id integer NOT NULL, + collection character varying(64) NOT NULL, + field character varying(64) NOT NULL, + special character varying(64), + interface character varying(64), + options json, + display character varying(64), + display_options json, + readonly boolean DEFAULT false NOT NULL, + hidden boolean DEFAULT false NOT NULL, + sort integer, + width character varying(30) DEFAULT 'full'::character varying, + translations json, + note text, + conditions json, + required boolean DEFAULT false, + "group" character varying(64), + validation json, + validation_message text, + searchable boolean DEFAULT true NOT NULL +); + + +-- +-- Name: directus_fields_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.directus_fields_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: directus_fields_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.directus_fields_id_seq OWNED BY public.directus_fields.id; + + +-- +-- Name: directus_files; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.directus_files ( + id uuid NOT NULL, + storage character varying(255) NOT NULL, + filename_disk character varying(255), + filename_download character varying(255) NOT NULL, + title character varying(255), + type character varying(255), + folder uuid, + uploaded_by uuid, + created_on timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + modified_by uuid, + modified_on timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + charset character varying(50), + filesize bigint, + width integer, + height integer, + duration integer, + embed character varying(200), + description text, + location text, + tags text, + metadata json, + focal_point_x integer, + focal_point_y integer, + tus_id character varying(64), + tus_data json, + uploaded_on timestamp with time zone +); + + +-- +-- Name: directus_flows; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.directus_flows ( + id uuid NOT NULL, + name character varying(255) NOT NULL, + icon character varying(64), + color character varying(255), + description text, + status character varying(255) DEFAULT 'active'::character varying NOT NULL, + trigger character varying(255), + accountability character varying(255) DEFAULT 'all'::character varying, + options json, + operation uuid, + date_created timestamp with time zone DEFAULT CURRENT_TIMESTAMP, + user_created uuid +); + + +-- +-- Name: directus_folders; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.directus_folders ( + id uuid NOT NULL, + name character varying(255) NOT NULL, + parent uuid +); + + +-- +-- Name: directus_migrations; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.directus_migrations ( + version character varying(255) NOT NULL, + name character varying(255) NOT NULL, + "timestamp" timestamp with time zone DEFAULT CURRENT_TIMESTAMP +); + + +-- +-- Name: directus_notifications; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.directus_notifications ( + id integer NOT NULL, + "timestamp" timestamp with time zone DEFAULT CURRENT_TIMESTAMP, + status character varying(255) DEFAULT 'inbox'::character varying, + recipient uuid NOT NULL, + sender uuid, + subject character varying(255) NOT NULL, + message text, + collection character varying(64), + item character varying(255) +); + + +-- +-- Name: directus_notifications_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.directus_notifications_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: directus_notifications_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.directus_notifications_id_seq OWNED BY public.directus_notifications.id; + + +-- +-- Name: directus_operations; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.directus_operations ( + id uuid NOT NULL, + name character varying(255), + key character varying(255) NOT NULL, + type character varying(255) NOT NULL, + position_x integer NOT NULL, + position_y integer NOT NULL, + options json, + resolve uuid, + reject uuid, + flow uuid NOT NULL, + date_created timestamp with time zone DEFAULT CURRENT_TIMESTAMP, + user_created uuid +); + + +-- +-- Name: directus_panels; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.directus_panels ( + id uuid NOT NULL, + dashboard uuid NOT NULL, + name character varying(255), + icon character varying(64) DEFAULT NULL::character varying, + color character varying(10), + show_header boolean DEFAULT false NOT NULL, + note text, + type character varying(255) NOT NULL, + position_x integer NOT NULL, + position_y integer NOT NULL, + width integer NOT NULL, + height integer NOT NULL, + options json, + date_created timestamp with time zone DEFAULT CURRENT_TIMESTAMP, + user_created uuid +); + + +-- +-- Name: directus_permissions; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.directus_permissions ( + id integer NOT NULL, + collection character varying(64) NOT NULL, + action character varying(10) NOT NULL, + permissions json, + validation json, + presets json, + fields text, + policy uuid NOT NULL +); + + +-- +-- Name: directus_permissions_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.directus_permissions_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: directus_permissions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.directus_permissions_id_seq OWNED BY public.directus_permissions.id; + + +-- +-- Name: directus_policies; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.directus_policies ( + id uuid NOT NULL, + name character varying(100) NOT NULL, + icon character varying(64) DEFAULT 'badge'::character varying NOT NULL, + description text, + ip_access text, + enforce_tfa boolean DEFAULT false NOT NULL, + admin_access boolean DEFAULT false NOT NULL, + app_access boolean DEFAULT false NOT NULL +); + + +-- +-- Name: directus_presets; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.directus_presets ( + id integer NOT NULL, + bookmark character varying(255), + "user" uuid, + role uuid, + collection character varying(64), + search character varying(100), + layout character varying(100) DEFAULT 'tabular'::character varying, + layout_query json, + layout_options json, + refresh_interval integer, + filter json, + icon character varying(64) DEFAULT 'bookmark'::character varying, + color character varying(255) +); + + +-- +-- Name: directus_presets_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.directus_presets_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: directus_presets_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.directus_presets_id_seq OWNED BY public.directus_presets.id; + + +-- +-- Name: directus_relations; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.directus_relations ( + id integer NOT NULL, + many_collection character varying(64) NOT NULL, + many_field character varying(64) NOT NULL, + one_collection character varying(64), + one_field character varying(64), + one_collection_field character varying(64), + one_allowed_collections text, + junction_field character varying(64), + sort_field character varying(64), + one_deselect_action character varying(255) DEFAULT 'nullify'::character varying NOT NULL +); + + +-- +-- Name: directus_relations_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.directus_relations_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: directus_relations_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.directus_relations_id_seq OWNED BY public.directus_relations.id; + + +-- +-- Name: directus_revisions; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.directus_revisions ( + id integer NOT NULL, + activity integer NOT NULL, + collection character varying(64) NOT NULL, + item character varying(255) NOT NULL, + data json, + delta json, + parent integer, + version uuid +); + + +-- +-- Name: directus_revisions_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.directus_revisions_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: directus_revisions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.directus_revisions_id_seq OWNED BY public.directus_revisions.id; + + +-- +-- Name: directus_roles; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.directus_roles ( + id uuid NOT NULL, + name character varying(100) NOT NULL, + icon character varying(64) DEFAULT 'supervised_user_circle'::character varying NOT NULL, + description text, + parent uuid +); + + +-- +-- Name: directus_sessions; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.directus_sessions ( + token character varying(64) NOT NULL, + "user" uuid, + expires timestamp with time zone NOT NULL, + ip character varying(255), + user_agent text, + share uuid, + origin character varying(255), + next_token character varying(64) +); + + +-- +-- Name: directus_settings; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.directus_settings ( + id integer NOT NULL, + project_name character varying(100) DEFAULT 'Directus'::character varying NOT NULL, + project_url character varying(255), + project_color character varying(255) DEFAULT '#6644FF'::character varying NOT NULL, + project_logo uuid, + public_foreground uuid, + public_background uuid, + public_note text, + auth_login_attempts integer DEFAULT 25, + auth_password_policy character varying(100), + storage_asset_transform character varying(7) DEFAULT 'all'::character varying, + storage_asset_presets json, + custom_css text, + storage_default_folder uuid, + basemaps json, + mapbox_key character varying(255), + module_bar json, + project_descriptor character varying(100), + default_language character varying(255) DEFAULT 'en-US'::character varying NOT NULL, + custom_aspect_ratios json, + public_favicon uuid, + default_appearance character varying(255) DEFAULT 'auto'::character varying NOT NULL, + default_theme_light character varying(255), + theme_light_overrides json, + default_theme_dark character varying(255), + theme_dark_overrides json, + report_error_url character varying(255), + report_bug_url character varying(255), + report_feature_url character varying(255), + public_registration boolean DEFAULT false NOT NULL, + public_registration_verify_email boolean DEFAULT true NOT NULL, + public_registration_role uuid, + public_registration_email_filter json, + visual_editor_urls json, + project_id uuid, + mcp_enabled boolean DEFAULT false NOT NULL, + mcp_allow_deletes boolean DEFAULT false NOT NULL, + mcp_prompts_collection character varying(255) DEFAULT NULL::character varying, + mcp_system_prompt_enabled boolean DEFAULT true NOT NULL, + mcp_system_prompt text, + project_owner character varying(255), + project_usage character varying(255), + org_name character varying(255), + product_updates boolean, + project_status character varying(255), + ai_openai_api_key text, + ai_anthropic_api_key text, + ai_system_prompt text +); + + +-- +-- Name: directus_settings_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.directus_settings_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: directus_settings_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.directus_settings_id_seq OWNED BY public.directus_settings.id; + + +-- +-- Name: directus_shares; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.directus_shares ( + id uuid NOT NULL, + name character varying(255), + collection character varying(64) NOT NULL, + item character varying(255) NOT NULL, + role uuid, + password character varying(255), + user_created uuid, + date_created timestamp with time zone DEFAULT CURRENT_TIMESTAMP, + date_start timestamp with time zone, + date_end timestamp with time zone, + times_used integer DEFAULT 0, + max_uses integer +); + + +-- +-- Name: directus_translations; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.directus_translations ( + id uuid NOT NULL, + language character varying(255) NOT NULL, + key character varying(255) NOT NULL, + value text NOT NULL +); + + +-- +-- Name: directus_users; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.directus_users ( + id uuid NOT NULL, + first_name character varying(50), + last_name character varying(50), + email character varying(128), + password character varying(255), + location character varying(255), + title character varying(50), + description text, + tags json, + avatar uuid, + language character varying(255) DEFAULT NULL::character varying, + tfa_secret character varying(255), + status character varying(16) DEFAULT 'active'::character varying NOT NULL, + role uuid, + token character varying(255), + last_access timestamp with time zone, + last_page character varying(255), + provider character varying(128) DEFAULT 'default'::character varying NOT NULL, + external_identifier character varying(255), + auth_data json, + email_notifications boolean DEFAULT true, + appearance character varying(255), + theme_dark character varying(255), + theme_light character varying(255), + theme_light_overrides json, + theme_dark_overrides json, + text_direction character varying(255) DEFAULT 'auto'::character varying NOT NULL +); + + +-- +-- Name: directus_versions; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.directus_versions ( + id uuid NOT NULL, + key character varying(64) NOT NULL, + name character varying(255), + collection character varying(64) NOT NULL, + item character varying(255) NOT NULL, + hash character varying(255), + date_created timestamp with time zone DEFAULT CURRENT_TIMESTAMP, + date_updated timestamp with time zone DEFAULT CURRENT_TIMESTAMP, + user_created uuid, + user_updated uuid, + delta json +); + + +-- +-- Name: contact_submissions id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.contact_submissions ALTER COLUMN id SET DEFAULT nextval('public.contact_submissions_id_seq'::regclass); + + +-- +-- Name: directus_activity id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_activity ALTER COLUMN id SET DEFAULT nextval('public.directus_activity_id_seq'::regclass); + + +-- +-- Name: directus_fields id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_fields ALTER COLUMN id SET DEFAULT nextval('public.directus_fields_id_seq'::regclass); + + +-- +-- Name: directus_notifications id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_notifications ALTER COLUMN id SET DEFAULT nextval('public.directus_notifications_id_seq'::regclass); + + +-- +-- Name: directus_permissions id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_permissions ALTER COLUMN id SET DEFAULT nextval('public.directus_permissions_id_seq'::regclass); + + +-- +-- Name: directus_presets id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_presets ALTER COLUMN id SET DEFAULT nextval('public.directus_presets_id_seq'::regclass); + + +-- +-- Name: directus_relations id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_relations ALTER COLUMN id SET DEFAULT nextval('public.directus_relations_id_seq'::regclass); + + +-- +-- Name: directus_revisions id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_revisions ALTER COLUMN id SET DEFAULT nextval('public.directus_revisions_id_seq'::regclass); + + +-- +-- Name: directus_settings id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_settings ALTER COLUMN id SET DEFAULT nextval('public.directus_settings_id_seq'::regclass); + + +-- +-- Data for Name: contact_submissions; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.contact_submissions (id, name, email, company, message, date_created) FROM stdin; +1 Test User test@example.com Test Company This is a test message from Antigravity to verify Directus integration. It must be at least 20 characters long. 2026-02-04 23:18:12.93+00 +2 Marc Mintel marc@mintel.me Mintel.me Hallo ich hab eine Frage! 2026-02-04 23:37:07.706+00 +3 Verification Test verify@mintel.me Nicht angegeben Testing resilient reporting and notifications. 2026-02-04 23:41:41.625+00 +4 Marc Mintel marc@mintel.me Mintel.me Hallo das ist ein Test 2026-02-04 23:46:51.035+00 +\. + + +-- +-- Data for Name: directus_access; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.directus_access (id, role, "user", policy, sort) FROM stdin; +5767af92-3332-4d77-9c0f-9ce4e26a6c79 \N \N abf8a154-5b1c-4a46-ac9c-7300570f4f17 1 +f477a1c6-0519-4aba-9daf-e6a2eb1236ac f243a35a-b244-4f26-a58b-5112a4af7513 \N 0509fa50-e980-4f3b-845a-f2bb0423f7fd \N +\. + + +-- +-- Data for Name: directus_activity; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.directus_activity (id, action, "user", "timestamp", ip, user_agent, collection, item, origin) FROM stdin; +1 login 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-04 23:18:02.581+00 162.159.140.98 node directus_users 031fc61d-1f42-4764-8ccf-98c5c854795a \N +2 update 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-04 23:18:03.06+00 162.159.140.98 node directus_settings 1 \N +3 create 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-04 23:18:03.08+00 162.159.140.98 node directus_fields 1 \N +4 create 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-04 23:18:03.082+00 162.159.140.98 node directus_collections contact_submissions \N +5 create 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-04 23:18:03.126+00 162.159.140.98 node directus_fields 2 \N +6 create 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-04 23:18:03.164+00 162.159.140.98 node directus_fields 3 \N +7 create 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-04 23:18:03.197+00 162.159.140.98 node directus_fields 4 \N +8 create 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-04 23:18:03.224+00 162.159.140.98 node directus_fields 5 \N +9 create 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-04 23:18:03.248+00 162.159.140.98 node directus_fields 6 \N +10 login 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-04 23:18:12.446+00 172.20.0.2 node directus_users 031fc61d-1f42-4764-8ccf-98c5c854795a \N +11 create 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-04 23:18:12.931+00 172.20.0.2 node contact_submissions 1 \N +12 login 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-04 23:18:25.749+00 162.159.140.98 node directus_users 031fc61d-1f42-4764-8ccf-98c5c854795a \N +13 login 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-04 23:37:07.237+00 172.20.0.2 node directus_users 031fc61d-1f42-4764-8ccf-98c5c854795a \N +14 create 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-04 23:37:07.707+00 172.20.0.2 node contact_submissions 2 \N +15 login 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-04 23:41:41.159+00 172.20.0.2 node directus_users 031fc61d-1f42-4764-8ccf-98c5c854795a \N +16 create 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-04 23:41:41.627+00 172.20.0.2 node contact_submissions 3 \N +17 login 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-04 23:41:58.84+00 162.159.140.98 node directus_users 031fc61d-1f42-4764-8ccf-98c5c854795a \N +18 login 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-04 23:42:07.904+00 162.159.140.98 node directus_users 031fc61d-1f42-4764-8ccf-98c5c854795a \N +19 login 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-04 23:46:50.557+00 172.20.0.2 node directus_users 031fc61d-1f42-4764-8ccf-98c5c854795a \N +20 create 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-04 23:46:51.036+00 172.20.0.2 node contact_submissions 4 \N +21 login 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-06 18:08:07.558+00 162.159.140.98 node directus_users 031fc61d-1f42-4764-8ccf-98c5c854795a \N +22 update 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-06 18:08:08.042+00 162.159.140.98 node directus_settings 1 \N +23 login 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-06 18:08:59.178+00 162.159.140.98 node directus_users 031fc61d-1f42-4764-8ccf-98c5c854795a \N +24 update 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-06 18:08:59.67+00 162.159.140.98 node directus_settings 1 \N +25 login 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-06 18:09:57.57+00 162.159.140.98 node directus_users 031fc61d-1f42-4764-8ccf-98c5c854795a \N +26 update 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-06 18:09:58.05+00 162.159.140.98 node directus_settings 1 \N +27 login 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-06 18:10:41.953+00 162.159.140.98 node directus_users 031fc61d-1f42-4764-8ccf-98c5c854795a \N +28 update 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-06 18:10:42.446+00 162.159.140.98 node directus_settings 1 \N +29 login 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-06 18:10:56.98+00 162.159.140.98 node directus_users 031fc61d-1f42-4764-8ccf-98c5c854795a \N +30 update 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-06 18:10:57.468+00 162.159.140.98 node directus_settings 1 \N +31 login 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-06 18:38:54.995+00 162.159.140.98 node directus_users 031fc61d-1f42-4764-8ccf-98c5c854795a \N +32 update 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-06 18:38:55.477+00 162.159.140.98 node directus_settings 1 \N +33 login 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-06 18:40:23.56+00 162.159.140.98 node directus_users 031fc61d-1f42-4764-8ccf-98c5c854795a \N +34 update 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-06 18:40:24.045+00 162.159.140.98 node directus_settings 1 \N +\. + + +-- +-- Data for Name: directus_collections; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.directus_collections (collection, icon, note, display_template, hidden, singleton, translations, archive_field, archive_app_filter, archive_value, unarchive_value, sort_field, accountability, color, item_duplication_fields, sort, "group", collapse, preview_url, versioning) FROM stdin; +contact_submissions contact_mail \N {{name}} <{{email}}> f f \N \N t \N \N \N all \N \N \N \N open \N f +\. + + +-- +-- Data for Name: directus_comments; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.directus_comments (id, collection, item, comment, date_created, date_updated, user_created, user_updated) FROM stdin; +\. + + +-- +-- Data for Name: directus_dashboards; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.directus_dashboards (id, name, icon, note, date_created, user_created, color) FROM stdin; +\. + + +-- +-- Data for Name: directus_extensions; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.directus_extensions (enabled, id, folder, source, bundle) FROM stdin; +\. + + +-- +-- Data for Name: directus_fields; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.directus_fields (id, collection, field, special, interface, options, display, display_options, readonly, hidden, sort, width, translations, note, conditions, required, "group", validation, validation_message, searchable) FROM stdin; +1 contact_submissions id \N numeric \N \N \N t t 1 full \N \N \N f \N \N \N t +2 contact_submissions name \N input \N \N \N f f 2 full \N \N \N f \N \N \N t +3 contact_submissions email \N input \N \N \N f f 3 full \N \N \N f \N \N \N t +4 contact_submissions company \N input \N \N \N f f 4 full \N \N \N f \N \N \N t +5 contact_submissions message \N textarea \N \N \N f f 5 full \N \N \N f \N \N \N t +6 contact_submissions date_created date-created datetime \N \N \N f f 6 full \N \N \N f \N \N \N t +\. + + +-- +-- Data for Name: directus_files; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.directus_files (id, storage, filename_disk, filename_download, title, type, folder, uploaded_by, created_on, modified_by, modified_on, charset, filesize, width, height, duration, embed, description, location, tags, metadata, focal_point_x, focal_point_y, tus_id, tus_data, uploaded_on) FROM stdin; +\. + + +-- +-- Data for Name: directus_flows; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.directus_flows (id, name, icon, color, description, status, trigger, accountability, options, operation, date_created, user_created) FROM stdin; +\. + + +-- +-- Data for Name: directus_folders; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.directus_folders (id, name, parent) FROM stdin; +\. + + +-- +-- Data for Name: directus_migrations; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.directus_migrations (version, name, "timestamp") FROM stdin; +20201028A Remove Collection Foreign Keys 2026-02-03 15:19:50.057902+00 +20201029A Remove System Relations 2026-02-03 15:19:50.062175+00 +20201029B Remove System Collections 2026-02-03 15:19:50.066773+00 +20201029C Remove System Fields 2026-02-03 15:19:50.071882+00 +20201105A Add Cascade System Relations 2026-02-03 15:19:50.094929+00 +20201105B Change Webhook URL Type 2026-02-03 15:19:50.099569+00 +20210225A Add Relations Sort Field 2026-02-03 15:19:50.102795+00 +20210304A Remove Locked Fields 2026-02-03 15:19:50.104504+00 +20210312A Webhooks Collections Text 2026-02-03 15:19:50.107669+00 +20210331A Add Refresh Interval 2026-02-03 15:19:50.109426+00 +20210415A Make Filesize Nullable 2026-02-03 15:19:50.113577+00 +20210416A Add Collections Accountability 2026-02-03 15:19:50.115937+00 +20210422A Remove Files Interface 2026-02-03 15:19:50.117197+00 +20210506A Rename Interfaces 2026-02-03 15:19:50.127166+00 +20210510A Restructure Relations 2026-02-03 15:19:50.133296+00 +20210518A Add Foreign Key Constraints 2026-02-03 15:19:50.137387+00 +20210519A Add System Fk Triggers 2026-02-03 15:19:50.152561+00 +20210521A Add Collections Icon Color 2026-02-03 15:19:50.154112+00 +20210525A Add Insights 2026-02-03 15:19:50.164687+00 +20210608A Add Deep Clone Config 2026-02-03 15:19:50.1664+00 +20210626A Change Filesize Bigint 2026-02-03 15:19:50.174562+00 +20210716A Add Conditions to Fields 2026-02-03 15:19:50.176557+00 +20210721A Add Default Folder 2026-02-03 15:19:50.180518+00 +20210802A Replace Groups 2026-02-03 15:19:50.18334+00 +20210803A Add Required to Fields 2026-02-03 15:19:50.184975+00 +20210805A Update Groups 2026-02-03 15:19:50.187237+00 +20210805B Change Image Metadata Structure 2026-02-03 15:19:50.189316+00 +20210811A Add Geometry Config 2026-02-03 15:19:50.19092+00 +20210831A Remove Limit Column 2026-02-03 15:19:50.192578+00 +20210903A Add Auth Provider 2026-02-03 15:19:50.20054+00 +20210907A Webhooks Collections Not Null 2026-02-03 15:19:50.204371+00 +20210910A Move Module Setup 2026-02-03 15:19:50.206614+00 +20210920A Webhooks URL Not Null 2026-02-03 15:19:50.210094+00 +20210924A Add Collection Organization 2026-02-03 15:19:50.213902+00 +20210927A Replace Fields Group 2026-02-03 15:19:50.218587+00 +20210927B Replace M2M Interface 2026-02-03 15:19:50.22008+00 +20210929A Rename Login Action 2026-02-03 15:19:50.221428+00 +20211007A Update Presets 2026-02-03 15:19:50.22467+00 +20211009A Add Auth Data 2026-02-03 15:19:50.226122+00 +20211016A Add Webhook Headers 2026-02-03 15:19:50.227954+00 +20211103A Set Unique to User Token 2026-02-03 15:19:50.230742+00 +20211103B Update Special Geometry 2026-02-03 15:19:50.232653+00 +20211104A Remove Collections Listing 2026-02-03 15:19:50.234636+00 +20211118A Add Notifications 2026-02-03 15:19:50.241983+00 +20211211A Add Shares 2026-02-03 15:19:50.251822+00 +20211230A Add Project Descriptor 2026-02-03 15:19:50.253575+00 +20220303A Remove Default Project Color 2026-02-03 15:19:50.257087+00 +20220308A Add Bookmark Icon and Color 2026-02-03 15:19:50.258684+00 +20220314A Add Translation Strings 2026-02-03 15:19:50.260264+00 +20220322A Rename Field Typecast Flags 2026-02-03 15:19:50.262683+00 +20220323A Add Field Validation 2026-02-03 15:19:50.26419+00 +20220325A Fix Typecast Flags 2026-02-03 15:19:50.266435+00 +20220325B Add Default Language 2026-02-03 15:19:50.271036+00 +20220402A Remove Default Value Panel Icon 2026-02-03 15:19:50.274391+00 +20220429A Add Flows 2026-02-03 15:19:50.291528+00 +20220429B Add Color to Insights Icon 2026-02-03 15:19:50.293165+00 +20220429C Drop Non Null From IP of Activity 2026-02-03 15:19:50.294566+00 +20220429D Drop Non Null From Sender of Notifications 2026-02-03 15:19:50.295834+00 +20220614A Rename Hook Trigger to Event 2026-02-03 15:19:50.29696+00 +20220801A Update Notifications Timestamp Column 2026-02-03 15:19:50.300239+00 +20220802A Add Custom Aspect Ratios 2026-02-03 15:19:50.301693+00 +20220826A Add Origin to Accountability 2026-02-03 15:19:50.303682+00 +20230401A Update Material Icons 2026-02-03 15:19:50.307474+00 +20230525A Add Preview Settings 2026-02-03 15:19:50.308721+00 +20230526A Migrate Translation Strings 2026-02-03 15:19:50.313754+00 +20230721A Require Shares Fields 2026-02-03 15:19:50.316526+00 +20230823A Add Content Versioning 2026-02-03 15:19:50.32597+00 +20230927A Themes 2026-02-03 15:19:50.33375+00 +20231009A Update CSV Fields to Text 2026-02-03 15:19:50.336674+00 +20231009B Update Panel Options 2026-02-03 15:19:50.337981+00 +20231010A Add Extensions 2026-02-03 15:19:50.340444+00 +20231215A Add Focalpoints 2026-02-03 15:19:50.341917+00 +20240122A Add Report URL Fields 2026-02-03 15:19:50.343434+00 +20240204A Marketplace 2026-02-03 15:19:50.354984+00 +20240305A Change Useragent Type 2026-02-03 15:19:50.359715+00 +20240311A Deprecate Webhooks 2026-02-03 15:19:50.364218+00 +20240422A Public Registration 2026-02-03 15:19:50.367066+00 +20240515A Add Session Window 2026-02-03 15:19:50.368511+00 +20240701A Add Tus Data 2026-02-03 15:19:50.370107+00 +20240716A Update Files Date Fields 2026-02-03 15:19:50.373558+00 +20240806A Permissions Policies 2026-02-03 15:19:50.397209+00 +20240817A Update Icon Fields Length 2026-02-03 15:19:50.409773+00 +20240909A Separate Comments 2026-02-03 15:19:50.416321+00 +20240909B Consolidate Content Versioning 2026-02-03 15:19:50.418546+00 +20240924A Migrate Legacy Comments 2026-02-03 15:19:50.421353+00 +20240924B Populate Versioning Deltas 2026-02-03 15:19:50.423741+00 +20250224A Visual Editor 2026-02-03 15:19:50.425749+00 +20250609A License Banner 2026-02-03 15:19:50.428481+00 +20250613A Add Project ID 2026-02-03 15:19:50.439812+00 +20250718A Add Direction 2026-02-03 15:19:50.44164+00 +20250813A Add MCP 2026-02-03 15:19:50.443844+00 +20251012A Add Field Searchable 2026-02-03 15:19:50.44587+00 +20251014A Add Project Owner 2026-02-03 15:19:50.470558+00 +20251028A Add Retention Indexes 2026-02-03 15:19:50.497896+00 +20251103A Add AI Settings 2026-02-03 15:19:50.499838+00 +20251224A Remove Webhooks 2026-02-03 15:19:50.50306+00 +20260113A Add Revisions Index 2026-02-03 15:19:50.513245+00 +\. + + +-- +-- Data for Name: directus_notifications; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.directus_notifications (id, "timestamp", status, recipient, sender, subject, message, collection, item) FROM stdin; +\. + + +-- +-- Data for Name: directus_operations; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.directus_operations (id, name, key, type, position_x, position_y, options, resolve, reject, flow, date_created, user_created) FROM stdin; +\. + + +-- +-- Data for Name: directus_panels; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.directus_panels (id, dashboard, name, icon, color, show_header, note, type, position_x, position_y, width, height, options, date_created, user_created) FROM stdin; +\. + + +-- +-- Data for Name: directus_permissions; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.directus_permissions (id, collection, action, permissions, validation, presets, fields, policy) FROM stdin; +\. + + +-- +-- Data for Name: directus_policies; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.directus_policies (id, name, icon, description, ip_access, enforce_tfa, admin_access, app_access) FROM stdin; +abf8a154-5b1c-4a46-ac9c-7300570f4f17 $t:public_label public $t:public_description \N f f f +0509fa50-e980-4f3b-845a-f2bb0423f7fd Administrator verified $t:admin_description \N f t t +\. + + +-- +-- Data for Name: directus_presets; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.directus_presets (id, bookmark, "user", role, collection, search, layout, layout_query, layout_options, refresh_interval, filter, icon, color) FROM stdin; +\. + + +-- +-- Data for Name: directus_relations; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.directus_relations (id, many_collection, many_field, one_collection, one_field, one_collection_field, one_allowed_collections, junction_field, sort_field, one_deselect_action) FROM stdin; +\. + + +-- +-- Data for Name: directus_revisions; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.directus_revisions (id, activity, collection, item, data, delta, parent, version) FROM stdin; +1 2 directus_settings 1 {"id":1,"project_name":"mb-grid-solutions","project_url":null,"project_color":"#82ed20","project_logo":null,"public_foreground":null,"public_background":null,"public_note":"\\n \\n
\\n

MINTEL INFRASTRUCTURE ENGINE

\\n

MB-GRID-SOLUTIONS RELIABILITY.

\\n
\\n ","auth_login_attempts":25,"auth_password_policy":null,"storage_asset_transform":"all","storage_asset_presets":null,"custom_css":null,"storage_default_folder":null,"basemaps":null,"mapbox_key":null,"module_bar":null,"project_descriptor":null,"default_language":"en-US","custom_aspect_ratios":null,"public_favicon":null,"default_appearance":"auto","default_theme_light":null,"theme_light_overrides":{"primary":"#82ed20","borderRadius":"16px","navigationBackground":"#000c24","navigationForeground":"#ffffff"},"default_theme_dark":null,"theme_dark_overrides":null,"report_error_url":null,"report_bug_url":null,"report_feature_url":null,"public_registration":false,"public_registration_verify_email":true,"public_registration_role":null,"public_registration_email_filter":null,"visual_editor_urls":null,"project_id":"019c2416-a7a6-76cd-9312-5d7815f23b29","mcp_enabled":false,"mcp_allow_deletes":false,"mcp_prompts_collection":null,"mcp_system_prompt_enabled":true,"mcp_system_prompt":null,"project_owner":null,"project_usage":null,"org_name":null,"product_updates":null,"project_status":null,"ai_openai_api_key":null,"ai_anthropic_api_key":null,"ai_system_prompt":null} {"project_name":"mb-grid-solutions","project_color":"#82ed20","public_note":"\\n \\n
\\n

MINTEL INFRASTRUCTURE ENGINE

\\n

MB-GRID-SOLUTIONS RELIABILITY.

\\n
\\n ","theme_light_overrides":{"primary":"#82ed20","borderRadius":"16px","navigationBackground":"#000c24","navigationForeground":"#ffffff"}} \N \N +2 3 directus_fields 1 {"sort":1,"hidden":true,"interface":"numeric","readonly":true,"field":"id","collection":"contact_submissions"} {"sort":1,"hidden":true,"interface":"numeric","readonly":true,"field":"id","collection":"contact_submissions"} \N \N +3 4 directus_collections contact_submissions {"icon":"contact_mail","display_template":"{{name}} <{{email}}>","collection":"contact_submissions"} {"icon":"contact_mail","display_template":"{{name}} <{{email}}>","collection":"contact_submissions"} \N \N +4 5 directus_fields 2 {"sort":2,"interface":"input","collection":"contact_submissions","field":"name"} {"sort":2,"interface":"input","collection":"contact_submissions","field":"name"} \N \N +5 6 directus_fields 3 {"sort":3,"interface":"input","collection":"contact_submissions","field":"email"} {"sort":3,"interface":"input","collection":"contact_submissions","field":"email"} \N \N +6 7 directus_fields 4 {"sort":4,"interface":"input","collection":"contact_submissions","field":"company"} {"sort":4,"interface":"input","collection":"contact_submissions","field":"company"} \N \N +7 8 directus_fields 5 {"sort":5,"interface":"textarea","collection":"contact_submissions","field":"message"} {"sort":5,"interface":"textarea","collection":"contact_submissions","field":"message"} \N \N +8 9 directus_fields 6 {"sort":6,"interface":"datetime","special":["date-created"],"collection":"contact_submissions","field":"date_created"} {"sort":6,"interface":"datetime","special":["date-created"],"collection":"contact_submissions","field":"date_created"} \N \N +9 11 contact_submissions 1 {"name":"Test User","email":"test@example.com","company":"Test Company","message":"This is a test message from Antigravity to verify Directus integration. It must be at least 20 characters long."} {"name":"Test User","email":"test@example.com","company":"Test Company","message":"This is a test message from Antigravity to verify Directus integration. It must be at least 20 characters long."} \N \N +10 14 contact_submissions 2 {"name":"Marc Mintel","email":"marc@mintel.me","company":"Mintel.me","message":"Hallo ich hab eine Frage!"} {"name":"Marc Mintel","email":"marc@mintel.me","company":"Mintel.me","message":"Hallo ich hab eine Frage!"} \N \N +11 16 contact_submissions 3 {"name":"Verification Test","email":"verify@mintel.me","company":"Nicht angegeben","message":"Testing resilient reporting and notifications."} {"name":"Verification Test","email":"verify@mintel.me","company":"Nicht angegeben","message":"Testing resilient reporting and notifications."} \N \N +12 20 contact_submissions 4 {"name":"Marc Mintel","email":"marc@mintel.me","company":"Mintel.me","message":"Hallo das ist ein Test"} {"name":"Marc Mintel","email":"marc@mintel.me","company":"Mintel.me","message":"Hallo das ist ein Test"} \N \N +13 22 directus_settings 1 {"id":1,"project_name":"mb-grid-solutions","project_url":null,"project_color":"#82ed20","project_logo":null,"public_foreground":null,"public_background":null,"public_note":"\\n \\n
\\n

MINTEL INFRASTRUCTURE ENGINE

\\n

MB-GRID-SOLUTIONS RELIABILITY.

\\n
\\n ","auth_login_attempts":25,"auth_password_policy":null,"storage_asset_transform":"all","storage_asset_presets":null,"custom_css":null,"storage_default_folder":null,"basemaps":null,"mapbox_key":null,"module_bar":null,"project_descriptor":null,"default_language":"en-US","custom_aspect_ratios":null,"public_favicon":null,"default_appearance":"auto","default_theme_light":null,"theme_light_overrides":{"primary":"#82ed20","borderRadius":"16px","navigationBackground":"#000c24","navigationForeground":"#ffffff"},"default_theme_dark":null,"theme_dark_overrides":null,"report_error_url":null,"report_bug_url":null,"report_feature_url":null,"public_registration":false,"public_registration_verify_email":true,"public_registration_role":null,"public_registration_email_filter":null,"visual_editor_urls":null,"project_id":"019c2416-a7a6-76cd-9312-5d7815f23b29","mcp_enabled":false,"mcp_allow_deletes":false,"mcp_prompts_collection":null,"mcp_system_prompt_enabled":true,"mcp_system_prompt":null,"project_owner":null,"project_usage":null,"org_name":null,"product_updates":null,"project_status":null,"ai_openai_api_key":null,"ai_anthropic_api_key":null,"ai_system_prompt":null} {"project_name":"mb-grid-solutions","project_color":"#82ed20","public_note":"\\n \\n
\\n

MINTEL INFRASTRUCTURE ENGINE

\\n

MB-GRID-SOLUTIONS RELIABILITY.

\\n
\\n ","theme_light_overrides":{"primary":"#82ed20","borderRadius":"16px","navigationBackground":"#000c24","navigationForeground":"#ffffff"}} \N \N +14 24 directus_settings 1 {"id":1,"project_name":"mb-grid-solutions","project_url":null,"project_color":"#82ed20","project_logo":null,"public_foreground":null,"public_background":null,"public_note":"\\n \\n
\\n

Mintel Infrastructure Engine

\\n

MB-GRID-SOLUTIONS SYNC.

\\n
\\n ","auth_login_attempts":25,"auth_password_policy":null,"storage_asset_transform":"all","storage_asset_presets":null,"custom_css":null,"storage_default_folder":null,"basemaps":null,"mapbox_key":null,"module_bar":null,"project_descriptor":null,"default_language":"en-US","custom_aspect_ratios":null,"public_favicon":null,"default_appearance":"auto","default_theme_light":null,"theme_light_overrides":{"primary":"#82ed20","borderRadius":"12px","navigationBackground":"#000c24","navigationForeground":"#ffffff","moduleBarBackground":"#00081a"},"default_theme_dark":null,"theme_dark_overrides":null,"report_error_url":null,"report_bug_url":null,"report_feature_url":null,"public_registration":false,"public_registration_verify_email":true,"public_registration_role":null,"public_registration_email_filter":null,"visual_editor_urls":null,"project_id":"019c2416-a7a6-76cd-9312-5d7815f23b29","mcp_enabled":false,"mcp_allow_deletes":false,"mcp_prompts_collection":null,"mcp_system_prompt_enabled":true,"mcp_system_prompt":null,"project_owner":null,"project_usage":null,"org_name":null,"product_updates":null,"project_status":null,"ai_openai_api_key":null,"ai_anthropic_api_key":null,"ai_system_prompt":null} {"project_name":"mb-grid-solutions","project_color":"#82ed20","public_note":"\\n \\n
\\n

Mintel Infrastructure Engine

\\n

MB-GRID-SOLUTIONS SYNC.

\\n
\\n ","theme_light_overrides":{"primary":"#82ed20","borderRadius":"12px","navigationBackground":"#000c24","navigationForeground":"#ffffff","moduleBarBackground":"#00081a"}} \N \N +15 26 directus_settings 1 {"id":1,"project_name":"mb-grid-solutions","project_url":null,"project_color":"#82ed20","project_logo":null,"public_foreground":null,"public_background":null,"public_note":"\\n \\n
\\n

Mintel Infrastructure Engine

\\n

MB-GRID-SOLUTIONS SYNC.

\\n
\\n ","auth_login_attempts":25,"auth_password_policy":null,"storage_asset_transform":"all","storage_asset_presets":null,"custom_css":null,"storage_default_folder":null,"basemaps":null,"mapbox_key":null,"module_bar":null,"project_descriptor":null,"default_language":"en-US","custom_aspect_ratios":null,"public_favicon":null,"default_appearance":"auto","default_theme_light":null,"theme_light_overrides":{"primary":"#82ed20","borderRadius":"12px","navigationBackground":"#000c24","navigationForeground":"#ffffff","moduleBarBackground":"#00081a"},"default_theme_dark":null,"theme_dark_overrides":null,"report_error_url":null,"report_bug_url":null,"report_feature_url":null,"public_registration":false,"public_registration_verify_email":true,"public_registration_role":null,"public_registration_email_filter":null,"visual_editor_urls":null,"project_id":"019c2416-a7a6-76cd-9312-5d7815f23b29","mcp_enabled":false,"mcp_allow_deletes":false,"mcp_prompts_collection":null,"mcp_system_prompt_enabled":true,"mcp_system_prompt":null,"project_owner":null,"project_usage":null,"org_name":null,"product_updates":null,"project_status":null,"ai_openai_api_key":null,"ai_anthropic_api_key":null,"ai_system_prompt":null} {"project_name":"mb-grid-solutions","project_color":"#82ed20","public_note":"\\n \\n
\\n

Mintel Infrastructure Engine

\\n

MB-GRID-SOLUTIONS SYNC.

\\n
\\n ","theme_light_overrides":{"primary":"#82ed20","borderRadius":"12px","navigationBackground":"#000c24","navigationForeground":"#ffffff","moduleBarBackground":"#00081a"}} \N \N +16 28 directus_settings 1 {"id":1,"project_name":"mb-grid-solutions","project_url":null,"project_color":"#82ed20","project_logo":null,"public_foreground":null,"public_background":null,"public_note":"\\n \\n
\\n

Mintel Infrastructure Engine

\\n

MB-GRID-SOLUTIONS SYNC.

\\n
\\n ","auth_login_attempts":25,"auth_password_policy":null,"storage_asset_transform":"all","storage_asset_presets":null,"custom_css":null,"storage_default_folder":null,"basemaps":null,"mapbox_key":null,"module_bar":null,"project_descriptor":null,"default_language":"en-US","custom_aspect_ratios":null,"public_favicon":null,"default_appearance":"auto","default_theme_light":null,"theme_light_overrides":{"primary":"#82ed20","borderRadius":"12px","navigationBackground":"#000c24","navigationForeground":"#ffffff","moduleBarBackground":"#00081a"},"default_theme_dark":null,"theme_dark_overrides":null,"report_error_url":null,"report_bug_url":null,"report_feature_url":null,"public_registration":false,"public_registration_verify_email":true,"public_registration_role":null,"public_registration_email_filter":null,"visual_editor_urls":null,"project_id":"019c2416-a7a6-76cd-9312-5d7815f23b29","mcp_enabled":false,"mcp_allow_deletes":false,"mcp_prompts_collection":null,"mcp_system_prompt_enabled":true,"mcp_system_prompt":null,"project_owner":null,"project_usage":null,"org_name":null,"product_updates":null,"project_status":null,"ai_openai_api_key":null,"ai_anthropic_api_key":null,"ai_system_prompt":null} {"project_name":"mb-grid-solutions","project_color":"#82ed20","public_note":"\\n \\n
\\n

Mintel Infrastructure Engine

\\n

MB-GRID-SOLUTIONS SYNC.

\\n
\\n ","theme_light_overrides":{"primary":"#82ed20","borderRadius":"12px","navigationBackground":"#000c24","navigationForeground":"#ffffff","moduleBarBackground":"#00081a"}} \N \N +17 30 directus_settings 1 {"id":1,"project_name":"mb-grid-solutions","project_url":null,"project_color":"#82ed20","project_logo":null,"public_foreground":null,"public_background":null,"public_note":"\\n \\n
\\n

Mintel Infrastructure Engine

\\n

MB-GRID-SOLUTIONS SYNC.

\\n
\\n ","auth_login_attempts":25,"auth_password_policy":null,"storage_asset_transform":"all","storage_asset_presets":null,"custom_css":null,"storage_default_folder":null,"basemaps":null,"mapbox_key":null,"module_bar":null,"project_descriptor":null,"default_language":"en-US","custom_aspect_ratios":null,"public_favicon":null,"default_appearance":"auto","default_theme_light":null,"theme_light_overrides":{"primary":"#82ed20","borderRadius":"12px","navigationBackground":"#000c24","navigationForeground":"#ffffff","moduleBarBackground":"#00081a"},"default_theme_dark":null,"theme_dark_overrides":null,"report_error_url":null,"report_bug_url":null,"report_feature_url":null,"public_registration":false,"public_registration_verify_email":true,"public_registration_role":null,"public_registration_email_filter":null,"visual_editor_urls":null,"project_id":"019c2416-a7a6-76cd-9312-5d7815f23b29","mcp_enabled":false,"mcp_allow_deletes":false,"mcp_prompts_collection":null,"mcp_system_prompt_enabled":true,"mcp_system_prompt":null,"project_owner":null,"project_usage":null,"org_name":null,"product_updates":null,"project_status":null,"ai_openai_api_key":null,"ai_anthropic_api_key":null,"ai_system_prompt":null} {"project_name":"mb-grid-solutions","project_color":"#82ed20","public_note":"\\n \\n
\\n

Mintel Infrastructure Engine

\\n

MB-GRID-SOLUTIONS SYNC.

\\n
\\n ","theme_light_overrides":{"primary":"#82ed20","borderRadius":"12px","navigationBackground":"#000c24","navigationForeground":"#ffffff","moduleBarBackground":"#00081a"}} \N \N +18 32 directus_settings 1 {"id":1,"project_name":"mb-grid-solutions","project_url":null,"project_color":"#82ed20","project_logo":null,"public_foreground":null,"public_background":null,"public_note":"\\n \\n
\\n

Mintel Infrastructure Engine

\\n

MB-GRID-SOLUTIONS SYNC.

\\n
\\n ","auth_login_attempts":25,"auth_password_policy":null,"storage_asset_transform":"all","storage_asset_presets":null,"custom_css":null,"storage_default_folder":null,"basemaps":null,"mapbox_key":null,"module_bar":null,"project_descriptor":null,"default_language":"en-US","custom_aspect_ratios":null,"public_favicon":null,"default_appearance":"auto","default_theme_light":null,"theme_light_overrides":{"primary":"#82ed20","borderRadius":"12px","navigationBackground":"#000c24","navigationForeground":"#ffffff","moduleBarBackground":"#00081a"},"default_theme_dark":null,"theme_dark_overrides":null,"report_error_url":null,"report_bug_url":null,"report_feature_url":null,"public_registration":false,"public_registration_verify_email":true,"public_registration_role":null,"public_registration_email_filter":null,"visual_editor_urls":null,"project_id":"019c2416-a7a6-76cd-9312-5d7815f23b29","mcp_enabled":false,"mcp_allow_deletes":false,"mcp_prompts_collection":null,"mcp_system_prompt_enabled":true,"mcp_system_prompt":null,"project_owner":null,"project_usage":null,"org_name":null,"product_updates":null,"project_status":null,"ai_openai_api_key":null,"ai_anthropic_api_key":null,"ai_system_prompt":null} {"project_name":"mb-grid-solutions","project_color":"#82ed20","public_note":"\\n \\n
\\n

Mintel Infrastructure Engine

\\n

MB-GRID-SOLUTIONS SYNC.

\\n
\\n ","theme_light_overrides":{"primary":"#82ed20","borderRadius":"12px","navigationBackground":"#000c24","navigationForeground":"#ffffff","moduleBarBackground":"#00081a"}} \N \N +19 34 directus_settings 1 {"id":1,"project_name":"mb-grid-solutions","project_url":null,"project_color":"#82ed20","project_logo":null,"public_foreground":null,"public_background":null,"public_note":"\\n \\n
\\n

Mintel Infrastructure Engine

\\n

MB-GRID-SOLUTIONS SYNC.

\\n
\\n ","auth_login_attempts":25,"auth_password_policy":null,"storage_asset_transform":"all","storage_asset_presets":null,"custom_css":null,"storage_default_folder":null,"basemaps":null,"mapbox_key":null,"module_bar":null,"project_descriptor":null,"default_language":"en-US","custom_aspect_ratios":null,"public_favicon":null,"default_appearance":"auto","default_theme_light":null,"theme_light_overrides":{"primary":"#82ed20","borderRadius":"12px","navigationBackground":"#000c24","navigationForeground":"#ffffff","moduleBarBackground":"#00081a"},"default_theme_dark":null,"theme_dark_overrides":null,"report_error_url":null,"report_bug_url":null,"report_feature_url":null,"public_registration":false,"public_registration_verify_email":true,"public_registration_role":null,"public_registration_email_filter":null,"visual_editor_urls":null,"project_id":"019c2416-a7a6-76cd-9312-5d7815f23b29","mcp_enabled":false,"mcp_allow_deletes":false,"mcp_prompts_collection":null,"mcp_system_prompt_enabled":true,"mcp_system_prompt":null,"project_owner":null,"project_usage":null,"org_name":null,"product_updates":null,"project_status":null,"ai_openai_api_key":null,"ai_anthropic_api_key":null,"ai_system_prompt":null} {"project_name":"mb-grid-solutions","project_color":"#82ed20","public_note":"\\n \\n
\\n

Mintel Infrastructure Engine

\\n

MB-GRID-SOLUTIONS SYNC.

\\n
\\n ","theme_light_overrides":{"primary":"#82ed20","borderRadius":"12px","navigationBackground":"#000c24","navigationForeground":"#ffffff","moduleBarBackground":"#00081a"}} \N \N +\. + + +-- +-- Data for Name: directus_roles; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.directus_roles (id, name, icon, description, parent) FROM stdin; +f243a35a-b244-4f26-a58b-5112a4af7513 Administrator verified $t:admin_description \N +\. + + +-- +-- Data for Name: directus_sessions; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.directus_sessions (token, "user", expires, ip, user_agent, share, origin, next_token) FROM stdin; +57UnludpW9LsiWWViXLoRaXz_Laq7hNz0LLDQNOGNBPxIl80SUfQ1fkjftlG90Cj 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-11 23:18:02.574+00 162.159.140.98 node \N \N \N +7tzRKqyYZGdtux5ax5hnzxo2oetUxZh39K8MMWtW5MZEQhKMV9YAZmJQJqYpigQy 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-11 23:18:12.443+00 172.20.0.2 node \N \N \N +DdmGqpFiANdmgxU5FofBsByK5Cmd189ptXnGkRMuaQFORTsPolbAO-wdWNCEo0Eu 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-11 23:18:25.747+00 162.159.140.98 node \N \N \N +iLhlWDwYnwZMJanRstUZa7DPbcChVDHF1ymWzqv8R6z965aTbatMatup4LFdx5sB 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-11 23:37:07.234+00 172.20.0.2 node \N \N \N +H_9ZhaUA_7-l_X6di8hH3Dol5xyQbNugyumEYSYmXr2bQjPeH0ssD-Vwr1BWDQi_ 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-11 23:41:41.156+00 172.20.0.2 node \N \N \N +3AQHxW1RXrLzLqsDfseTChmRucKkZ4dH4EzDskBaFadpJMbgHxteW4Bpj3LUvVRZ 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-11 23:41:58.839+00 162.159.140.98 node \N \N \N +51REEQM1RHh2ep_dmRbp4FpPRX6IMzH19WuPHUc955sy-v0qQQuzPXTAxSVp6agh 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-11 23:42:07.903+00 162.159.140.98 node \N \N \N +jbU5yCsAJZEO_EHNZFVQAxnenN_UOHWPHleNZ2N60adFNCfTPNCK_7Qrwh6fKXRI 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-11 23:46:50.556+00 172.20.0.2 node \N \N \N +Xjsj_hDXPq1teXoPo6gxe7s3_Aq_Esbu4OpzfOx2yMzjjk-6KK8BsNiFtaj_Wror 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-13 18:08:07.552+00 162.159.140.98 node \N \N \N +u09wlYPPN0ovRvaAZCPohso3AjmEGGD0JH650Zs2yaPWiYt1MEYxco2MaufvSf0k 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-13 18:08:59.176+00 162.159.140.98 node \N \N \N +geYblKY9XMEpn51JUgS3rsiASZqongfEAG46ryWyTH-ixrz7_sRmW4H_4N94D_so 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-13 18:09:57.567+00 162.159.140.98 node \N \N \N +6vb1Z1GZkoBpTXFyOEaZ7G9h9IH74QlpvnWhKDhjT_NDcgJhmsZ0UviGJDtACO-A 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-13 18:10:41.951+00 162.159.140.98 node \N \N \N +MGakUOCvCQNVkgsBLB5XyaiN4S8NAr8qGjuMuSb54l4dw07DZ3mY6KDL-OgN43ZO 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-13 18:10:56.977+00 162.159.140.98 node \N \N \N +UUXiLZ90KltiqnJHgwTYvGj7cO5lAQt1V59F7UcrUbmBjEUcZERVsLqe6qrjlUZs 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-13 18:38:54.991+00 162.159.140.98 node \N \N \N +vq3j02FC70aFPYy6PkKSlAhtFcWiupYkELuUsl6Z8lB6029iiPZLUzM05Uyn-I6u 031fc61d-1f42-4764-8ccf-98c5c854795a 2026-02-13 18:40:23.558+00 162.159.140.98 node \N \N \N +\. + + +-- +-- Data for Name: directus_settings; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.directus_settings (id, project_name, project_url, project_color, project_logo, public_foreground, public_background, public_note, auth_login_attempts, auth_password_policy, storage_asset_transform, storage_asset_presets, custom_css, storage_default_folder, basemaps, mapbox_key, module_bar, project_descriptor, default_language, custom_aspect_ratios, public_favicon, default_appearance, default_theme_light, theme_light_overrides, default_theme_dark, theme_dark_overrides, report_error_url, report_bug_url, report_feature_url, public_registration, public_registration_verify_email, public_registration_role, public_registration_email_filter, visual_editor_urls, project_id, mcp_enabled, mcp_allow_deletes, mcp_prompts_collection, mcp_system_prompt_enabled, mcp_system_prompt, project_owner, project_usage, org_name, product_updates, project_status, ai_openai_api_key, ai_anthropic_api_key, ai_system_prompt) FROM stdin; +1 mb-grid-solutions \N #82ed20 \N \N \N \n \n
\n

Mintel Infrastructure Engine

\n

MB-GRID-SOLUTIONS SYNC.

\n
\n 25 \N all \N \N \N \N \N \N \N en-US \N \N auto \N {"primary":"#82ed20","borderRadius":"12px","navigationBackground":"#000c24","navigationForeground":"#ffffff","moduleBarBackground":"#00081a"} \N \N \N \N \N f t \N \N \N 019c2416-a7a6-76cd-9312-5d7815f23b29 f f \N t \N \N \N \N \N \N \N \N \N +\. + + +-- +-- Data for Name: directus_shares; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.directus_shares (id, name, collection, item, role, password, user_created, date_created, date_start, date_end, times_used, max_uses) FROM stdin; +\. + + +-- +-- Data for Name: directus_translations; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.directus_translations (id, language, key, value) FROM stdin; +\. + + +-- +-- Data for Name: directus_users; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.directus_users (id, first_name, last_name, email, password, location, title, description, tags, avatar, language, tfa_secret, status, role, token, last_access, last_page, provider, external_identifier, auth_data, email_notifications, appearance, theme_dark, theme_light, theme_light_overrides, theme_dark_overrides, text_direction) FROM stdin; +031fc61d-1f42-4764-8ccf-98c5c854795a Admin User marc@mintel.me $argon2id$v=19$m=65536,t=3,p=4$FJh7vRdJeK3yk/OvJGb48A$l54FNJE53uYz8uyDIDgxQHrRM+MTKKg6q66k1dTOz7E \N \N \N \N \N \N \N active f243a35a-b244-4f26-a58b-5112a4af7513 \N 2026-02-06 18:40:23.56+00 \N default \N \N t \N \N \N \N \N auto +\. + + +-- +-- Data for Name: directus_versions; Type: TABLE DATA; Schema: public; Owner: - +-- + +COPY public.directus_versions (id, key, name, collection, item, hash, date_created, date_updated, user_created, user_updated, delta) FROM stdin; +\. + + +-- +-- Name: contact_submissions_id_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.contact_submissions_id_seq', 4, true); + + +-- +-- Name: directus_activity_id_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.directus_activity_id_seq', 34, true); + + +-- +-- Name: directus_fields_id_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.directus_fields_id_seq', 6, true); + + +-- +-- Name: directus_notifications_id_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.directus_notifications_id_seq', 1, false); + + +-- +-- Name: directus_permissions_id_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.directus_permissions_id_seq', 1, false); + + +-- +-- Name: directus_presets_id_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.directus_presets_id_seq', 1, false); + + +-- +-- Name: directus_relations_id_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.directus_relations_id_seq', 1, false); + + +-- +-- Name: directus_revisions_id_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.directus_revisions_id_seq', 19, true); + + +-- +-- Name: directus_settings_id_seq; Type: SEQUENCE SET; Schema: public; Owner: - +-- + +SELECT pg_catalog.setval('public.directus_settings_id_seq', 1, true); + + +-- +-- Name: contact_submissions contact_submissions_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.contact_submissions + ADD CONSTRAINT contact_submissions_pkey PRIMARY KEY (id); + + +-- +-- Name: directus_access directus_access_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_access + ADD CONSTRAINT directus_access_pkey PRIMARY KEY (id); + + +-- +-- Name: directus_activity directus_activity_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_activity + ADD CONSTRAINT directus_activity_pkey PRIMARY KEY (id); + + +-- +-- Name: directus_collections directus_collections_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_collections + ADD CONSTRAINT directus_collections_pkey PRIMARY KEY (collection); + + +-- +-- Name: directus_comments directus_comments_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_comments + ADD CONSTRAINT directus_comments_pkey PRIMARY KEY (id); + + +-- +-- Name: directus_dashboards directus_dashboards_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_dashboards + ADD CONSTRAINT directus_dashboards_pkey PRIMARY KEY (id); + + +-- +-- Name: directus_extensions directus_extensions_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_extensions + ADD CONSTRAINT directus_extensions_pkey PRIMARY KEY (id); + + +-- +-- Name: directus_fields directus_fields_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_fields + ADD CONSTRAINT directus_fields_pkey PRIMARY KEY (id); + + +-- +-- Name: directus_files directus_files_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_files + ADD CONSTRAINT directus_files_pkey PRIMARY KEY (id); + + +-- +-- Name: directus_flows directus_flows_operation_unique; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_flows + ADD CONSTRAINT directus_flows_operation_unique UNIQUE (operation); + + +-- +-- Name: directus_flows directus_flows_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_flows + ADD CONSTRAINT directus_flows_pkey PRIMARY KEY (id); + + +-- +-- Name: directus_folders directus_folders_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_folders + ADD CONSTRAINT directus_folders_pkey PRIMARY KEY (id); + + +-- +-- Name: directus_migrations directus_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_migrations + ADD CONSTRAINT directus_migrations_pkey PRIMARY KEY (version); + + +-- +-- Name: directus_notifications directus_notifications_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_notifications + ADD CONSTRAINT directus_notifications_pkey PRIMARY KEY (id); + + +-- +-- Name: directus_operations directus_operations_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_operations + ADD CONSTRAINT directus_operations_pkey PRIMARY KEY (id); + + +-- +-- Name: directus_operations directus_operations_reject_unique; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_operations + ADD CONSTRAINT directus_operations_reject_unique UNIQUE (reject); + + +-- +-- Name: directus_operations directus_operations_resolve_unique; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_operations + ADD CONSTRAINT directus_operations_resolve_unique UNIQUE (resolve); + + +-- +-- Name: directus_panels directus_panels_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_panels + ADD CONSTRAINT directus_panels_pkey PRIMARY KEY (id); + + +-- +-- Name: directus_permissions directus_permissions_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_permissions + ADD CONSTRAINT directus_permissions_pkey PRIMARY KEY (id); + + +-- +-- Name: directus_policies directus_policies_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_policies + ADD CONSTRAINT directus_policies_pkey PRIMARY KEY (id); + + +-- +-- Name: directus_presets directus_presets_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_presets + ADD CONSTRAINT directus_presets_pkey PRIMARY KEY (id); + + +-- +-- Name: directus_relations directus_relations_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_relations + ADD CONSTRAINT directus_relations_pkey PRIMARY KEY (id); + + +-- +-- Name: directus_revisions directus_revisions_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_revisions + ADD CONSTRAINT directus_revisions_pkey PRIMARY KEY (id); + + +-- +-- Name: directus_roles directus_roles_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_roles + ADD CONSTRAINT directus_roles_pkey PRIMARY KEY (id); + + +-- +-- Name: directus_sessions directus_sessions_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_sessions + ADD CONSTRAINT directus_sessions_pkey PRIMARY KEY (token); + + +-- +-- Name: directus_settings directus_settings_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_settings + ADD CONSTRAINT directus_settings_pkey PRIMARY KEY (id); + + +-- +-- Name: directus_shares directus_shares_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_shares + ADD CONSTRAINT directus_shares_pkey PRIMARY KEY (id); + + +-- +-- Name: directus_translations directus_translations_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_translations + ADD CONSTRAINT directus_translations_pkey PRIMARY KEY (id); + + +-- +-- Name: directus_users directus_users_email_unique; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_users + ADD CONSTRAINT directus_users_email_unique UNIQUE (email); + + +-- +-- Name: directus_users directus_users_external_identifier_unique; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_users + ADD CONSTRAINT directus_users_external_identifier_unique UNIQUE (external_identifier); + + +-- +-- Name: directus_users directus_users_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_users + ADD CONSTRAINT directus_users_pkey PRIMARY KEY (id); + + +-- +-- Name: directus_users directus_users_token_unique; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_users + ADD CONSTRAINT directus_users_token_unique UNIQUE (token); + + +-- +-- Name: directus_versions directus_versions_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_versions + ADD CONSTRAINT directus_versions_pkey PRIMARY KEY (id); + + +-- +-- Name: directus_activity_timestamp_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX directus_activity_timestamp_index ON public.directus_activity USING btree ("timestamp"); + + +-- +-- Name: directus_revisions_activity_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX directus_revisions_activity_index ON public.directus_revisions USING btree (activity); + + +-- +-- Name: directus_revisions_parent_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX directus_revisions_parent_index ON public.directus_revisions USING btree (parent); + + +-- +-- Name: directus_access directus_access_policy_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_access + ADD CONSTRAINT directus_access_policy_foreign FOREIGN KEY (policy) REFERENCES public.directus_policies(id) ON DELETE CASCADE; + + +-- +-- Name: directus_access directus_access_role_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_access + ADD CONSTRAINT directus_access_role_foreign FOREIGN KEY (role) REFERENCES public.directus_roles(id) ON DELETE CASCADE; + + +-- +-- Name: directus_access directus_access_user_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_access + ADD CONSTRAINT directus_access_user_foreign FOREIGN KEY ("user") REFERENCES public.directus_users(id) ON DELETE CASCADE; + + +-- +-- Name: directus_collections directus_collections_group_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_collections + ADD CONSTRAINT directus_collections_group_foreign FOREIGN KEY ("group") REFERENCES public.directus_collections(collection); + + +-- +-- Name: directus_comments directus_comments_user_created_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_comments + ADD CONSTRAINT directus_comments_user_created_foreign FOREIGN KEY (user_created) REFERENCES public.directus_users(id) ON DELETE SET NULL; + + +-- +-- Name: directus_comments directus_comments_user_updated_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_comments + ADD CONSTRAINT directus_comments_user_updated_foreign FOREIGN KEY (user_updated) REFERENCES public.directus_users(id); + + +-- +-- Name: directus_dashboards directus_dashboards_user_created_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_dashboards + ADD CONSTRAINT directus_dashboards_user_created_foreign FOREIGN KEY (user_created) REFERENCES public.directus_users(id) ON DELETE SET NULL; + + +-- +-- Name: directus_files directus_files_folder_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_files + ADD CONSTRAINT directus_files_folder_foreign FOREIGN KEY (folder) REFERENCES public.directus_folders(id) ON DELETE SET NULL; + + +-- +-- Name: directus_files directus_files_modified_by_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_files + ADD CONSTRAINT directus_files_modified_by_foreign FOREIGN KEY (modified_by) REFERENCES public.directus_users(id); + + +-- +-- Name: directus_files directus_files_uploaded_by_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_files + ADD CONSTRAINT directus_files_uploaded_by_foreign FOREIGN KEY (uploaded_by) REFERENCES public.directus_users(id); + + +-- +-- Name: directus_flows directus_flows_user_created_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_flows + ADD CONSTRAINT directus_flows_user_created_foreign FOREIGN KEY (user_created) REFERENCES public.directus_users(id) ON DELETE SET NULL; + + +-- +-- Name: directus_folders directus_folders_parent_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_folders + ADD CONSTRAINT directus_folders_parent_foreign FOREIGN KEY (parent) REFERENCES public.directus_folders(id); + + +-- +-- Name: directus_notifications directus_notifications_recipient_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_notifications + ADD CONSTRAINT directus_notifications_recipient_foreign FOREIGN KEY (recipient) REFERENCES public.directus_users(id) ON DELETE CASCADE; + + +-- +-- Name: directus_notifications directus_notifications_sender_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_notifications + ADD CONSTRAINT directus_notifications_sender_foreign FOREIGN KEY (sender) REFERENCES public.directus_users(id); + + +-- +-- Name: directus_operations directus_operations_flow_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_operations + ADD CONSTRAINT directus_operations_flow_foreign FOREIGN KEY (flow) REFERENCES public.directus_flows(id) ON DELETE CASCADE; + + +-- +-- Name: directus_operations directus_operations_reject_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_operations + ADD CONSTRAINT directus_operations_reject_foreign FOREIGN KEY (reject) REFERENCES public.directus_operations(id); + + +-- +-- Name: directus_operations directus_operations_resolve_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_operations + ADD CONSTRAINT directus_operations_resolve_foreign FOREIGN KEY (resolve) REFERENCES public.directus_operations(id); + + +-- +-- Name: directus_operations directus_operations_user_created_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_operations + ADD CONSTRAINT directus_operations_user_created_foreign FOREIGN KEY (user_created) REFERENCES public.directus_users(id) ON DELETE SET NULL; + + +-- +-- Name: directus_panels directus_panels_dashboard_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_panels + ADD CONSTRAINT directus_panels_dashboard_foreign FOREIGN KEY (dashboard) REFERENCES public.directus_dashboards(id) ON DELETE CASCADE; + + +-- +-- Name: directus_panels directus_panels_user_created_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_panels + ADD CONSTRAINT directus_panels_user_created_foreign FOREIGN KEY (user_created) REFERENCES public.directus_users(id) ON DELETE SET NULL; + + +-- +-- Name: directus_permissions directus_permissions_policy_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_permissions + ADD CONSTRAINT directus_permissions_policy_foreign FOREIGN KEY (policy) REFERENCES public.directus_policies(id) ON DELETE CASCADE; + + +-- +-- Name: directus_presets directus_presets_role_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_presets + ADD CONSTRAINT directus_presets_role_foreign FOREIGN KEY (role) REFERENCES public.directus_roles(id) ON DELETE CASCADE; + + +-- +-- Name: directus_presets directus_presets_user_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_presets + ADD CONSTRAINT directus_presets_user_foreign FOREIGN KEY ("user") REFERENCES public.directus_users(id) ON DELETE CASCADE; + + +-- +-- Name: directus_revisions directus_revisions_activity_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_revisions + ADD CONSTRAINT directus_revisions_activity_foreign FOREIGN KEY (activity) REFERENCES public.directus_activity(id) ON DELETE CASCADE; + + +-- +-- Name: directus_revisions directus_revisions_parent_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_revisions + ADD CONSTRAINT directus_revisions_parent_foreign FOREIGN KEY (parent) REFERENCES public.directus_revisions(id); + + +-- +-- Name: directus_revisions directus_revisions_version_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_revisions + ADD CONSTRAINT directus_revisions_version_foreign FOREIGN KEY (version) REFERENCES public.directus_versions(id) ON DELETE CASCADE; + + +-- +-- Name: directus_roles directus_roles_parent_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_roles + ADD CONSTRAINT directus_roles_parent_foreign FOREIGN KEY (parent) REFERENCES public.directus_roles(id); + + +-- +-- Name: directus_sessions directus_sessions_share_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_sessions + ADD CONSTRAINT directus_sessions_share_foreign FOREIGN KEY (share) REFERENCES public.directus_shares(id) ON DELETE CASCADE; + + +-- +-- Name: directus_sessions directus_sessions_user_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_sessions + ADD CONSTRAINT directus_sessions_user_foreign FOREIGN KEY ("user") REFERENCES public.directus_users(id) ON DELETE CASCADE; + + +-- +-- Name: directus_settings directus_settings_project_logo_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_settings + ADD CONSTRAINT directus_settings_project_logo_foreign FOREIGN KEY (project_logo) REFERENCES public.directus_files(id); + + +-- +-- Name: directus_settings directus_settings_public_background_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_settings + ADD CONSTRAINT directus_settings_public_background_foreign FOREIGN KEY (public_background) REFERENCES public.directus_files(id); + + +-- +-- Name: directus_settings directus_settings_public_favicon_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_settings + ADD CONSTRAINT directus_settings_public_favicon_foreign FOREIGN KEY (public_favicon) REFERENCES public.directus_files(id); + + +-- +-- Name: directus_settings directus_settings_public_foreground_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_settings + ADD CONSTRAINT directus_settings_public_foreground_foreign FOREIGN KEY (public_foreground) REFERENCES public.directus_files(id); + + +-- +-- Name: directus_settings directus_settings_public_registration_role_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_settings + ADD CONSTRAINT directus_settings_public_registration_role_foreign FOREIGN KEY (public_registration_role) REFERENCES public.directus_roles(id) ON DELETE SET NULL; + + +-- +-- Name: directus_settings directus_settings_storage_default_folder_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_settings + ADD CONSTRAINT directus_settings_storage_default_folder_foreign FOREIGN KEY (storage_default_folder) REFERENCES public.directus_folders(id) ON DELETE SET NULL; + + +-- +-- Name: directus_shares directus_shares_collection_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_shares + ADD CONSTRAINT directus_shares_collection_foreign FOREIGN KEY (collection) REFERENCES public.directus_collections(collection) ON DELETE CASCADE; + + +-- +-- Name: directus_shares directus_shares_role_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_shares + ADD CONSTRAINT directus_shares_role_foreign FOREIGN KEY (role) REFERENCES public.directus_roles(id) ON DELETE CASCADE; + + +-- +-- Name: directus_shares directus_shares_user_created_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_shares + ADD CONSTRAINT directus_shares_user_created_foreign FOREIGN KEY (user_created) REFERENCES public.directus_users(id) ON DELETE SET NULL; + + +-- +-- Name: directus_users directus_users_role_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_users + ADD CONSTRAINT directus_users_role_foreign FOREIGN KEY (role) REFERENCES public.directus_roles(id) ON DELETE SET NULL; + + +-- +-- Name: directus_versions directus_versions_collection_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_versions + ADD CONSTRAINT directus_versions_collection_foreign FOREIGN KEY (collection) REFERENCES public.directus_collections(collection) ON DELETE CASCADE; + + +-- +-- Name: directus_versions directus_versions_user_created_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_versions + ADD CONSTRAINT directus_versions_user_created_foreign FOREIGN KEY (user_created) REFERENCES public.directus_users(id) ON DELETE SET NULL; + + +-- +-- Name: directus_versions directus_versions_user_updated_foreign; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.directus_versions + ADD CONSTRAINT directus_versions_user_updated_foreign FOREIGN KEY (user_updated) REFERENCES public.directus_users(id); + + +-- +-- PostgreSQL database dump complete +-- + +\unrestrict CbprhGcTL0byLeqVbuTsFhBReWMe8OyOY53RONJHhoY17zx1fVVjxl30zrOpqe6 + diff --git a/lib/config.ts b/lib/config.ts index 83505e5..394af32 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -27,11 +27,9 @@ function createConfig() { analytics: { umami: { - websiteId: env.NEXT_PUBLIC_UMAMI_WEBSITE_ID, - scriptUrl: env.NEXT_PUBLIC_UMAMI_SCRIPT_URL, - // The proxied path used in the frontend - proxyPath: "/stats/script.js", - enabled: Boolean(env.NEXT_PUBLIC_UMAMI_WEBSITE_ID), + websiteId: env.UMAMI_WEBSITE_ID, + apiEndpoint: env.UMAMI_API_ENDPOINT, + enabled: Boolean(env.UMAMI_WEBSITE_ID), }, }, @@ -153,7 +151,7 @@ export function getMaskedConfig() { analytics: { umami: { websiteId: mask(c.analytics.umami.websiteId), - scriptUrl: c.analytics.umami.scriptUrl, + apiEndpoint: c.analytics.umami.apiEndpoint, enabled: c.analytics.umami.enabled, }, }, diff --git a/lib/env.ts b/lib/env.ts index 6877d51..e741fd3 100644 --- a/lib/env.ts +++ b/lib/env.ts @@ -8,78 +8,99 @@ const preprocessEmptyString = (val: unknown) => (val === "" ? undefined : val); /** * Environment variable schema. */ -export const envSchema = z.object({ - NODE_ENV: z - .enum(["development", "production", "test"]) - .default("development"), - NEXT_PUBLIC_BASE_URL: z.preprocess( - preprocessEmptyString, - z.string().url().optional(), - ), - NEXT_PUBLIC_TARGET: z - .enum(["development", "testing", "staging", "production"]) - .optional(), +export const envSchema = z + .object({ + NODE_ENV: z + .enum(["development", "production", "test"]) + .default("development"), + NEXT_PUBLIC_BASE_URL: z.preprocess( + preprocessEmptyString, + z.string().url().optional(), + ), + NEXT_PUBLIC_TARGET: z + .enum(["development", "testing", "staging", "production"]) + .optional(), - // Analytics - NEXT_PUBLIC_UMAMI_WEBSITE_ID: z.preprocess( - preprocessEmptyString, - z.string().optional(), - ), - NEXT_PUBLIC_UMAMI_SCRIPT_URL: z.preprocess( - preprocessEmptyString, - z.string().url().default("https://analytics.infra.mintel.me/script.js"), - ), + // Analytics + UMAMI_WEBSITE_ID: z.preprocess( + preprocessEmptyString, + z.string().optional(), + ), + UMAMI_API_ENDPOINT: z.preprocess( + preprocessEmptyString, + z.string().url().default("https://analytics.infra.mintel.me"), + ), - // Error Tracking - SENTRY_DSN: z.preprocess(preprocessEmptyString, z.string().optional()), + // Error Tracking + SENTRY_DSN: z.preprocess(preprocessEmptyString, z.string().optional()), - // Logging - LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"), + // Logging + LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"), - // Mail - MAIL_HOST: z.preprocess(preprocessEmptyString, z.string().optional()), - MAIL_PORT: z.preprocess( - preprocessEmptyString, - z.coerce.number().default(587), - ), - MAIL_USERNAME: z.preprocess(preprocessEmptyString, z.string().optional()), - MAIL_PASSWORD: z.preprocess(preprocessEmptyString, z.string().optional()), - MAIL_FROM: z.preprocess(preprocessEmptyString, z.string().optional()), - MAIL_RECIPIENTS: z.preprocess( - (val) => (typeof val === "string" ? val.split(",").filter(Boolean) : val), - z.array(z.string()).default([]), - ), + // Mail + MAIL_HOST: z.preprocess(preprocessEmptyString, z.string().optional()), + MAIL_PORT: z.preprocess( + preprocessEmptyString, + z.coerce.number().default(587), + ), + MAIL_USERNAME: z.preprocess(preprocessEmptyString, z.string().optional()), + MAIL_PASSWORD: z.preprocess(preprocessEmptyString, z.string().optional()), + MAIL_FROM: z.preprocess(preprocessEmptyString, z.string().optional()), + MAIL_RECIPIENTS: z.preprocess( + (val) => (typeof val === "string" ? val.split(",").filter(Boolean) : val), + z.array(z.string()).default([]), + ), - // Directus - DIRECTUS_URL: z.preprocess( - preprocessEmptyString, - z.string().url().default("http://localhost:8055"), - ), - DIRECTUS_ADMIN_EMAIL: z.preprocess( - preprocessEmptyString, - z.string().optional(), - ), - DIRECTUS_ADMIN_PASSWORD: z.preprocess( - preprocessEmptyString, - z.string().optional(), - ), - DIRECTUS_API_TOKEN: z.preprocess( - preprocessEmptyString, - z.string().optional(), - ), - INTERNAL_DIRECTUS_URL: z.preprocess( - preprocessEmptyString, - z.string().url().optional(), - ), + // Directus + DIRECTUS_URL: z.preprocess( + preprocessEmptyString, + z.string().url().default("http://localhost:8055"), + ), + DIRECTUS_ADMIN_EMAIL: z.preprocess( + preprocessEmptyString, + z.string().optional(), + ), + DIRECTUS_ADMIN_PASSWORD: z.preprocess( + preprocessEmptyString, + z.string().optional(), + ), + DIRECTUS_API_TOKEN: z.preprocess( + preprocessEmptyString, + z.string().optional(), + ), + INTERNAL_DIRECTUS_URL: z.preprocess( + preprocessEmptyString, + z.string().url().optional(), + ), - // Deploy Target - TARGET: z - .enum(["development", "testing", "staging", "production"]) - .optional(), - // Gotify - GOTIFY_URL: z.preprocess(preprocessEmptyString, z.string().url().optional()), - GOTIFY_TOKEN: z.preprocess(preprocessEmptyString, z.string().optional()), -}); + // Deploy Target + TARGET: z + .enum(["development", "testing", "staging", "production"]) + .optional(), + // Gotify + GOTIFY_URL: z.preprocess( + preprocessEmptyString, + z.string().url().optional(), + ), + GOTIFY_TOKEN: z.preprocess(preprocessEmptyString, z.string().optional()), + }) + .superRefine((data, ctx) => { + const target = data.NEXT_PUBLIC_TARGET || data.TARGET; + const isDev = target === "development" || !target; + const isBuildTimeValidation = + process.env.SKIP_RUNTIME_ENV_VALIDATION === "true"; + const isServer = typeof window === "undefined"; + + // Only enforce server-only variables when running on the server. + // In the browser, non-NEXT_PUBLIC_ variables are undefined and should not trigger validation errors. + if (isServer && !isDev && !isBuildTimeValidation && !data.MAIL_HOST) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "MAIL_HOST is required in non-development environments", + path: ["MAIL_HOST"], + }); + } + }); export type Env = z.infer; @@ -92,8 +113,12 @@ export function getRawEnv() { NODE_ENV: process.env.NODE_ENV, NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL, NEXT_PUBLIC_TARGET: process.env.NEXT_PUBLIC_TARGET, - NEXT_PUBLIC_UMAMI_WEBSITE_ID: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID, - NEXT_PUBLIC_UMAMI_SCRIPT_URL: process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL, + UMAMI_WEBSITE_ID: + process.env.UMAMI_WEBSITE_ID || process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID, + UMAMI_API_ENDPOINT: + process.env.UMAMI_API_ENDPOINT || + process.env.UMAMI_SCRIPT_URL || + process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL, SENTRY_DSN: process.env.SENTRY_DSN, LOG_LEVEL: process.env.LOG_LEVEL, MAIL_HOST: process.env.MAIL_HOST, diff --git a/lib/services/analytics/README.md b/lib/services/analytics/README.md new file mode 100644 index 0000000..0c9e5e1 --- /dev/null +++ b/lib/services/analytics/README.md @@ -0,0 +1,445 @@ +# Analytics Service Layer + +This directory contains the service layer implementation for analytics tracking in the KLZ Cables application. + +## Overview + +The analytics service layer provides a clean abstraction over different analytics implementations (Umami, Google Analytics, etc.) while maintaining a consistent API. + +## Architecture + +``` +lib/services/analytics/ +├── analytics-service.ts # Interface definition +├── umami-analytics-service.ts # Umami implementation +├── noop-analytics-service.ts # No-op fallback implementation +└── README.md # This file +``` + +## Components + +### 1. AnalyticsService Interface (`analytics-service.ts`) + +Defines the contract for all analytics services: + +```typescript +export interface AnalyticsService { + track(eventName: string, props?: AnalyticsEventProperties): void; + trackPageview(url?: string): void; +} +``` + +**Key Features:** + +- Type-safe event properties +- Consistent API across implementations +- Well-documented with JSDoc comments + +### 2. UmamiAnalyticsService (`umami-analytics-service.ts`) + +Implements the `AnalyticsService` interface for Umami analytics. + +**Features:** + +- Type-safe event tracking +- Automatic pageview tracking +- Browser environment detection +- Graceful error handling +- Comprehensive JSDoc documentation + +**Usage:** + +```typescript +import { UmamiAnalyticsService } from "@/lib/services/analytics/umami-analytics-service"; + +const service = new UmamiAnalyticsService({ enabled: true }); +service.track("button_click", { button_id: "cta" }); +service.trackPageview("/products/123"); +``` + +### 3. NoopAnalyticsService (`noop-analytics-service.ts`) + +A no-op implementation used as a fallback when analytics are disabled. + +**Features:** + +- Maintains the same API as other services +- Safe to call even when analytics are disabled +- No performance impact +- Comprehensive JSDoc documentation + +**Usage:** + +```typescript +import { NoopAnalyticsService } from "@/lib/services/analytics/noop-analytics-service"; + +const service = new NoopAnalyticsService(); +service.track("button_click", { button_id: "cta" }); // Does nothing +service.trackPageview("/products/123"); // Does nothing +``` + +## Service Selection + +The service layer automatically selects the appropriate implementation based on environment variables: + +```typescript +// In lib/services/create-services.ts +const umamiEnabled = Boolean(process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID); + +const analytics = umamiEnabled + ? new UmamiAnalyticsService({ enabled: true }) + : new NoopAnalyticsService(); +``` + +## Environment Variables + +### Required for Umami + +```bash +NEXT_PUBLIC_UMAMI_WEBSITE_ID=59a7db94-0100-4c7e-98ef-99f45b17f9c3 +``` + +### Optional (defaults provided) + +```bash +NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js +``` + +## API Reference + +### AnalyticsService Interface + +#### `track(eventName: string, props?: AnalyticsEventProperties): void` + +Track a custom event with optional properties. + +**Parameters:** + +- `eventName` - The name of the event to track +- `props` - Optional event properties (metadata) + +**Example:** + +```typescript +service.track("product_add_to_cart", { + product_id: "123", + product_name: "Cable", + price: 99.99, + quantity: 1, +}); +``` + +#### `trackPageview(url?: string): void` + +Track a pageview. + +**Parameters:** + +- `url` - The URL to track (defaults to current location) + +**Example:** + +```typescript +// Track current page +service.trackPageview(); + +// Track custom URL +service.trackPageview("/products/123?category=cables"); +``` + +### UmamiAnalyticsService + +#### Constructor + +```typescript +new UmamiAnalyticsService(options: UmamiAnalyticsServiceOptions) +``` + +**Options:** + +- `enabled: boolean` - Whether analytics are enabled + +**Example:** + +```typescript +const service = new UmamiAnalyticsService({ enabled: true }); +``` + +### NoopAnalyticsService + +#### Constructor + +```typescript +new NoopAnalyticsService(); +``` + +**Example:** + +```typescript +const service = new NoopAnalyticsService(); +``` + +## Type Definitions + +### AnalyticsEventProperties + +```typescript +type AnalyticsEventProperties = Record< + string, + string | number | boolean | null | undefined +>; +``` + +**Example:** + +```typescript +const properties: AnalyticsEventProperties = { + product_id: "123", + product_name: "Cable", + price: 99.99, + quantity: 1, + in_stock: true, + discount: null, +}; +``` + +### UmamiAnalyticsServiceOptions + +```typescript +type UmamiAnalyticsServiceOptions = { + enabled: boolean; +}; +``` + +## Best Practices + +### 1. Use the Service Layer + +Always use the service layer instead of calling Umami directly: + +```typescript +// ✅ Good +import { getAppServices } from "@/lib/services/create-services"; + +const services = getAppServices(); +services.analytics.track("button_click", { button_id: "cta" }); + +// ❌ Avoid +(window as any).umami?.track("button_click", { button_id: "cta" }); +``` + +### 2. Check Environment + +The service layer automatically handles environment detection: + +```typescript +// ✅ Safe - works in both server and client +const services = getAppServices(); +services.analytics.track("event", { prop: "value" }); + +// ❌ Unsafe - may fail in server environment +if (typeof window !== "undefined") { + window.umami?.track("event", { prop: "value" }); +} +``` + +### 3. Use Type-Safe Events + +Import events from the centralized definitions: + +```typescript +import { AnalyticsEvents } from "@/components/analytics/analytics-events"; + +// ✅ Type-safe +services.analytics.track(AnalyticsEvents.BUTTON_CLICK, { + button_id: "cta", +}); + +// ❌ Prone to typos +services.analytics.track("button_click", { + button_id: "cta", +}); +``` + +### 4. Handle Disabled Analytics + +The service layer gracefully handles disabled analytics: + +```typescript +// When NEXT_PUBLIC_UMAMI_WEBSITE_ID is not set: +// - NoopAnalyticsService is used +// - All calls are safe (no-op) +// - No errors are thrown + +const services = getAppServices(); +services.analytics.track("event", { prop: "value" }); // Safe, does nothing +``` + +## Testing + +### Mocking for Tests + +```typescript +// __tests__/analytics-mock.ts +export const mockAnalytics = { + track: jest.fn(), + trackPageview: jest.fn(), +}; + +jest.mock("@/lib/services/create-services", () => ({ + getAppServices: () => ({ + analytics: mockAnalytics, + }), +})); + +// Usage in tests +import { mockAnalytics } from "./analytics-mock"; + +test("tracks button click", () => { + // ... test code ... + expect(mockAnalytics.track).toHaveBeenCalledWith("button_click", { + button_id: "cta", + }); +}); +``` + +### Development Mode + +In development, the service layer logs to console: + +```bash +# Console output: +[Umami] Tracked event: button_click { button_id: 'cta' } +[Umami] Tracked pageview: /products/123 +``` + +## Error Handling + +The service layer includes built-in error handling: + +1. **Environment Detection** - Checks for browser environment +2. **Service Availability** - Checks if Umami is loaded +3. **Graceful Degradation** - Falls back to NoopAnalyticsService if needed + +```typescript +// These are all safe: +const services = getAppServices(); +services.analytics.track("event", { prop: "value" }); // Works or does nothing +services.analytics.trackPageview("/path"); // Works or does nothing +``` + +## Performance + +### Singleton Pattern + +The service layer uses a singleton pattern for performance: + +```typescript +// First call creates the singleton +const services1 = getAppServices(); + +// Subsequent calls return the cached singleton +const services2 = getAppServices(); + +// services1 === services2 (same instance) +``` + +### Lazy Initialization + +Services are only created when first accessed: + +```typescript +// Services are not created until getAppServices() is called +// This keeps initial bundle size minimal +``` + +## Integration with Components + +### Client Components + +```typescript +'use client'; + +import { getAppServices } from '@/lib/services/create-services'; + +function MyComponent() { + const handleClick = () => { + const services = getAppServices(); + services.analytics.track('button_click', { button_id: 'my-button' }); + }; + + return ; +} +``` + +### Server Components + +```typescript +import { getAppServices } from '@/lib/services/create-services'; + +async function MyServerComponent() { + const services = getAppServices(); + + // Note: Analytics won't work in server components + // Use client components for analytics tracking + // But you can still access other services like cache + + const data = await services.cache.get('key'); + + return
{data}
; +} +``` + +## Troubleshooting + +### Analytics Not Working + +1. **Check environment variables:** + + ```bash + echo $NEXT_PUBLIC_UMAMI_WEBSITE_ID + ``` + +2. **Verify service selection:** + + ```typescript + import { getAppServices } from "@/lib/services/create-services"; + + const services = getAppServices(); + console.log(services.analytics); // Should be UmamiAnalyticsService + ``` + +3. **Check Umami dashboard:** + - Log into Umami + - Verify website ID matches + - Check if data is being received + +### Common Issues + +| Issue | Solution | +| ------------------- | ----------------------------------- | +| No data in Umami | Check website ID and script URL | +| Events not tracking | Verify service is being used | +| Script not loading | Check network connection, CORS | +| Wrong data | Verify event properties are correct | + +## Related Files + +- [`components/analytics/useAnalytics.ts`](../components/analytics/useAnalytics.ts) - Custom hook for easy event tracking +- [`components/analytics/analytics-events.ts`](../components/analytics/analytics-events.ts) - Event definitions +- [`components/analytics/UmamiScript.tsx`](../components/analytics/UmamiScript.tsx) - Script loader component +- [`components/analytics/AnalyticsProvider.tsx`](../components/analytics/AnalyticsProvider.tsx) - Route change tracker +- [`lib/services/create-services.ts`](../lib/services/create-services.ts) - Service factory + +## Summary + +The analytics service layer provides: + +- ✅ **Type-safe API** - TypeScript throughout +- ✅ **Clean abstraction** - Easy to switch analytics providers +- ✅ **Graceful degradation** - Safe no-op fallback +- ✅ **Comprehensive documentation** - JSDoc comments and examples +- ✅ **Performance optimized** - Singleton pattern, lazy initialization +- ✅ **Error handling** - Safe in all environments + +This layer is the foundation for all analytics tracking in the application. diff --git a/lib/services/analytics/analytics-service.ts b/lib/services/analytics/analytics-service.ts index a3400da..b9a1477 100644 --- a/lib/services/analytics/analytics-service.ts +++ b/lib/services/analytics/analytics-service.ts @@ -1,3 +1,76 @@ +/** + * Type definition for analytics event properties. + * + * @example + * ```typescript + * const properties: AnalyticsEventProperties = { + * product_id: '123', + * product_name: 'Cable', + * price: 99.99, + * quantity: 1, + * in_stock: true, + * }; + * ``` + */ +export type AnalyticsEventProperties = Record< + string, + string | number | boolean | null | undefined +>; + +/** + * Interface for analytics service implementations. + * + * This interface defines the contract for all analytics services, + * allowing for different implementations (Umami, Google Analytics, etc.) + * while maintaining a consistent API. + * + * @example + * ```typescript + * // Using the service directly + * const service = new UmamiAnalyticsService({ enabled: true }); + * service.track('button_click', { button_id: 'cta' }); + * service.trackPageview('/products/123'); + * ``` + * + * @example + * ```typescript + * // Using the useAnalytics hook (recommended) + * const { trackEvent, trackPageview } = useAnalytics(); + * trackEvent('button_click', { button_id: 'cta' }); + * trackPageview('/products/123'); + * ``` + */ export interface AnalyticsService { - trackEvent(name: string, properties?: Record): void; + /** + * Track a custom event with optional properties. + * + * @param eventName - The name of the event to track + * @param props - Optional event properties (metadata) + * + * @example + * ```typescript + * track('product_add_to_cart', { + * product_id: '123', + * product_name: 'Cable', + * price: 99.99, + * }); + * ``` + */ + track(eventName: string, props?: AnalyticsEventProperties): void; + + /** + * Track a pageview. + * + * @param url - The URL to track (defaults to current location) + * + * @example + * ```typescript + * // Track current page + * trackPageview(); + * + * // Track custom URL + * trackPageview('/products/123?category=cables'); + * ``` + */ + trackPageview(url?: string): void; } diff --git a/lib/services/analytics/noop-analytics-service.ts b/lib/services/analytics/noop-analytics-service.ts index d171874..701e893 100644 --- a/lib/services/analytics/noop-analytics-service.ts +++ b/lib/services/analytics/noop-analytics-service.ts @@ -1,5 +1,70 @@ -import type { AnalyticsService } from "./analytics-service"; +import type { + AnalyticsEventProperties, + AnalyticsService, +} from "./analytics-service"; +/** + * No-op Analytics Service Implementation. + * + * This service implements the AnalyticsService interface but does nothing. + * It's used as a fallback when analytics are disabled or not configured. + * + * @example + * ```typescript + * // Service creation (usually done by create-services.ts) + * const service = new NoopAnalyticsService(); + * + * // These calls do nothing but are safe to execute + * service.track('button_click', { button_id: 'cta' }); + * service.trackPageview('/products/123'); + * ``` + * + * @example + * ```typescript + * // Automatic fallback in create-services.ts + * const umamiEnabled = Boolean(process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID); + * const analytics = umamiEnabled + * ? new UmamiAnalyticsService({ enabled: true }) + * : new NoopAnalyticsService(); // Fallback when no website ID + * ``` + */ export class NoopAnalyticsService implements AnalyticsService { - trackEvent() {} + /** + * No-op implementation of track. + * + * This method does nothing but maintains the same signature as other + * analytics services for consistency. + * + * @param _eventName - Event name (ignored) + * @param _props - Event properties (ignored) + * + * @example + * ```typescript + * // Safe to call even when analytics are disabled + * service.track('button_click', { button_id: 'cta' }); + * // No error, no action taken + * ``` + */ + track(_eventName: string, _props?: AnalyticsEventProperties) { + // intentionally noop - analytics are disabled + } + + /** + * No-op implementation of trackPageview. + * + * This method does nothing but maintains the same signature as other + * analytics services for consistency. + * + * @param _url - URL to track (ignored) + * + * @example + * ```typescript + * // Safe to call even when analytics are disabled + * service.trackPageview('/products/123'); + * // No error, no action taken + * ``` + */ + trackPageview(_url?: string) { + // intentionally noop - analytics are disabled + } } diff --git a/lib/services/analytics/umami-analytics-service.ts b/lib/services/analytics/umami-analytics-service.ts new file mode 100644 index 0000000..c295f7d --- /dev/null +++ b/lib/services/analytics/umami-analytics-service.ts @@ -0,0 +1,111 @@ +import type { + AnalyticsEventProperties, + AnalyticsService, +} from "./analytics-service"; +import { config } from "../../config"; + +/** + * Configuration options for UmamiAnalyticsService. + * + * @property enabled - Whether analytics are enabled + */ +export type UmamiAnalyticsServiceOptions = { + enabled: boolean; +}; + +/** + * Umami Analytics Service Implementation (Script-less/Proxy edition). + * + * This version implements the Umami tracking protocol directly via fetch, + * eliminating the need to load an external script.js file. + * + * In the browser, it gathers standard metadata (screen, language, referrer) + * and sends it to the proxied '/stats/api/send' endpoint. + */ +export class UmamiAnalyticsService implements AnalyticsService { + private websiteId?: string; + private endpoint: string; + + constructor(private readonly options: UmamiAnalyticsServiceOptions) { + this.websiteId = config.analytics.umami.websiteId; + + // On server, use the full internal URL; on client, use the proxied path + this.endpoint = + typeof window === "undefined" + ? config.analytics.umami.apiEndpoint + : "/stats"; + } + + /** + * Internal method to send the payload to Umami API. + */ + private async sendPayload(type: "event", data: Record) { + if (!this.options.enabled || !this.websiteId) return; + + try { + const payload = { + website: this.websiteId, + hostname: + typeof window !== "undefined" ? window.location.hostname : "server", + screen: + typeof window !== "undefined" + ? `${window.screen.width}x${window.screen.height}` + : undefined, + language: + typeof window !== "undefined" ? navigator.language : undefined, + referrer: typeof window !== "undefined" ? document.referrer : undefined, + ...data, + }; + + const response = await fetch(`${this.endpoint}/api/send`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "User-Agent": + typeof window === "undefined" ? "KLZ-Server" : navigator.userAgent, + }, + body: JSON.stringify({ type, payload }), + // Use keepalive for page navigation events to ensure they complete + keepalive: true, + } as any); + + if (!response.ok && process.env.NODE_ENV === "development") { + const errorText = await response.text(); + console.warn( + `[Umami] API responded with ${response.status}: ${errorText}`, + ); + } + } catch (error) { + if (process.env.NODE_ENV === "development") { + console.error("[Umami] Failed to send analytics:", error); + } + } + } + + /** + * Track a custom event. + */ + track(eventName: string, props?: AnalyticsEventProperties) { + this.sendPayload("event", { + name: eventName, + data: props, + url: + typeof window !== "undefined" + ? window.location.pathname + window.location.search + : undefined, + }); + } + + /** + * Track a pageview. + */ + trackPageview(url?: string) { + this.sendPayload("event", { + url: + url || + (typeof window !== "undefined" + ? window.location.pathname + window.location.search + : undefined), + }); + } +} diff --git a/lib/services/app-services.ts b/lib/services/app-services.ts index f7732cf..e175d5e 100644 --- a/lib/services/app-services.ts +++ b/lib/services/app-services.ts @@ -4,6 +4,7 @@ import type { ErrorReportingService } from "./errors/error-reporting-service"; import type { LoggerService } from "./logging/logger-service"; import type { NotificationService } from "./notifications/notification-service"; +// Simple constructor-based DI container. export class AppServices { constructor( public readonly analytics: AnalyticsService, diff --git a/lib/services/cache/cache-service.ts b/lib/services/cache/cache-service.ts index 80dca1c..34f7f86 100644 --- a/lib/services/cache/cache-service.ts +++ b/lib/services/cache/cache-service.ts @@ -1,5 +1,9 @@ +export type CacheSetOptions = { + ttlSeconds?: number; +}; + export interface CacheService { - get(key: string): Promise; - set(key: string, value: T, ttlSeconds?: number): Promise; - delete(key: string): Promise; + get(key: string): Promise; + set(key: string, value: T, options?: CacheSetOptions): Promise; + del(key: string): Promise; } diff --git a/lib/services/cache/memory-cache-service.ts b/lib/services/cache/memory-cache-service.ts index 8e92159..84803e5 100644 --- a/lib/services/cache/memory-cache-service.ts +++ b/lib/services/cache/memory-cache-service.ts @@ -1,26 +1,30 @@ -import type { CacheService } from "./cache-service"; +import type { CacheService, CacheSetOptions } from "./cache-service"; + +type Entry = { + value: unknown; + expiresAt?: number; +}; export class MemoryCacheService implements CacheService { - private cache = new Map(); + private readonly store = new Map(); - async get(key: string): Promise { - const item = this.cache.get(key); - if (!item) return null; - - if (item.expiry && item.expiry < Date.now()) { - this.cache.delete(key); - return null; + async get(key: string) { + const entry = this.store.get(key); + if (!entry) return undefined; + if (entry.expiresAt && Date.now() > entry.expiresAt) { + this.store.delete(key); + return undefined; } - - return item.value as T; + return entry.value as T; } - async set(key: string, value: T, ttlSeconds?: number): Promise { - const expiry = ttlSeconds ? Date.now() + ttlSeconds * 1000 : null; - this.cache.set(key, { value, expiry }); + async set(key: string, value: T, options?: CacheSetOptions) { + const ttl = options?.ttlSeconds; + const expiresAt = ttl ? Date.now() + ttl * 1000 : undefined; + this.store.set(key, { value, expiresAt }); } - async delete(key: string): Promise { - this.cache.delete(key); + async del(key: string) { + this.store.delete(key); } } diff --git a/lib/services/create-services.server.ts b/lib/services/create-services.server.ts index 54ec88b..5d70238 100644 --- a/lib/services/create-services.server.ts +++ b/lib/services/create-services.server.ts @@ -1,18 +1,21 @@ import { AppServices } from "./app-services"; import { NoopAnalyticsService } from "./analytics/noop-analytics-service"; +import { UmamiAnalyticsService } from "./analytics/umami-analytics-service"; import { MemoryCacheService } from "./cache/memory-cache-service"; import { GlitchtipErrorReportingService } from "./errors/glitchtip-error-reporting-service"; import { NoopErrorReportingService } from "./errors/noop-error-reporting-service"; -import { GotifyNotificationService } from "./notifications/gotify-notification-service"; -import { NoopNotificationService } from "./notifications/noop-notification-service"; +import { + GotifyNotificationService, + NoopNotificationService, +} from "./notifications/gotify-notification-service"; import { PinoLoggerService } from "./logging/pino-logger-service"; import { config, getMaskedConfig } from "../config"; let singleton: AppServices | undefined; - export function getServerAppServices(): AppServices { if (singleton) return singleton; + // Create logger first to log initialization const logger = new PinoLoggerService("server"); logger.info("Initializing server application services", { @@ -20,7 +23,22 @@ export function getServerAppServices(): AppServices { timestamp: new Date().toISOString(), }); - const analytics = new NoopAnalyticsService(); + logger.info("Service configuration", { + umamiEnabled: config.analytics.umami.enabled, + sentryEnabled: config.errors.glitchtip.enabled, + mailEnabled: Boolean(config.mail.host && config.mail.user), + gotifyEnabled: config.notifications.gotify.enabled, + }); + + const analytics = config.analytics.umami.enabled + ? new UmamiAnalyticsService({ enabled: true }) + : new NoopAnalyticsService(); + + if (config.analytics.umami.enabled) { + logger.info("Umami analytics service initialized"); + } else { + logger.info("Noop analytics service initialized (analytics disabled)"); + } const notifications = config.notifications.gotify.enabled ? new GotifyNotificationService({ @@ -30,11 +48,35 @@ export function getServerAppServices(): AppServices { }) : new NoopNotificationService(); + if (config.notifications.gotify.enabled) { + logger.info("Gotify notification service initialized"); + } else { + logger.info( + "Noop notification service initialized (notifications disabled)", + ); + } + const errors = config.errors.glitchtip.enabled ? new GlitchtipErrorReportingService({ enabled: true }, notifications) : new NoopErrorReportingService(); + if (config.errors.glitchtip.enabled) { + logger.info("GlitchTip error reporting service initialized", { + dsnPresent: Boolean(config.errors.glitchtip.dsn), + }); + } else { + logger.info( + "Noop error reporting service initialized (error reporting disabled)", + ); + } + const cache = new MemoryCacheService(); + logger.info("Memory cache service initialized"); + + logger.info("Pino logger service initialized", { + name: "server", + level: config.logging.level, + }); singleton = new AppServices(analytics, errors, cache, logger, notifications); diff --git a/lib/services/create-services.ts b/lib/services/create-services.ts new file mode 100644 index 0000000..ee96c82 --- /dev/null +++ b/lib/services/create-services.ts @@ -0,0 +1,154 @@ +import { AppServices } from "./app-services"; +import { NoopAnalyticsService } from "./analytics/noop-analytics-service"; +import { MemoryCacheService } from "./cache/memory-cache-service"; +import { GlitchtipErrorReportingService } from "./errors/glitchtip-error-reporting-service"; +import { NoopErrorReportingService } from "./errors/noop-error-reporting-service"; +import { NoopLoggerService } from "./logging/noop-logger-service"; +import { PinoLoggerService } from "./logging/pino-logger-service"; +import { NoopNotificationService } from "./notifications/gotify-notification-service"; +import { config, getMaskedConfig } from "../config"; + +/** + * Singleton instance of AppServices. + * + * In Next.js, module singletons are per-process (server) and per-tab (client). + * This is sufficient for a small service layer and provides better performance + * than creating new instances on every request. + * + * @private + */ +let singleton: AppServices | undefined; + +/** + * Get the application services singleton. + * + * This function creates and caches the application services, including: + * - Analytics service (Umami or no-op) + * - Error reporting service (GlitchTip/Sentry or no-op) + * - Cache service (in-memory) + * + * The services are configured based on environment variables: + * - `UMAMI_WEBSITE_ID` - Enables Umami analytics + * - `NEXT_PUBLIC_SENTRY_DSN` - Enables client-side error reporting + * - `SENTRY_DSN` - Enables server-side error reporting + * + * @returns {AppServices} The application services singleton + * + * @example + * ```typescript + * // Get services in a client component + * import { getAppServices } from '@/lib/services/create-services'; + * + * const services = getAppServices(); + * services.analytics.track('button_click', { button_id: 'cta' }); + * ``` + * + * @example + * ```typescript + * // Get services in a server component or API route + * import { getAppServices } from '@/lib/services/create-services'; + * + * const services = getAppServices(); + * await services.cache.set('key', 'value'); + * ``` + * + * @example + * ```typescript + * // Automatic service selection based on environment + * // If NEXT_PUBLIC_UMAMI_WEBSITE_ID is set: + * // services.analytics = UmamiAnalyticsService + * // If not set: + * // services.analytics = NoopAnalyticsService (safe no-op) + * ``` + * + * @see {@link UmamiAnalyticsService} for analytics implementation + * @see {@link NoopAnalyticsService} for no-op fallback + * @see {@link GlitchtipErrorReportingService} for error reporting + * @see {@link MemoryCacheService} for caching + */ +export function getAppServices(): AppServices { + // Return cached instance if available + if (singleton) return singleton; + + // Create logger first to log initialization + const logger = + typeof window === "undefined" + ? new PinoLoggerService("server") + : new NoopLoggerService(); + + // Log initialization + if (typeof window === "undefined") { + // Server-side + logger.info("Initializing server application services", { + environment: getMaskedConfig(), + timestamp: new Date().toISOString(), + }); + } else { + // Client-side + logger.info("Initializing client application services", { + environment: getMaskedConfig(), + timestamp: new Date().toISOString(), + }); + } + + // Determine which services to enable based on environment variables + const umamiEnabled = config.analytics.umami.enabled; + const sentryEnabled = config.errors.glitchtip.enabled; + + logger.info("Service configuration", { + umamiEnabled, + sentryEnabled, + isServer: typeof window === "undefined", + }); + + // Create analytics service (Umami or no-op) + // Use dynamic import to avoid importing server-only code in client components + const analytics = umamiEnabled + ? (() => { + const { + UmamiAnalyticsService, + } = require("./analytics/umami-analytics-service"); + return new UmamiAnalyticsService({ enabled: true }); + })() + : new NoopAnalyticsService(); + + if (umamiEnabled) { + logger.info("Umami analytics service initialized"); + } else { + logger.info("Noop analytics service initialized (analytics disabled)"); + } + + // Create error reporting service (GlitchTip/Sentry or no-op) + const errors = sentryEnabled + ? new GlitchtipErrorReportingService({ enabled: true }) + : new NoopErrorReportingService(); + + if (sentryEnabled) { + logger.info( + `GlitchTip error reporting service initialized (${typeof window === "undefined" ? "server" : "client"})`, + ); + } else { + logger.info( + "Noop error reporting service initialized (error reporting disabled)", + ); + } + + // IMPORTANT: This module is imported by client components. + // Do not import Node-only modules (like the `redis` client) here. + // Use [`getServerAppServices()`](lib/services/create-services.server.ts:1) on the server. + const cache = new MemoryCacheService(); + logger.info("Memory cache service initialized"); + + logger.info("Pino logger service initialized", { + name: typeof window === "undefined" ? "server" : "client", + level: config.logging.level, + }); + + // Create and cache the singleton + const notifications = new NoopNotificationService(); + singleton = new AppServices(analytics, errors, cache, logger, notifications); + + logger.info("All application services initialized successfully"); + + return singleton; +} diff --git a/lib/services/errors/error-reporting-service.ts b/lib/services/errors/error-reporting-service.ts index 5ba6cc2..ef3f734 100644 --- a/lib/services/errors/error-reporting-service.ts +++ b/lib/services/errors/error-reporting-service.ts @@ -1,4 +1,27 @@ +export type ErrorReportingUser = { + id?: string; + email?: string; + username?: string; +}; + +export type ErrorReportingLevel = + | "fatal" + | "error" + | "warning" + | "info" + | "debug" + | "log"; + export interface ErrorReportingService { - captureException(error: unknown, context?: Record): void; - captureMessage(message: string, context?: Record): void; + captureException( + error: unknown, + context?: Record, + ): Promise | string | undefined; + captureMessage( + message: string, + level?: ErrorReportingLevel, + ): Promise | string | undefined; + setUser(user: ErrorReportingUser | null): void; + setTag(key: string, value: string): void; + withScope(fn: () => T, context?: Record): T; } diff --git a/lib/services/errors/glitchtip-error-reporting-service.ts b/lib/services/errors/glitchtip-error-reporting-service.ts index d78eb82..e58c251 100644 --- a/lib/services/errors/glitchtip-error-reporting-service.ts +++ b/lib/services/errors/glitchtip-error-reporting-service.ts @@ -1,48 +1,74 @@ import * as Sentry from "@sentry/nextjs"; -import type { ErrorReportingService } from "./error-reporting-service"; +import type { + ErrorReportingLevel, + ErrorReportingService, + ErrorReportingUser, +} from "./error-reporting-service"; import type { NotificationService } from "../notifications/notification-service"; -export interface GlitchtipConfig { - enabled: boolean; -} +type SentryLike = typeof Sentry; +export type GlitchtipErrorReportingServiceOptions = { + enabled: boolean; +}; + +// GlitchTip speaks the Sentry protocol; @sentry/nextjs can send to GlitchTip via DSN. export class GlitchtipErrorReportingService implements ErrorReportingService { constructor( - private readonly config: GlitchtipConfig, + private readonly options: GlitchtipErrorReportingServiceOptions, private readonly notifications?: NotificationService, + private readonly sentry: SentryLike = Sentry, ) {} - captureException(error: unknown, context?: Record) { - if (!this.config.enabled) return; - - Sentry.withScope((scope) => { - if (context) { - scope.setExtras(context); - } - Sentry.captureException(error); - }); + async captureException(error: unknown, context?: Record) { + if (!this.options.enabled) return undefined; + const result = this.sentry.captureException(error, context as any) as any; + // Send to Gotify if it's considered critical or if we just want all exceptions there + // For now, let's send all exceptions to Gotify as requested "notify me via gotify about critical error messages" + // We'll treat all captureException calls as potentially critical or at least noteworthy if (this.notifications) { - this.notifications - .notify({ - title: "🚨 Exception Captured", - message: error instanceof Error ? error.message : String(error), - priority: 10, - }) - .catch((err) => - console.error("Failed to send notification for exception", err), - ); + const errorMessage = + error instanceof Error ? error.message : String(error); + const contextStr = context + ? `\nContext: ${JSON.stringify(context, null, 2)}` + : ""; + + await this.notifications.notify({ + title: "🔥 Critical Error Captured", + message: `Error: ${errorMessage}${contextStr}`, + priority: 7, + }); } + + return result; } - captureMessage(message: string, context?: Record) { - if (!this.config.enabled) return; + captureMessage(message: string, level: ErrorReportingLevel = "error") { + if (!this.options.enabled) return undefined; + return this.sentry.captureMessage(message, level as any) as any; + } - Sentry.withScope((scope) => { + setUser(user: ErrorReportingUser | null) { + if (!this.options.enabled) return; + this.sentry.setUser(user as any); + } + + setTag(key: string, value: string) { + if (!this.options.enabled) return; + this.sentry.setTag(key, value); + } + + withScope(fn: () => T, context?: Record) { + if (!this.options.enabled) return fn(); + + return this.sentry.withScope((scope) => { if (context) { - scope.setExtras(context); + for (const [key, value] of Object.entries(context)) { + scope.setExtra(key, value); + } } - Sentry.captureMessage(message); + return fn(); }); } } diff --git a/lib/services/errors/noop-error-reporting-service.ts b/lib/services/errors/noop-error-reporting-service.ts index 156cff9..28452b5 100644 --- a/lib/services/errors/noop-error-reporting-service.ts +++ b/lib/services/errors/noop-error-reporting-service.ts @@ -1,6 +1,22 @@ -import type { ErrorReportingService } from "./error-reporting-service"; +import type { + ErrorReportingLevel, + ErrorReportingService, + ErrorReportingUser, +} from "./error-reporting-service"; export class NoopErrorReportingService implements ErrorReportingService { - captureException() {} - captureMessage() {} + async captureException(_error: unknown, _context?: Record) { + return undefined; + } + + async captureMessage(_message: string, _level?: ErrorReportingLevel) { + return undefined; + } + + setUser(_user: ErrorReportingUser | null) {} + setTag(_key: string, _value: string) {} + + withScope(fn: () => T, _context?: Record) { + return fn(); + } } diff --git a/lib/services/logging/logger-service.ts b/lib/services/logging/logger-service.ts index 6cb6a3e..ed9680d 100644 --- a/lib/services/logging/logger-service.ts +++ b/lib/services/logging/logger-service.ts @@ -1,7 +1,11 @@ +export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal"; + export interface LoggerService { - debug(message: string, context?: Record): void; - info(message: string, context?: Record): void; - warn(message: string, context?: Record): void; - error(message: string, context?: Record): void; - child(context: Record): LoggerService; + trace(msg: string, ...args: any[]): void; + debug(msg: string, ...args: any[]): void; + info(msg: string, ...args: any[]): void; + warn(msg: string, ...args: any[]): void; + error(msg: string, ...args: any[]): void; + fatal(msg: string, ...args: any[]): void; + child(bindings: Record): LoggerService; } diff --git a/lib/services/logging/noop-logger-service.ts b/lib/services/logging/noop-logger-service.ts new file mode 100644 index 0000000..342660a --- /dev/null +++ b/lib/services/logging/noop-logger-service.ts @@ -0,0 +1,13 @@ +import type { LoggerService } from "./logger-service"; + +export class NoopLoggerService implements LoggerService { + trace() {} + debug() {} + info() {} + warn() {} + error() {} + fatal() {} + child() { + return this; + } +} diff --git a/lib/services/logging/pino-logger-service.ts b/lib/services/logging/pino-logger-service.ts index 084a564..fec1ceb 100644 --- a/lib/services/logging/pino-logger-service.ts +++ b/lib/services/logging/pino-logger-service.ts @@ -1,14 +1,17 @@ -import { pino, type Logger as PinoLogger } from "pino"; +import pino, { Logger as PinoLogger } from "pino"; import type { LoggerService } from "./logger-service"; import { config } from "../../config"; export class PinoLoggerService implements LoggerService { - private logger: PinoLogger; + private readonly logger: PinoLogger; constructor(name?: string, parent?: PinoLogger) { if (parent) { this.logger = parent.child({ name }); } else { + // In Next.js, especially in the Edge runtime or during instrumentation, + // pino transports (which use worker threads) can cause issues. + // We disable transport in production and during instrumentation. const useTransport = config.isDevelopment && typeof window === "undefined"; @@ -27,29 +30,40 @@ export class PinoLoggerService implements LoggerService { } } - debug(message: string, context?: Record) { - if (context) this.logger.debug(context, message); - else this.logger.debug(message); + trace(msg: string, ...args: any[]) { + // @ts-expect-error - pino types can be strict + this.logger.trace(msg, ...args); } - info(message: string, context?: Record) { - if (context) this.logger.info(context, message); - else this.logger.info(message); + debug(msg: string, ...args: any[]) { + // @ts-expect-error - pino types can be strict + this.logger.debug(msg, ...args); } - warn(message: string, context?: Record) { - if (context) this.logger.warn(context, message); - else this.logger.warn(message); + info(msg: string, ...args: any[]) { + // @ts-expect-error - pino types can be strict + this.logger.info(msg, ...args); } - error(message: string, context?: Record) { - if (context) this.logger.error(context, message); - else this.logger.error(message); + warn(msg: string, ...args: any[]) { + // @ts-expect-error - pino types can be strict + this.logger.warn(msg, ...args); } - child(context: Record): LoggerService { - const childPino = this.logger.child(context); + error(msg: string, ...args: any[]) { + // @ts-expect-error - pino types can be strict + this.logger.error(msg, ...args); + } + + fatal(msg: string, ...args: any[]) { + // @ts-expect-error - pino types can be strict + this.logger.fatal(msg, ...args); + } + + child(bindings: Record): LoggerService { + const childPino = this.logger.child(bindings); const service = new PinoLoggerService(); + // @ts-expect-error - accessing private member for child creation service.logger = childPino; return service; } diff --git a/lib/services/notifications/gotify-notification-service.ts b/lib/services/notifications/gotify-notification-service.ts index ae3f8ef..159ad98 100644 --- a/lib/services/notifications/gotify-notification-service.ts +++ b/lib/services/notifications/gotify-notification-service.ts @@ -1,5 +1,5 @@ -import type { - NotificationMessage, +import { + NotificationOptions, NotificationService, } from "./notification-service"; @@ -10,35 +10,43 @@ export interface GotifyConfig { } export class GotifyNotificationService implements NotificationService { - constructor(private readonly config: GotifyConfig) {} + constructor(private config: GotifyConfig) {} - async notify(message: NotificationMessage): Promise { + async notify(options: NotificationOptions): Promise { if (!this.config.enabled) return; try { - const response = await fetch( - `${this.config.url}/message?token=${this.config.token}`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - title: message.title, - message: message.message, - priority: message.priority ?? 5, - }), + const { title, message, priority = 4 } = options; + const url = new URL("message", this.config.url); + url.searchParams.set("token", this.config.token); + + const response = await fetch(url.toString(), { + method: "POST", + headers: { + "Content-Type": "application/json", }, - ); + body: JSON.stringify({ + title, + message, + priority, + }), + }); if (!response.ok) { - console.error( - "Failed to send Gotify notification", - await response.text(), - ); + const errorText = await response.text(); + console.error("Gotify notification failed:", { + status: response.status, + error: errorText, + }); } } catch (error) { - console.error("Error sending Gotify notification", error); + console.error("Gotify notification error:", error); } } } + +export class NoopNotificationService implements NotificationService { + async notify(): Promise { + // Do nothing + } +} diff --git a/lib/services/notifications/noop-notification-service.ts b/lib/services/notifications/noop-notification-service.ts deleted file mode 100644 index 3368334..0000000 --- a/lib/services/notifications/noop-notification-service.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { NotificationService } from "./notification-service"; - -export class NoopNotificationService implements NotificationService { - async notify() {} -} diff --git a/lib/services/notifications/notification-service.ts b/lib/services/notifications/notification-service.ts index f7553ae..ac5a52d 100644 --- a/lib/services/notifications/notification-service.ts +++ b/lib/services/notifications/notification-service.ts @@ -1,9 +1,9 @@ -export interface NotificationMessage { +export interface NotificationOptions { title: string; message: string; - priority?: number; // 0-10, Gotify style + priority?: number; } export interface NotificationService { - notify(message: NotificationMessage): Promise; + notify(options: NotificationOptions): Promise; } diff --git a/next.config.mjs b/next.config.mjs index f804c7c..c26add1 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,6 +1,28 @@ import withMintelConfig from "@mintel/next-config"; /** @type {import('next').NextConfig} */ -const nextConfig = {}; +const nextConfig = { + async rewrites() { + const umamiUrl = + process.env.UMAMI_API_ENDPOINT || + process.env.UMAMI_SCRIPT_URL || + process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL || + "https://analytics.infra.mintel.me"; + const glitchtipUrl = process.env.SENTRY_DSN + ? new URL(process.env.SENTRY_DSN).origin + : "https://errors.infra.mintel.me"; + + return [ + { + source: "/stats/:path*", + destination: `${umamiUrl}/:path*`, + }, + { + source: "/errors/:path*", + destination: `${glitchtipUrl}/:path*`, + }, + ]; + }, +}; export default withMintelConfig(nextConfig);