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);