Skip to content
1 change: 1 addition & 0 deletions config/quality/e2e-timings.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"memory-qdrant-routes.spec.ts": 360,
"memory-settings.spec.ts": 193,
"navigation.spec.ts": 28,
"onboarding-first-use.spec.ts": 268,
"playground-compare.spec.ts": 138,
"playground-studio.spec.ts": 101,
"protocol-visibility.spec.ts": 25,
Expand Down
22 changes: 0 additions & 22 deletions config/quality/i18n-placeholder-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,9 @@
"common.configuredProvidersHint",
"common.configuredProvidersLabel",
"common.confirmDbImportDesc",
"common.confirmPasswordPlaceholder",
"common.defaultStrategyDesc",
"common.disableCloudTitle",
"common.domainPlaceholder",
"common.doneDesc",
"common.embeddingsDesc",
"common.enableCloudTitle",
"common.error_description",
Expand Down Expand Up @@ -76,7 +74,6 @@
"common.protocolToolsLabel",
"common.protocolsDescription",
"common.protocolsTitle",
"common.providerDesc",
"common.providerHealthStatusAria",
"common.providerLabel",
"common.providerMaxRetriesAria",
Expand All @@ -93,16 +90,13 @@
"common.searchProvidersHeading",
"common.sectionDescription",
"common.sectionTitle",
"common.securityDesc",
"common.stickyLimitDesc",
"common.tabsAria",
"common.templateLoadHint",
"common.testDesc",
"common.textToSpeechDesc",
"common.trackMetricsDesc",
"common.videoDesc",
"common.webSearchDesc",
"common.welcomeDesc",
"common.zedImportHint",
"endpoint.requestBody",
"providers.apiFormatLabel",
Expand All @@ -111,19 +105,11 @@
"providers.bailianBaseUrlHint",
"providers.blackboxWebCookieHint",
"providers.blackboxWebCookiePlaceholder",
"providers.customUserAgentHint",
"providers.customUserAgentLabel",
"providers.databricksBaseUrlHint",
"providers.excludedModelsHint",
"providers.excludedModelsLabel",
"providers.excludedModelsPlaceholder",
"providers.extraApiKeysHint",
"providers.extraApiKeysLabel",
"providers.grokWebCookieHint",
"providers.grokWebCookiePlaceholder",
"providers.herokuBaseUrlHint",
"providers.imagesShortLabel",
"providers.localProviderApiKeyOptionalHint",
"providers.localProviderBaseUrlHint",
"providers.museSparkWebCookieHint",
"providers.museSparkWebCookiePlaceholder",
Expand All @@ -132,19 +118,11 @@
"providers.refreshOauthTokenTitle",
"providers.regionHint",
"providers.regionLabel",
"providers.routingTagsHint",
"providers.routingTagsLabel",
"providers.routingTagsPlaceholder",
"providers.searchEngineIdHint",
"providers.searchEngineIdLabel",
"providers.searchProviderDesc",
"providers.searchProvidersHeading",
"providers.searxngBaseUrlHint",
"providers.sessionCookieLabel",
"providers.snowflakeBaseUrlHint",
"providers.tagGroupHint",
"providers.tagGroupLabel",
"providers.tagGroupPlaceholder",
"providers.xiaomiMimoBaseUrlHint",
"providers.zedImportHint",
"settings.cliproxyapiHealthLabel",
Expand Down
4 changes: 3 additions & 1 deletion docs/getting-started/FIRST_10_MINUTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ Open `http://localhost:20128`. What you see depends on `INITIAL_PASSWORD`:
| not set | The onboarding wizard (`/dashboard/onboarding`) asks you to set a dashboard password (or explicitly continue without one for local-only use). |
| set | **The onboarding wizard is skipped.** On first settings read OmniRoute stores `setupComplete=true` and `requireLogin=true` (`src/lib/db/settings.ts`), so you land on `/login` and sign in with that password. |

The bootstrap runs once. After signing in you can still run the guided setup at `/dashboard/onboarding?rerun=1`: it keeps the password you already have and walks through adding a provider, validating the credential, choosing a model, a test request, the client configuration and the first request in the logs. `PATCH /api/settings` with `{"setupComplete": false}` also brings the wizard back, and the setting is no longer re-forced on the next read.

