From 3abbb60ec694f7a995d904ad377f8003ed860495 Mon Sep 17 00:00:00 2001
From: ignamiranda <34501347+ignamiranda@users.noreply.github.com>
Date: Sun, 23 Aug 2026 17:11:46 -0400
Subject: [PATCH] Beginner UX: soft first-run readiness card on Home (#11282)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Validated on the combined 8-PR board: home-page-static 4/4, check:dashboard-typecheck clean (220 pre-existing, all within frozen baseline — zero syntax errors), 88/88 across the board's focused suites, static gates within baseline. The soft first-run readiness card replaces the hard Home→onboarding redirect with a dismissable 4-step path. Thank you @ignamiranda!
---
.../11282-first-run-readiness-card.md | 1 +
.../dashboard/FirstRunReadinessCard.tsx | 97 +++++++++++++++++++
src/app/(dashboard)/home/page.tsx | 6 +-
src/i18n/messages/en.json | 11 ++-
tests/unit/home-page-static.test.ts | 77 +++++++++++++++
5 files changed, 187 insertions(+), 5 deletions(-)
create mode 100644 changelog.d/features/11282-first-run-readiness-card.md
create mode 100644 src/app/(dashboard)/dashboard/FirstRunReadinessCard.tsx
create mode 100644 tests/unit/home-page-static.test.ts
diff --git a/changelog.d/features/11282-first-run-readiness-card.md b/changelog.d/features/11282-first-run-readiness-card.md
new file mode 100644
index 0000000000..e2899a54e5
--- /dev/null
+++ b/changelog.d/features/11282-first-run-readiness-card.md
@@ -0,0 +1 @@
+- **feat(dashboard):** replace the hard Home → onboarding redirect with a dismissable first-run readiness card so returning users can stay on Home while new users still get a clear 4-step path ([#11282](https://github.com/diegosouzapw/OmniRoute/pull/11282))
diff --git a/src/app/(dashboard)/dashboard/FirstRunReadinessCard.tsx b/src/app/(dashboard)/dashboard/FirstRunReadinessCard.tsx
new file mode 100644
index 0000000000..43eb5b6bc9
--- /dev/null
+++ b/src/app/(dashboard)/dashboard/FirstRunReadinessCard.tsx
@@ -0,0 +1,97 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import Link from "next/link";
+import { useTranslations } from "next-intl";
+
+const DISMISS_STORAGE_KEY = "omniroute-first-run-readiness-dismissed";
+
+type FirstRunReadinessCardProps = {
+ setupComplete: boolean;
+};
+
+/**
+ * Soft entry path for first-run users. Replaces the hard redirect to
+ * /dashboard/onboarding so returning users can dismiss and stay on Home.
+ */
+export default function FirstRunReadinessCard({ setupComplete }: FirstRunReadinessCardProps) {
+ const t = useTranslations("home");
+ const [visible, setVisible] = useState(false);
+
+ useEffect(() => {
+ if (setupComplete) {
+ setVisible(false);
+ return;
+ }
+ try {
+ setVisible(!localStorage.getItem(DISMISS_STORAGE_KEY));
+ } catch {
+ setVisible(true);
+ }
+ }, [setupComplete]);
+
+ if (!visible || setupComplete) return null;
+
+ const dismiss = () => {
+ try {
+ localStorage.setItem(DISMISS_STORAGE_KEY, "true");
+ } catch {
+ // ignore storage failures; still hide for this session
+ }
+ setVisible(false);
+ };
+
+ const steps = [
+ t("readinessStep1"),
+ t("readinessStep2"),
+ t("readinessStep3"),
+ t("readinessStep4"),
+ ];
+
+ return (
+
+
+
+
+ {t("readinessEyebrow")}
+
+
+ {t("readinessTitle")}
+
+
+ {t("readinessSubtitle")}
+
+
+ {steps.map((label, index) => (
+ -
+
+ {index + 1}
+
+ {label}
+
+ ))}
+
+
+
+ {t("readinessContinue")}
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(dashboard)/home/page.tsx b/src/app/(dashboard)/home/page.tsx
index bc10df88f4..2c2fa62405 100644
--- a/src/app/(dashboard)/home/page.tsx
+++ b/src/app/(dashboard)/home/page.tsx
@@ -1,4 +1,3 @@
-import { redirect } from "next/navigation";
import { getMachineId } from "@/shared/utils/machine";
import { getSettings } from "@/lib/localDb";
import HomePageClient from "../dashboard/HomePageClient";
@@ -7,19 +6,18 @@ import KimiSponsorBanner from "../dashboard/KimiSponsorBanner";
import CheaperInferenceSponsorBanner from "../dashboard/CheaperInferenceSponsorBanner";
import VscodeCopilotBanner from "../dashboard/VscodeCopilotBanner";
import NewsBanner from "../dashboard/NewsBanner";
+import FirstRunReadinessCard from "../dashboard/FirstRunReadinessCard";
export const dynamic = "force-dynamic";
export default async function HomePage() {
const settings = await getSettings();
- if (!settings.setupComplete) {
- redirect("/dashboard/onboarding");
- }
const machineId = await getMachineId();
const isBootstrapped = process.env.OMNIROUTE_BOOTSTRAPPED === "true";
return (
<>
{isBootstrapped && }
+
diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json
index 12e1f03e47..4aaaf250cc 100644
--- a/src/i18n/messages/en.json
+++ b/src/i18n/messages/en.json
@@ -1874,7 +1874,16 @@
"directDownloadHint": "Or download the respective installer format directly:",
"releaseNotes": "Release Notes",
"readMore": "Read More",
- "noAuthLabel": "No Auth"
+ "noAuthLabel": "No Auth",
+ "readinessEyebrow": "Get ready to route",
+ "readinessTitle": "Send your first request",
+ "readinessSubtitle": "Four small steps. OmniRoute checks readiness as you go.",
+ "readinessStep1": "Connect a provider",
+ "readinessStep2": "Configure endpoint authentication",
+ "readinessStep3": "Copy your endpoint",
+ "readinessStep4": "Send a test request",
+ "readinessContinue": "Continue setup",
+ "readinessDismiss": "Dismiss for now"
},
"analytics": {
"title": "Analytics",
diff --git a/tests/unit/home-page-static.test.ts b/tests/unit/home-page-static.test.ts
new file mode 100644
index 0000000000..3e4ffe62e4
--- /dev/null
+++ b/tests/unit/home-page-static.test.ts
@@ -0,0 +1,77 @@
+import { describe, it } from "node:test";
+import assert from "node:assert/strict";
+import { readFileSync } from "node:fs";
+import { dirname, join } from "node:path";
+import { fileURLToPath } from "node:url";
+
+const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "../..");
+
+function readHomePage(): string {
+ return readFileSync(
+ join(repoRoot, "src/app/(dashboard)/home/page.tsx"),
+ "utf8",
+ );
+}
+
+function readReadinessCard(): string {
+ return readFileSync(
+ join(repoRoot, "src/app/(dashboard)/dashboard/FirstRunReadinessCard.tsx"),
+ "utf8",
+ );
+}
+
+function readEnKeys(): string[] {
+ const en = JSON.parse(
+ readFileSync(join(repoRoot, "src/i18n/messages/en.json"), "utf8"),
+ ) as { home: Record };
+ return Object.keys(en.home);
+}
+
+describe("home page first-run readiness card", () => {
+ it("does not hard-redirect incomplete setup to onboarding", () => {
+ const source = readHomePage();
+ assert.doesNotMatch(source, /redirect\(["']\/dashboard\/onboarding["']\)/);
+ assert.match(source, /FirstRunReadinessCard/);
+ assert.match(source, /setupComplete=\{Boolean\(settings\.setupComplete\)\}/);
+ });
+
+ it("keeps the readiness card dismissable via localStorage", () => {
+ const source = readReadinessCard();
+ assert.match(source, /omniroute-first-run-readiness-dismissed/);
+ assert.match(source, /localStorage/);
+ assert.match(source, /readinessContinue/);
+ assert.match(source, /readinessDismiss/);
+ });
+
+ it("uses t() keys for readiness copy", () => {
+ const source = readReadinessCard();
+ for (const key of [
+ "readinessEyebrow",
+ "readinessTitle",
+ "readinessSubtitle",
+ "readinessStep1",
+ "readinessStep2",
+ "readinessStep3",
+ "readinessStep4",
+ ]) {
+ assert.match(source, new RegExp(key));
+ }
+ });
+
+ it("new i18n keys exist in en.json home namespace", () => {
+ const keys = readEnKeys();
+ for (const key of [
+ "readinessEyebrow",
+ "readinessTitle",
+ "readinessSubtitle",
+ "readinessStep1",
+ "readinessStep2",
+ "readinessStep3",
+ "readinessStep4",
+ "readinessContinue",
+ "readinessDismiss",
+ ]) {
+ assert.ok(keys.includes(key), `Missing home.${key}`);
+ }
+ });
+});