diff --git a/app/admin/settings/page.tsx b/app/admin/settings/page.tsx
index 3deeef2..3c87329 100644
--- a/app/admin/settings/page.tsx
+++ b/app/admin/settings/page.tsx
@@ -20,6 +20,7 @@ import {
Share2,
MapPin,
Image as ImageIcon,
+ EyeOff,
} from "lucide-react";
import Image from "next/image";
import Link from "next/link";
@@ -580,6 +581,69 @@ export default function AdminSettingsPage() {
+ {/* Coming Soon — landing CTA */}
+
+
+
+
+
+
+
+
+ Coming Soon (หน้า Landing)
+
+
+ เมื่อเปิด ปุ่มเข้าสู่ระบบบนหน้าแรกจะถูกปิด และแสดงข้อความแทน
+
+
+
+
+
+ {settings.comingSoonEnabled ? (
+
+
+
+ setSettings({
+ ...settings,
+ comingSoonMessage: e.target.value,
+ })
+ }
+ placeholder="พบกันเร็วๆนี้"
+ className="w-full px-4 py-2 bg-white dark:bg-gray-600 border border-gray-200 dark:border-gray-500 rounded-lg focus:outline-none focus:ring-2 focus:ring-line-green"
+ />
+
+ ) : null}
+
+
+
{/* Map & GPS — managed on dedicated page */}
mapArticle(row as Record));
} catch (error) {
- console.error("[blog] listPublishedArticles:", error);
+ if (!isMissingRelationError(error)) {
+ console.error("[blog] listPublishedArticles:", error);
+ }
return [];
}
}
@@ -58,13 +63,17 @@ export async function getPublishedArticleBySlug(
const { data, error } = await query.maybeSingle();
if (error) {
- console.error("[blog] getPublishedArticleBySlug:", error);
+ if (!isMissingRelationError(error)) {
+ console.error("[blog] getPublishedArticleBySlug:", error);
+ }
return null;
}
if (!data) return null;
return mapArticle(data as Record);
} catch (error) {
- console.error("[blog] getPublishedArticleBySlug:", error);
+ if (!isMissingRelationError(error)) {
+ console.error("[blog] getPublishedArticleBySlug:", error);
+ }
return null;
}
}
@@ -81,13 +90,17 @@ export async function getArticleById(id: string): Promise {
.eq("id", id)
.maybeSingle();
if (error) {
- console.error("[blog] getArticleById:", error);
+ if (!isMissingRelationError(error)) {
+ console.error("[blog] getArticleById:", error);
+ }
return null;
}
if (!data) return null;
return mapArticle(data as Record);
} catch (error) {
- console.error("[blog] getArticleById:", error);
+ if (!isMissingRelationError(error)) {
+ console.error("[blog] getArticleById:", error);
+ }
return null;
}
}
diff --git a/lib/help/data.ts b/lib/help/data.ts
index 849c42d..4e04497 100644
--- a/lib/help/data.ts
+++ b/lib/help/data.ts
@@ -1,6 +1,7 @@
import { createAdminClient } from "@/lib/supabase/admin";
import { hasSupabaseAdminEnv } from "@/lib/setup/db-url";
import { getPublishedArticleBySlug } from "@/lib/blog/data";
+import { isMissingRelationError } from "@/lib/supabase/missing-relation";
import type { Article } from "@/lib/blog/types";
import type {
HelpAudience,
@@ -61,8 +62,12 @@ export async function getHelpPageWithSections(
.order("sort_order", { ascending: true }),
]);
- if (pageError) console.error("[help] page fetch error:", pageError);
- if (sectionError) console.error("[help] sections fetch error:", sectionError);
+ if (pageError && !isMissingRelationError(pageError)) {
+ console.error("[help] page fetch error:", pageError);
+ }
+ if (sectionError && !isMissingRelationError(sectionError)) {
+ console.error("[help] sections fetch error:", sectionError);
+ }
if (pageError || sectionError || !pageRow) return null;
return {
@@ -72,7 +77,9 @@ export async function getHelpPageWithSections(
),
};
} catch (error) {
- console.error("[help] getHelpPageWithSections:", error);
+ if (!isMissingRelationError(error)) {
+ console.error("[help] getHelpPageWithSections:", error);
+ }
return null;
}
}
@@ -110,8 +117,12 @@ export async function listHelpPages(): Promise {
.order("published_at", { ascending: false }),
]);
- if (legacyError) console.error("[help] listHelpPages legacy:", legacyError);
- if (articleError) console.error("[help] listHelpPages articles:", articleError);
+ if (legacyError && !isMissingRelationError(legacyError)) {
+ console.error("[help] listHelpPages legacy:", legacyError);
+ }
+ if (articleError && !isMissingRelationError(articleError)) {
+ console.error("[help] listHelpPages articles:", articleError);
+ }
const legacy = (legacyRows ?? []).map((row) =>
mapPage(row as Record)
@@ -130,7 +141,9 @@ export async function listHelpPages(): Promise {
return [...legacy, ...fromArticles];
} catch (error) {
- console.error("[help] listHelpPages:", error);
+ if (!isMissingRelationError(error)) {
+ console.error("[help] listHelpPages:", error);
+ }
return [];
}
}
diff --git a/lib/landing-public-data.ts b/lib/landing-public-data.ts
index f3f6550..5ae6559 100644
--- a/lib/landing-public-data.ts
+++ b/lib/landing-public-data.ts
@@ -88,7 +88,7 @@ export async function getPublicLandingSettings(): Promise
};
} catch {
return {
- comingSoonEnabled: DEFAULT_APP_SETTINGS.comingSoonEnabled ?? true,
+ comingSoonEnabled: DEFAULT_APP_SETTINGS.comingSoonEnabled ?? false,
comingSoonMessage: normalizeComingSoonMessage(
DEFAULT_APP_SETTINGS.comingSoonMessage,
"พบกันเร็วๆนี้"
diff --git a/lib/setup/ensure-content-cms.ts b/lib/setup/ensure-content-cms.ts
new file mode 100644
index 0000000..f8795c3
--- /dev/null
+++ b/lib/setup/ensure-content-cms.ts
@@ -0,0 +1,48 @@
+import postgres from "postgres";
+import { resolvePostgresUrl } from "@/lib/setup/db-url";
+import { tableExists } from "@/lib/setup/probe";
+import {
+ loadArticlesMigrationSql,
+ loadHelpPagesMigrationSql,
+} from "@/lib/setup/schemas";
+
+/**
+ * Applies help_pages + articles CMS migrations when missing.
+ * Needed because hydrateDatabase() often returns early in "skipped" mode
+ * after lost_items + system_config already exist (Vercel/Supabase new school).
+ */
+export async function ensureContentCmsWithSql(sql: postgres.Sql): Promise {
+ const hasHelp = await tableExists(sql, "public", "help_pages");
+ if (!hasHelp) {
+ const helpSql = loadHelpPagesMigrationSql();
+ if (helpSql) {
+ await sql.unsafe(helpSql);
+ }
+ }
+
+ const hasArticles = await tableExists(sql, "public", "articles");
+ if (!hasArticles) {
+ const articlesSql = loadArticlesMigrationSql();
+ if (articlesSql) {
+ await sql.unsafe(articlesSql);
+ }
+ }
+}
+
+export async function ensureContentCms(): Promise {
+ const connectionString = resolvePostgresUrl();
+ if (!connectionString) return;
+
+ const sql = postgres(connectionString, {
+ max: 1,
+ idle_timeout: 5,
+ connect_timeout: 15,
+ prepare: false,
+ });
+
+ try {
+ await ensureContentCmsWithSql(sql);
+ } finally {
+ await sql.end({ timeout: 5 });
+ }
+}
diff --git a/lib/setup/hydrator.ts b/lib/setup/hydrator.ts
index 58e4a88..a257fd2 100644
--- a/lib/setup/hydrator.ts
+++ b/lib/setup/hydrator.ts
@@ -4,6 +4,7 @@ import {
RESET_FALSE_SETUP_WITHOUT_ADMIN_SQL,
} from "@/lib/setup/backfill-sql";
import { ensureAccountsTableWithSql } from "@/lib/setup/ensure-accounts-table";
+import { ensureContentCmsWithSql } from "@/lib/setup/ensure-content-cms";
import { ensureStorageBucketsWithSql } from "@/lib/setup/ensure-storage-buckets";
import { SETUP_ADVISORY_LOCK_ID } from "@/lib/setup/constants";
import { resolvePostgresUrl } from "@/lib/setup/db-url";
@@ -44,6 +45,7 @@ export async function hydrateDatabase(): Promise {
if (state.hasSystemConfig && state.hasLostItems) {
await ensureAccountsTableWithSql(sql);
+ await ensureContentCmsWithSql(sql);
await backfillSetupStatusIfNeeded(sql);
await ensureStorageBucketsWithSql(sql);
return { ok: true, mode: "skipped" };
@@ -56,6 +58,7 @@ export async function hydrateDatabase(): Promise {
}
await runSqlBatch(sql, systemConfigSql);
await ensureAccountsTableWithSql(sql);
+ await ensureContentCmsWithSql(sql);
await backfillSetupStatusIfNeeded(sql);
await ensureStorageBucketsWithSql(sql);
return { ok: true, mode: "system_config_only" };
@@ -71,6 +74,7 @@ export async function hydrateDatabase(): Promise {
}
await ensureAccountsTableWithSql(sql);
+ await ensureContentCmsWithSql(sql);
await ensureStorageBucketsWithSql(sql);
await backfillSetupStatusIfNeeded(sql);
diff --git a/lib/setup/schemas/index.ts b/lib/setup/schemas/index.ts
index 5239f82..6e31174 100644
--- a/lib/setup/schemas/index.ts
+++ b/lib/setup/schemas/index.ts
@@ -40,3 +40,17 @@ export function loadAccountsMigrationSql(): string | null {
);
return file ? readMigrationSql(file) : null;
}
+
+export function loadHelpPagesMigrationSql(): string | null {
+ const file = listMigrationFiles().find((name) =>
+ name.includes("help_pages_cms")
+ );
+ return file ? readMigrationSql(file) : null;
+}
+
+export function loadArticlesMigrationSql(): string | null {
+ const file = listMigrationFiles().find((name) =>
+ name.includes("articles_blog_cms")
+ );
+ return file ? readMigrationSql(file) : null;
+}
diff --git a/lib/supabase/missing-relation.ts b/lib/supabase/missing-relation.ts
new file mode 100644
index 0000000..9a18f01
--- /dev/null
+++ b/lib/supabase/missing-relation.ts
@@ -0,0 +1,11 @@
+/** PostgREST / Supabase error when a table is not in the schema yet */
+export function isMissingRelationError(error: unknown): boolean {
+ if (!error || typeof error !== "object") return false;
+ const e = error as { code?: string; message?: string };
+ if (e.code === "PGRST205" || e.code === "42P01") return true;
+ const message = String(e.message ?? "");
+ return (
+ message.includes("Could not find the table") ||
+ message.includes("does not exist")
+ );
+}
diff --git a/lib/types.ts b/lib/types.ts
index 04fbe85..d1b0b3f 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -163,7 +163,7 @@ export const DEFAULT_APP_SETTINGS: AppSettings = {
compressionQuality: 0.8,
nfcEnabled: true,
nfcRequireLoginToReport: true,
- comingSoonEnabled: true,
+ comingSoonEnabled: false,
comingSoonMessage: "พบกันเร็วๆนี้",
};
diff --git a/supabase/migrations/20260613193519_seed_coming_soon_and_admin_flag.sql b/supabase/migrations/20260613193519_seed_coming_soon_and_admin_flag.sql
index befafaf..8238d71 100644
--- a/supabase/migrations/20260613193519_seed_coming_soon_and_admin_flag.sql
+++ b/supabase/migrations/20260613193519_seed_coming_soon_and_admin_flag.sql
@@ -1,7 +1,7 @@
--- Enable coming soon on landing page
+-- Coming soon is off by default (admins can enable from Settings)
UPDATE public.app_settings
SET settings = COALESCE(settings, '{}'::jsonb) || jsonb_build_object(
- 'comingSoonEnabled', true,
+ 'comingSoonEnabled', false,
'comingSoonMessage', 'พบกันเร็วๆนี้'
),
updated_at = now()
@@ -10,7 +10,7 @@ WHERE id = 'default';
-- Ensure default row exists if missing
INSERT INTO public.app_settings (id, settings, updated_at)
SELECT 'default', jsonb_build_object(
- 'comingSoonEnabled', true,
+ 'comingSoonEnabled', false,
'comingSoonMessage', 'พบกันเร็วๆนี้'
), now()
WHERE NOT EXISTS (SELECT 1 FROM public.app_settings WHERE id = 'default');;
diff --git a/supabase/migrations/20260718010000_coming_soon_default_off.sql b/supabase/migrations/20260718010000_coming_soon_default_off.sql
new file mode 100644
index 0000000..0d095b6
--- /dev/null
+++ b/supabase/migrations/20260718010000_coming_soon_default_off.sql
@@ -0,0 +1,8 @@
+-- Flip coming-soon off for existing installs (was seeded true).
+-- Admins can re-enable from /admin/settings.
+UPDATE public.app_settings
+SET settings = COALESCE(settings, '{}'::jsonb) || jsonb_build_object(
+ 'comingSoonEnabled', false
+),
+updated_at = now()
+WHERE id = 'default';