If the password is the `.env.example` placeholder `CHANGEME`, change it right away in **Settings → Security** (`/dashboard/settings/security`). Forgot it? Run `omniroute-reset-password` (from source: `node bin/reset-password.mjs`).

**Confirm:** you see the dashboard home while logged in.
Expand Down Expand Up @@ -159,7 +161,7 @@ Model: <model-id-from-/v1/models>

## Known caveats

- Setting `INITIAL_PASSWORD` skips onboarding entirely (see the table above). Use it for headless deploys, not for a first local try if you want the wizard.
- Setting `INITIAL_PASSWORD` skips the onboarding wizard on first boot (see the table above). Use it for headless deploys; to get the guided setup anyway, sign in and open `/dashboard/onboarding?rerun=1`.
- Data location: `DATA_DIR` when set; otherwise an existing `~/.omniroute` is kept, then `%APPDATA%\omniroute` on Windows, `$XDG_CONFIG_HOME/omniroute` when `XDG_CONFIG_HOME` is set, else `~/.omniroute` (`src/lib/dataPaths.ts`). Back it up before upgrading: [BACKUP_RESTORE.md](../ops/BACKUP_RESTORE.md).
- `:next` Docker images change under you. Upgrades, pinning and rollback: [MIGRATION_GUIDE.md](../guides/MIGRATION_GUIDE.md).
- Binding to `127.0.0.1` (as in the Docker example) keeps the gateway local. Before exposing it, read [SECURITY.md](../../SECURITY.md) and [VM_DEPLOYMENT_GUIDE.md](../ops/VM_DEPLOYMENT_GUIDE.md).
Expand Down
4 changes: 3 additions & 1 deletion docs/i18n/pt-BR/docs/getting-started/FIRST_10_MINUTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ Abra `http://localhost:20128`. O que aparece depende de `INITIAL_PASSWORD`:
| não definida | O assistente inicial (`/dashboard/onboarding`) pede para você definir uma senha do painel (ou continuar explicitamente sem senha, para uso apenas local). |
| definida | **O assistente inicial é pulado.** Na primeira leitura das configurações o OmniRoute grava `setupComplete=true` e `requireLogin=true` (`src/lib/db/settings.ts`), então você cai em `/login` e entra com essa senha. |

Essa inicialização acontece uma única vez. Depois de entrar, você ainda pode fazer a configuração guiada em `/dashboard/onboarding?rerun=1`: ela mantém a senha que você já tem e passa por adicionar um provedor, validar a credencial, escolher um modelo, uma requisição de teste, a configuração do cliente e a primeira requisição nos logs. `PATCH /api/settings` com `{"setupComplete": false}` também traz o assistente de volta, e o valor não é mais forçado de novo na leitura seguinte.

Se a senha for o valor de exemplo `CHANGEME` do `.env.example`, troque imediatamente em **Configurações → Segurança** (`/dashboard/settings/security`). Esqueceu a senha? Rode `omniroute-reset-password` (a partir do código-fonte: `node bin/reset-password.mjs`).

**Confirme:** você vê a página inicial do painel, logado.
Expand Down Expand Up @@ -159,7 +161,7 @@ Model: <model-id-from-/v1/models>

## Ressalvas conhecidas

- Definir `INITIAL_PASSWORD` pula o assistente inicial por completo (veja a tabela acima). Use em implantações headless, não num primeiro teste local se você quiser o assistente.
- Definir `INITIAL_PASSWORD` pula o assistente inicial na primeira inicialização (veja a tabela acima). Use em implantações headless; para ter a configuração guiada mesmo assim, entre e abra `/dashboard/onboarding?rerun=1`.
- Local dos dados: `DATA_DIR` quando definido; senão um `~/.omniroute` já existente é mantido; depois `%APPDATA%\omniroute` no Windows, `$XDG_CONFIG_HOME/omniroute` quando `XDG_CONFIG_HOME` está definido, e por fim `~/.omniroute` (`src/lib/dataPaths.ts`). Faça backup antes de atualizar: [Backup e restauração](../ops/BACKUP_RESTORE.md).
- Imagens Docker `:next` mudam sem aviso. Atualização, fixação de versão e rollback: [Guia de atualização e migração](../guides/MIGRATION_GUIDE.md).
- Publicar a porta em `127.0.0.1` (como no exemplo Docker) mantém o gateway local. Antes de expor, leia o [SECURITY.md](../../SECURITY.md) e o [Guia de implantação em VM](../ops/VM_DEPLOYMENT_GUIDE.md).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"use client";

import { useTranslations } from "next-intl";
import type { ActionableErrorGuide } from "@/shared/utils/actionableError";

interface ActionableErrorCalloutProps {
/** What happened — the readable headline (announced via role="alert"). */
message: string;
guide: ActionableErrorGuide;
onRetry?: () => void;
/** Optional second action, e.g. "Back to provider" for a rejected credential. */
secondaryAction?: { label: string; onClick: () => void };
}

/**
* Error block of the first-use flow: what happened, why, how to fix it, whether trying
* again can help, and a link to the guide. Only the headline carries role="alert" so
* screen readers announce the failure once; the guidance stays readable below it.
*/
export function ActionableErrorCallout({
message,
guide,
onRetry,
secondaryAction,
}: ActionableErrorCalloutProps) {
const t = useTranslations("onboarding");
return (
<div
data-testid="actionable-error"
data-error-kind={guide.kind}
className="space-y-2 rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-left animate-in fade-in duration-200"
>
<p role="alert" className="text-sm font-medium text-red-400 break-words">
{message}
</p>
<dl className="space-y-1 text-xs text-text-muted">
<div>
<dt className="inline font-semibold text-text-main">{t("errorGuide.whyLabel")}: </dt>
<dd className="inline">{t(`errorGuide.${guide.kind}.why`)}</dd>
</div>
<div>
<dt className="inline font-semibold text-text-main">{t("errorGuide.fixLabel")}: </dt>
<dd className="inline">{t(`errorGuide.${guide.kind}.fix`)}</dd>
</div>
</dl>
<p className="text-xs text-text-muted">
{guide.retryable ? t("errorGuide.retryPossible") : t("errorGuide.retryAfterFix")}
</p>
<div className="flex flex-wrap items-center gap-x-4 gap-y-1">
{onRetry && (
<button
type="button"
onClick={onRetry}
className="text-xs font-medium text-text-main underline cursor-pointer"
>
{t("retry")}
</button>
)}
{secondaryAction && (
<button
type="button"
onClick={secondaryAction.onClick}
className="text-xs font-medium text-text-main underline cursor-pointer"
>
{secondaryAction.label}
</button>
)}
<a
href={guide.docsHref}
target="_blank"
rel="noreferrer"
className="text-xs text-primary hover:underline"
>
{t("errorGuide.docsLink")}
</a>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use client";

/**
* Step indicator of the onboarding wizard. The connectors shrink (flex-1 with a small
* minimum) instead of using fixed widths, so six steps fit the card from 320 px up and
* never push the page into horizontal scroll at intermediate widths (audit C-09).
*/
export function WizardProgress({ stepCount, current }: { stepCount: number; current: number }) {
return (
<ol className="mb-8 flex w-full min-w-0 items-center justify-center">
{Array.from({ length: stepCount }, (_, i) => (
<li
key={i}
aria-current={i === current ? "step" : undefined}
className={`flex min-w-0 items-center ${i < stepCount - 1 ? "flex-1" : ""}`}
>
<div
className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-sm font-semibold transition-all duration-300 ${
i < current
? "bg-green-500/20 text-green-400"
: i === current
? "bg-primary/20 text-primary ring-2 ring-primary/40"
: "bg-white/5 text-text-muted"
}`}
>
{i < current ? (
<span className="material-symbols-outlined text-[16px]" aria-hidden="true">
check
</span>
) : (
i + 1
)}
</div>
{i < stepCount - 1 && (
<div
className={`mx-1 h-0.5 min-w-2 flex-1 rounded-full transition-colors sm:mx-2 ${
i < current ? "bg-green-500/40" : "bg-white/10"
}`}
/>
)}
</li>
))}
</ol>
);
}
Loading
Loading