fix(client): a foreign 404 page is a routing problem, not a backend error (#1386)
A rehosted UI whose API requests land on its own static host (or a reverse proxy with no API route) got that host's 404 page back, and we echoed it verbatim — the reporter saw 'NOT_FOUND bom1::...' and had no way to know their requests were reaching the wrong server. The backend now stamps x-omnivoice-backend on every response, exposed through CORS, so the client can tell 'the backend answered 404' from 'something else answered 404' without guessing at body shape. A 404 in any other voice is reported as a routing problem, naming the URL that answered and where to fix it. The message goes through the same i18n helper as the other backend-diagnosis copy, in all 21 locales.
This commit is contained in:
@@ -29,6 +29,7 @@ The bundled TTS model package (`pyproject.toml`) is versioned independently.
|
||||
|
||||
### Fixed
|
||||
|
||||
- A UI whose API requests land on the wrong host — a rehosted frontend, or a reverse proxy with no API route — no longer echoes that host's raw 404 page as the error. It now says the responding server is not an OmniVoice backend and points at the Backend URL setting and the proxy route. (#1385)
|
||||
- Building the GGUF engine from source produced a binary that died on its very first spawn ("libggml.so.0: cannot open shared object file") — the build script deleted the shared libraries it had just linked against. It now ships them next to the binary on every platform, and the backend puts that folder on the loader path — thanks @vanderlpp! (#1348)
|
||||
- The GGUF engine's hard 120-second per-render kill switch — which was reaping legitimate CPU-only renders mid-synthesis — is now 600s, tunable via `OMNIVOICE_GGUF_GENERATE_TIMEOUT_S`, and the timeout error names that setting — thanks @vanderlpp! (#1348)
|
||||
- Every subprocess TTS engine would have turned a stereo render into noise: the mono downmix always averaged axis 0, which is time rather than channels for channels-last audio. Unreachable today since every engine returns mono, fixed in all five before it isn't. (#1328)
|
||||
|
||||
+56
-1
@@ -1049,6 +1049,52 @@ class NetworkAccessMiddleware:
|
||||
return await self.app(scope, receive, send)
|
||||
|
||||
|
||||
#: Header stamped on EVERY response so a client can tell this backend apart
|
||||
#: from whatever else might answer at the same URL (#1385). A rehosted UI
|
||||
#: whose API requests land on a static host or a proxy with no API route gets
|
||||
#: that host's 404 page; the frontend needs an authoritative "this really is
|
||||
#: an OmniVoice backend" signal rather than guessing from the body shape,
|
||||
#: since a proxy can return JSON too. Value is the version, which is also
|
||||
#: useful when a desktop app talks to an older remote backend.
|
||||
BACKEND_MARKER_HEADER = "x-omnivoice-backend"
|
||||
|
||||
|
||||
class BackendMarkerMiddleware:
|
||||
"""Stamp ``x-omnivoice-backend: <version>`` on every response.
|
||||
|
||||
Pure ASGI, same reasoning as the gates below it: wrapping only the
|
||||
``http.response.start`` message keeps streaming bodies streaming.
|
||||
"""
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
if scope["type"] != "http":
|
||||
return await self.app(scope, receive, send)
|
||||
|
||||
async def send_with_marker(message):
|
||||
if message["type"] == "http.response.start":
|
||||
MutableHeaders(scope=message).setdefault(
|
||||
BACKEND_MARKER_HEADER, _backend_marker_value()
|
||||
)
|
||||
await send(message)
|
||||
|
||||
return await self.app(scope, receive, send_with_marker)
|
||||
|
||||
|
||||
def _backend_marker_value() -> str:
|
||||
# ImportError only: the marker's JOB is to be present, so a frozen build
|
||||
# that cannot import the version module still answers "yes, a backend".
|
||||
# Anything else is a real defect and should surface, not be masked.
|
||||
try:
|
||||
from core.version import APP_VERSION
|
||||
|
||||
return str(APP_VERSION)
|
||||
except ImportError:
|
||||
return "unknown"
|
||||
|
||||
|
||||
class BearerKeyMiddleware:
|
||||
"""When OMNIVOICE_API_KEY is set, non-loopback clients must present it on
|
||||
every HTTP + WebSocket request: ``Authorization: Bearer <key>``,
|
||||
@@ -1135,7 +1181,10 @@ app.add_middleware(
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
expose_headers=["Content-Disposition"],
|
||||
# The marker must be readable cross-origin too — a browser UI served from
|
||||
# another origin is exactly the deployment that needs to tell "the backend
|
||||
# answered 404" from "something else answered 404" (#1385).
|
||||
expose_headers=["Content-Disposition", BACKEND_MARKER_HEADER],
|
||||
)
|
||||
|
||||
# Registered AFTER CORS so CORS remains the outermost layer (CORS headers are
|
||||
@@ -1151,6 +1200,12 @@ app.add_middleware(NetworkAccessMiddleware)
|
||||
# keyed non-loopback client must reach them.
|
||||
app.add_middleware(BearerKeyMiddleware)
|
||||
|
||||
# Registered LAST, which in Starlette means OUTERMOST — so the marker lands on
|
||||
# every response, including the two gates' 401s above and StaticFiles' bare
|
||||
# "Not Found". Its absence is what lets a client conclude "whatever answered
|
||||
# me is not an OmniVoice backend" (#1385).
|
||||
app.add_middleware(BackendMarkerMiddleware)
|
||||
|
||||
# Register canonical audio MIME types before any StaticFiles mount.
|
||||
# Python's `mimetypes.guess_type()` returns `audio/x-wav` for `.wav` and
|
||||
# `audio/x-flac` for `.flac` on most platforms — these are vendor-experimental
|
||||
|
||||
@@ -113,6 +113,80 @@ describe('apiFetch 401 routing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('apiFetch 404 from a non-OmniVoice server (#1385)', () => {
|
||||
// A rehosted UI (static host, reverse proxy) whose API requests land on the
|
||||
// wrong host gets that host's 404 page back. Echoing it ("NOT_FOUND
|
||||
// bom1::…") is useless — the error must say where requests are going.
|
||||
let realFetch: typeof globalThis.fetch;
|
||||
beforeEach(() => {
|
||||
realFetch = globalThis.fetch;
|
||||
});
|
||||
afterEach(() => {
|
||||
globalThis.fetch = realFetch;
|
||||
});
|
||||
|
||||
const stub404 = (body: string, headers: Record<string, string> = {}) =>
|
||||
vi.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
headers: { get: (k: string) => headers[k.toLowerCase()] ?? null },
|
||||
text: async () => body,
|
||||
}),
|
||||
) as any;
|
||||
|
||||
const thrownBy = async (body: string, headers?: Record<string, string>) => {
|
||||
globalThis.fetch = stub404(body, headers);
|
||||
const { apiFetch } = await import('./client');
|
||||
try {
|
||||
await apiFetch('/generate');
|
||||
} catch (e) {
|
||||
return e as Error;
|
||||
}
|
||||
throw new Error('expected apiFetch to throw');
|
||||
};
|
||||
|
||||
it('names the misrouted host instead of echoing its 404 page', async () => {
|
||||
const err = await thrownBy(
|
||||
'The page could not be found\n\nNOT_FOUND\n\nbom1::2lhsl-1786000655272',
|
||||
);
|
||||
expect(err.message).toMatch(/not an OmniVoice backend/);
|
||||
expect(err.message).toMatch(/Backend URL in Settings/);
|
||||
expect(err.message).not.toMatch(/bom1/);
|
||||
});
|
||||
|
||||
it("keeps the plain message for the backend's own JSON 404", async () => {
|
||||
const err = await thrownBy(JSON.stringify({ detail: 'Voice not found' }));
|
||||
expect(err.message).toMatch(/404 Not Found: Voice not found/);
|
||||
expect(err.message).not.toMatch(/not an OmniVoice backend/);
|
||||
});
|
||||
|
||||
it('treats Starlette StaticFiles\' plain-text "Not Found" as the backend speaking', async () => {
|
||||
// Mounted StaticFiles apps answer text/plain "Not Found" — the one
|
||||
// non-JSON 404 the backend itself produces. Not a routing problem.
|
||||
const err = await thrownBy('Not Found');
|
||||
expect(err.message).not.toMatch(/not an OmniVoice backend/);
|
||||
});
|
||||
|
||||
it("believes the backend's marker header over any body shape", async () => {
|
||||
// The header is what makes this authoritative rather than a guess: a
|
||||
// backend 404 whose body looks like nothing we recognise is still a
|
||||
// backend 404.
|
||||
const err = await thrownBy('<html>weird proxy rewrite</html>', {
|
||||
'x-omnivoice-backend': '0.4.3',
|
||||
});
|
||||
expect(err.message).not.toMatch(/not an OmniVoice backend/);
|
||||
});
|
||||
|
||||
it('is not fooled by a proxy that imitates a JSON error body', async () => {
|
||||
// CodeRabbit: an unmarked `{"error": …}` 404 is a foreign server — the
|
||||
// backend never answers 404 with an `error` key, only `detail`.
|
||||
const err = await thrownBy(JSON.stringify({ error: 'Not Found' }));
|
||||
expect(err.message).toMatch(/not an OmniVoice backend/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('_parseDeepLinkCredentials', () => {
|
||||
it('reads the API key from the fragment (not the query) and scrubs it', () => {
|
||||
const r = _parseDeepLinkCredentials('https://h:3900/#api_key=SECRET');
|
||||
|
||||
@@ -22,6 +22,7 @@ import { scrubText } from '../utils/scrub.js';
|
||||
import { deploymentMode } from '../utils/deploymentMode.ts';
|
||||
import {
|
||||
lastBackendContact,
|
||||
misroutedBackendMessage,
|
||||
recordBackendContact,
|
||||
unreachableBackendMessage,
|
||||
} from '../utils/backendContact.ts';
|
||||
@@ -172,13 +173,34 @@ export function apiUrl(path?: string): string {
|
||||
return path.startsWith('http') ? path : `${API}${path.startsWith('/') ? '' : '/'}${path}`;
|
||||
}
|
||||
|
||||
async function readError(res: Response): Promise<string> {
|
||||
// Stamped on EVERY response by the backend's BackendMarkerMiddleware and
|
||||
// exposed cross-origin, so its presence is AUTHORITATIVE: this really is an
|
||||
// OmniVoice backend answering, whatever the body looks like (#1385).
|
||||
const BACKEND_MARKER_HEADER = 'x-omnivoice-backend';
|
||||
|
||||
// `backendShaped` — did this response come from an OmniVoice backend?
|
||||
//
|
||||
// The marker header settles it outright. Body shape is the fallback for a
|
||||
// backend older than the header (a desktop app pointed at a remote box that
|
||||
// hasn't been updated): every backend error route answers JSON with
|
||||
// `detail`/`error`, and Starlette's mounted StaticFiles answer plain-text
|
||||
// "Not Found" — the one non-JSON backend voice. That fallback is a heuristic,
|
||||
// which is exactly why the header exists: a proxy CAN return
|
||||
// `{"error":"Not Found"}` and impersonate the shape.
|
||||
async function readError(res: Response): Promise<{ detail: unknown; backendShaped: boolean }> {
|
||||
const marked = Boolean(res.headers?.get?.(BACKEND_MARKER_HEADER));
|
||||
const text = await res.text().catch(() => '');
|
||||
try {
|
||||
const j = JSON.parse(text);
|
||||
return j.detail || j.error || text || res.statusText;
|
||||
// `detail` is FastAPI/Starlette's own error key — including the
|
||||
// `{"detail":"Not Found"}` an unrouted path produces. `error` is used by
|
||||
// a few 4xx/5xx handlers but never for a 404, so an unmarked
|
||||
// `{"error":…}` 404 is a foreign server, not an old backend.
|
||||
if (j.detail) return { detail: j.detail, backendShaped: true };
|
||||
if (j.error) return { detail: j.error, backendShaped: marked };
|
||||
return { detail: text || res.statusText, backendShaped: marked };
|
||||
} catch {
|
||||
return text || res.statusText;
|
||||
return { detail: text || res.statusText, backendShaped: marked || text.trim() === 'Not Found' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -392,7 +414,18 @@ export async function apiFetch(path: string, opts: RequestInit = {}): Promise<Re
|
||||
}
|
||||
if (!res.ok) {
|
||||
// An HTTP error means the backend *did* respond — never retry it.
|
||||
const detail = await readError(res);
|
||||
const { detail, backendShaped } = await readError(res);
|
||||
// #1385: a 404 in some other server's voice means the request never
|
||||
// reached an OmniVoice backend at all — a static host's catch-all page
|
||||
// or a reverse proxy with no route for this path. Echoing that page
|
||||
// ("NOT_FOUND bom1::…") sends the user chasing a page that never
|
||||
// existed; name the actual problem instead: where requests are going.
|
||||
if (res.status === 404 && !backendShaped) {
|
||||
throw new ApiError(misroutedBackendMessage(apiUrl(path)), {
|
||||
status: res.status,
|
||||
detail,
|
||||
});
|
||||
}
|
||||
// 401 on a remote device: route to the right gate by reading the detail.
|
||||
// "API key required" (BearerKeyMiddleware, OMNIVOICE_API_KEY) vs anything
|
||||
// else, i.e. "PIN required" (NetworkAccessMiddleware). Both are 401; the
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"contact_recent": "كان يستجيب قبل {{ago}} ثم توقف عن الاستجابة — على الأرجح أنه انهار أو تم إنهاؤه في منتصف الطلب.",
|
||||
"contact_never": "لم يستجب إطلاقًا في هذه الجلسة — ربما لم يبدأ التشغيل أصلًا.",
|
||||
"dev": "لا يمكن الوصول إلى خلفية OmniVoice المحلية. {{contact}} في `bun run dev` تعمل الخلفية بإعادة التحميل التلقائي، فأي تغيير في ملف — بما في ذلك الحفظ أثناء تنفيذ طلب — يعيد تشغيلها ويقطع الاتصال. أعد المحاولة أولًا؛ فإن كان الأمر إعادة تحميل فسينجح. وإن استمر الفشل، فافحص الطرفية التي تشغّل `bun run dev` بحثًا عن تتبّع Python أو رسالة خروج، وكذلك omnivoice.log في مجلد بيانات OmniVoice.",
|
||||
"server": "تعذّر الوصول إلى خادم الواجهة الخلفية لـ OmniVoice. {{contact}} راجع سجلات الخادم لمعرفة السبب (مثل `docker logs <container>` أو `journalctl`) — ولاحظ أنه إذا كان Docker يقدّم هذه الصفحة، فقد تتوقف الصفحة نفسها مع الواجهة الخلفية."
|
||||
"server": "تعذّر الوصول إلى خادم الواجهة الخلفية لـ OmniVoice. {{contact}} راجع سجلات الخادم لمعرفة السبب (مثل `docker logs <container>` أو `journalctl`) — ولاحظ أنه إذا كان Docker يقدّم هذه الصفحة، فقد تتوقف الصفحة نفسها مع الواجهة الخلفية.",
|
||||
"misrouted": "الخادم الذي يستجيب على {{url}} ليس خادم OmniVoice الخلفي — لقد أعاد صفحة 404 الخاصة به. طلبات API تصل إلى المضيف الخطأ: تحقّق من عنوان الخادم الخلفي في الإعدادات ← المشاركة، أو من مسار مسارات API في الوكيل العكسي إن كان يقدّم هذه الواجهة."
|
||||
},
|
||||
"nav": {
|
||||
"clone": "استنساخ",
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"contact_recent": "Es hat vor {{ago}} noch geantwortet und dann aufgehört zu reagieren — höchstwahrscheinlich ist es mitten in einer Anfrage abgestürzt oder wurde beendet.",
|
||||
"contact_never": "Es hat in dieser Sitzung überhaupt nicht geantwortet — möglicherweise ist es nie gestartet.",
|
||||
"dev": "Der lokale OmniVoice-Dienst ist nicht erreichbar. {{contact}} Bei `bun run dev` läuft das Backend mit Auto-Reload: jede Dateiänderung — auch ein Speichern während einer laufenden Anfrage — startet es neu und bricht die Verbindung ab. Wiederholen Sie die Aktion zuerst; war es ein Reload, funktioniert es einfach. Bleibt es fehlerhaft, prüfen Sie das Terminal mit `bun run dev` auf einen Python-Traceback oder eine Exit-Meldung und omnivoice.log in Ihrem OmniVoice-Datenordner.",
|
||||
"server": "Der OmniVoice-Backend-Server ist nicht erreichbar. {{contact}} Prüfe die Server-Logs auf die Ursache (z. B. `docker logs <container>` oder `journalctl`) — und beachte: Wenn Docker diese Seite ausliefert, kann die Seite selbst zusammen mit dem Backend ausfallen."
|
||||
"server": "Der OmniVoice-Backend-Server ist nicht erreichbar. {{contact}} Prüfe die Server-Logs auf die Ursache (z. B. `docker logs <container>` oder `journalctl`) — und beachte: Wenn Docker diese Seite ausliefert, kann die Seite selbst zusammen mit dem Backend ausfallen.",
|
||||
"misrouted": "Der Server, der {{url}} beantwortet, ist kein OmniVoice-Backend — er hat seine eigene 404-Seite zurückgegeben. Die API-Anfragen landen beim falschen Host: Prüfe die Backend-URL unter Einstellungen → Freigabe oder, falls ein Reverse-Proxy diese Oberfläche ausliefert, dessen Route für API-Pfade."
|
||||
},
|
||||
"nav": {
|
||||
"clone": "Klonen",
|
||||
|
||||
@@ -1959,7 +1959,8 @@
|
||||
"contact_recent": "It was answering {{ago}} ago and then stopped responding — it most likely crashed or was killed mid-request.",
|
||||
"contact_never": "It has not answered at all this session — it may never have started.",
|
||||
"dev": "Can't reach the local OmniVoice backend. {{contact}} In `bun run dev` the backend runs with auto-reload, so any file change — including a save while a request was in flight — restarts it and drops the connection. Retry the action first; if it was a reload, it just works. If it keeps failing, check the terminal running `bun run dev` for a Python traceback or an exit banner, and omnivoice.log in your OmniVoice data folder for the last thing the backend logged.",
|
||||
"server": "Can't reach the OmniVoice backend server. {{contact}} Check the server logs for the cause (e.g. `docker logs <container>` or `journalctl`) — and note that if Docker serves this page, the page itself can go down with the backend."
|
||||
"server": "Can't reach the OmniVoice backend server. {{contact}} Check the server logs for the cause (e.g. `docker logs <container>` or `journalctl`) — and note that if Docker serves this page, the page itself can go down with the backend.",
|
||||
"misrouted": "The server answering {{url}} is not an OmniVoice backend — it returned its own 404 page. API requests are landing on the wrong host: check the Backend URL in Settings → Sharing, or, if a reverse proxy serves this UI, its route for API paths."
|
||||
},
|
||||
"backend": {
|
||||
"restarting": "The voice backend stopped and is restarting automatically — hang tight, this takes ~15 seconds.",
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"contact_recent": "Estaba respondiendo hace {{ago}} y dejó de responder — lo más probable es que se haya bloqueado o haya sido terminado en mitad de una petición.",
|
||||
"contact_never": "No ha respondido en toda esta sesión — puede que nunca haya llegado a iniciarse.",
|
||||
"dev": "No se puede contactar con el backend local de OmniVoice. {{contact}} Con `bun run dev` el backend se ejecuta con recarga automática: cualquier cambio de archivo — incluido guardar mientras había una petición en curso — lo reinicia y corta la conexión. Reintente la acción primero; si fue una recarga, funcionará. Si sigue fallando, revise la terminal con `bun run dev` por un traceback de Python o un mensaje de salida, y omnivoice.log en su carpeta de datos de OmniVoice.",
|
||||
"server": "No se puede conectar con el servidor backend de OmniVoice. {{contact}} Revisa los registros del servidor para encontrar la causa (p. ej. `docker logs <contenedor>` o `journalctl`) — y ten en cuenta que si Docker sirve esta página, la propia página puede caerse junto con el backend."
|
||||
"server": "No se puede conectar con el servidor backend de OmniVoice. {{contact}} Revisa los registros del servidor para encontrar la causa (p. ej. `docker logs <contenedor>` o `journalctl`) — y ten en cuenta que si Docker sirve esta página, la propia página puede caerse junto con el backend.",
|
||||
"misrouted": "El servidor que responde en {{url}} no es un backend de OmniVoice: devolvió su propia página 404. Las solicitudes de la API están llegando al host equivocado: revisa la URL del backend en Configuración → Compartir o, si un proxy inverso sirve esta interfaz, su ruta para las rutas de la API."
|
||||
},
|
||||
"nav": {
|
||||
"clone": "Clonar",
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"contact_recent": "Il répondait il y a {{ago}} puis a cessé de répondre — il a très probablement planté ou a été arrêté en pleine requête.",
|
||||
"contact_never": "Il n'a pas répondu du tout durant cette session — il n'a peut-être jamais démarré.",
|
||||
"dev": "Impossible de joindre le backend OmniVoice local. {{contact}} Avec `bun run dev`, le backend tourne en rechargement automatique : toute modification de fichier — y compris une sauvegarde pendant une requête en cours — le redémarre et coupe la connexion. Réessayez d'abord l'action ; s'il s'agissait d'un rechargement, cela fonctionnera. Si l'échec persiste, consultez le terminal exécutant `bun run dev` pour un traceback Python ou un message de sortie, et omnivoice.log dans votre dossier de données OmniVoice.",
|
||||
"server": "Impossible de joindre le serveur backend OmniVoice. {{contact}} Consultez les journaux du serveur pour en trouver la cause (p. ex. `docker logs <conteneur>` ou `journalctl`) — et notez que si Docker sert cette page, la page elle-même peut tomber avec le backend."
|
||||
"server": "Impossible de joindre le serveur backend OmniVoice. {{contact}} Consultez les journaux du serveur pour en trouver la cause (p. ex. `docker logs <conteneur>` ou `journalctl`) — et notez que si Docker sert cette page, la page elle-même peut tomber avec le backend.",
|
||||
"misrouted": "Le serveur qui répond à {{url}} n'est pas un backend OmniVoice — il a renvoyé sa propre page 404. Les requêtes API arrivent sur le mauvais hôte : vérifiez l'URL du backend dans Paramètres → Partage ou, si un proxy inverse sert cette interface, sa route pour les chemins d'API."
|
||||
},
|
||||
"nav": {
|
||||
"clone": "Cloner",
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"contact_recent": "यह {{ago}} पहले तक जवाब दे रहा था और फिर जवाब देना बंद कर दिया — बहुत संभव है कि यह अनुरोध के बीच में क्रैश हो गया या बंद कर दिया गया।",
|
||||
"contact_never": "इस सत्र में इसने अभी तक कोई जवाब नहीं दिया — हो सकता है कि यह कभी शुरू ही न हुआ हो।",
|
||||
"dev": "स्थानीय OmniVoice बैकएंड से संपर्क नहीं हो पा रहा। {{contact}} `bun run dev` में बैकएंड ऑटो-रीलोड के साथ चलता है, इसलिए कोई भी फ़ाइल बदलाव — अनुरोध चलते समय सेव करना भी — उसे पुनः आरंभ कर देता है और कनेक्शन टूट जाता है। पहले क्रिया दोबारा करें; यदि रीलोड था तो यह चल जाएगा। यदि फिर भी विफल हो, तो `bun run dev` चला रहे टर्मिनल में Python ट्रेसबैक या exit संदेश देखें, और अपने OmniVoice डेटा फ़ोल्डर की omnivoice.log देखें।",
|
||||
"server": "OmniVoice बैकएंड सर्वर से संपर्क नहीं हो पा रहा है। {{contact}} कारण जानने के लिए सर्वर लॉग देखें (जैसे `docker logs <container>` या `journalctl`) — और ध्यान रखें कि अगर Docker यह पेज सर्व कर रहा है, तो बैकएंड के साथ यह पेज भी बंद हो सकता है।"
|
||||
"server": "OmniVoice बैकएंड सर्वर से संपर्क नहीं हो पा रहा है। {{contact}} कारण जानने के लिए सर्वर लॉग देखें (जैसे `docker logs <container>` या `journalctl`) — और ध्यान रखें कि अगर Docker यह पेज सर्व कर रहा है, तो बैकएंड के साथ यह पेज भी बंद हो सकता है।",
|
||||
"misrouted": "{{url}} पर उत्तर देने वाला सर्वर OmniVoice बैकएंड नहीं है — उसने अपना ही 404 पेज लौटाया। API अनुरोध गलत होस्ट पर जा रहे हैं: सेटिंग्स → साझाकरण में बैकएंड URL जाँचें, या यदि कोई रिवर्स प्रॉक्सी यह UI परोसता है, तो API पथों के लिए उसका रूट देखें।"
|
||||
},
|
||||
"nav": {
|
||||
"clone": "क्लोन",
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"contact_recent": "Backend masih merespons {{ago}} yang lalu lalu berhenti merespons — kemungkinan besar crash atau dihentikan di tengah permintaan.",
|
||||
"contact_never": "Backend sama sekali belum merespons pada sesi ini — mungkin tidak pernah berhasil dijalankan.",
|
||||
"dev": "Tidak dapat menghubungi backend OmniVoice lokal. {{contact}} Pada `bun run dev` backend berjalan dengan muat-ulang otomatis: setiap perubahan berkas — termasuk menyimpan saat ada permintaan berjalan — membuatnya dimulai ulang dan memutus koneksi. Coba ulangi tindakan dulu; jika itu muat-ulang, akan langsung berhasil. Jika terus gagal, periksa terminal `bun run dev` untuk traceback Python atau pesan keluar, dan omnivoice.log di folder data OmniVoice Anda.",
|
||||
"server": "Tidak dapat menghubungi server backend OmniVoice. {{contact}} Periksa log server untuk penyebabnya (mis. `docker logs <container>` atau `journalctl`) — dan perlu diketahui, jika Docker menyajikan halaman ini, halaman ini sendiri bisa ikut mati bersama backend."
|
||||
"server": "Tidak dapat menghubungi server backend OmniVoice. {{contact}} Periksa log server untuk penyebabnya (mis. `docker logs <container>` atau `journalctl`) — dan perlu diketahui, jika Docker menyajikan halaman ini, halaman ini sendiri bisa ikut mati bersama backend.",
|
||||
"misrouted": "Server yang menjawab {{url}} bukan backend OmniVoice — ia mengembalikan halaman 404 miliknya sendiri. Permintaan API mendarat di host yang salah: periksa URL Backend di Pengaturan → Berbagi, atau, jika reverse proxy menyajikan UI ini, rutenya untuk jalur API."
|
||||
},
|
||||
"nav": {
|
||||
"clone": "Kloning",
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"contact_recent": "Rispondeva {{ago}} fa e poi ha smesso di rispondere — molto probabilmente è andato in crash o è stato terminato durante una richiesta.",
|
||||
"contact_never": "Non ha risposto affatto in questa sessione — forse non si è mai avviato.",
|
||||
"dev": "Impossibile raggiungere il backend OmniVoice locale. {{contact}} Con `bun run dev` il backend gira con ricarica automatica: qualsiasi modifica a un file — incluso un salvataggio mentre una richiesta era in corso — lo riavvia e interrompe la connessione. Riprovi prima l'operazione; se era una ricarica, funzionerà. Se continua a fallire, controlli il terminale con `bun run dev` per un traceback Python o un messaggio di uscita, e omnivoice.log nella sua cartella dati OmniVoice.",
|
||||
"server": "Impossibile raggiungere il server backend di OmniVoice. {{contact}} Controlla i log del server per la causa (es. `docker logs <container>` o `journalctl`) — e tieni presente che se Docker serve questa pagina, la pagina stessa può cadere insieme al backend."
|
||||
"server": "Impossibile raggiungere il server backend di OmniVoice. {{contact}} Controlla i log del server per la causa (es. `docker logs <container>` o `journalctl`) — e tieni presente che se Docker serve questa pagina, la pagina stessa può cadere insieme al backend.",
|
||||
"misrouted": "Il server che risponde a {{url}} non è un backend OmniVoice: ha restituito la propria pagina 404. Le richieste API stanno arrivando all'host sbagliato: controlla l'URL del backend in Impostazioni → Condivisione oppure, se un reverse proxy serve questa interfaccia, la sua rotta per i percorsi API."
|
||||
},
|
||||
"nav": {
|
||||
"clone": "Clona",
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"contact_recent": "{{ago}}前までは応答していましたが、応答が止まりました — リクエストの途中でクラッシュしたか、強制終了された可能性が高いです。",
|
||||
"contact_never": "このセッションでは一度も応答していません — そもそも起動していない可能性があります。",
|
||||
"dev": "ローカルの OmniVoice バックエンドに接続できません。{{contact}} `bun run dev` ではバックエンドが自動リロードで動作するため、ファイルの変更(リクエスト中の保存も含む)で再起動し、接続が切れます。まず操作をやり直してください。リロードが原因ならそれで動きます。それでも失敗する場合は、`bun run dev` を実行しているターミナルの Python トレースバックや終了メッセージ、および OmniVoice データフォルダーの omnivoice.log を確認してください。",
|
||||
"server": "OmniVoice バックエンドサーバーに接続できません。{{contact}} サーバーログで原因を確認してください(例: `docker logs <container>` や `journalctl`)。なお、このページを Docker が配信している場合、バックエンドと一緒にページ自体も落ちることがあります。"
|
||||
"server": "OmniVoice バックエンドサーバーに接続できません。{{contact}} サーバーログで原因を確認してください(例: `docker logs <container>` や `journalctl`)。なお、このページを Docker が配信している場合、バックエンドと一緒にページ自体も落ちることがあります。",
|
||||
"misrouted": "{{url}} に応答しているサーバーは OmniVoice バックエンドではありません — そのサーバー自身の 404 ページが返されました。API リクエストが誤ったホストに届いています。設定 → 共有 でバックエンド URL を確認するか、リバースプロキシがこの UI を配信している場合は API パスのルート設定を確認してください。"
|
||||
},
|
||||
"nav": {
|
||||
"clone": "複製",
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"contact_recent": "{{ago}} 전까지는 응답하다가 응답을 멈췄습니다 — 요청 처리 중에 크래시했거나 강제 종료되었을 가능성이 큽니다.",
|
||||
"contact_never": "이번 세션에서 한 번도 응답하지 않았습니다 — 아예 시작되지 않았을 수 있습니다.",
|
||||
"dev": "로컬 OmniVoice 백엔드에 연결할 수 없습니다. {{contact}} `bun run dev`에서는 백엔드가 자동 리로드로 실행되므로, 파일 변경(요청 도중 저장 포함)이 백엔드를 재시작하고 연결을 끊습니다. 먼저 작업을 다시 시도하세요. 리로드였다면 바로 됩니다. 계속 실패하면 `bun run dev`를 실행한 터미널의 Python 트레이스백이나 종료 메시지, 그리고 OmniVoice 데이터 폴더의 omnivoice.log를 확인하세요.",
|
||||
"server": "OmniVoice 백엔드 서버에 연결할 수 없습니다. {{contact}} 서버 로그에서 원인을 확인하세요(예: `docker logs <container>` 또는 `journalctl`) — Docker가 이 페이지를 제공하는 경우 백엔드와 함께 페이지 자체가 중단될 수 있습니다."
|
||||
"server": "OmniVoice 백엔드 서버에 연결할 수 없습니다. {{contact}} 서버 로그에서 원인을 확인하세요(예: `docker logs <container>` 또는 `journalctl`) — Docker가 이 페이지를 제공하는 경우 백엔드와 함께 페이지 자체가 중단될 수 있습니다.",
|
||||
"misrouted": "{{url}} 에 응답한 서버는 OmniVoice 백엔드가 아닙니다 — 해당 서버 자체의 404 페이지가 반환되었습니다. API 요청이 잘못된 호스트로 가고 있습니다. 설정 → 공유 에서 백엔드 URL을 확인하거나, 리버스 프록시가 이 UI를 제공한다면 API 경로에 대한 라우트를 확인하세요."
|
||||
},
|
||||
"nav": {
|
||||
"clone": "복제",
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"contact_recent": "Het reageerde {{ago}} geleden nog en stopte toen met reageren — waarschijnlijk is het midden in een verzoek gecrasht of beëindigd.",
|
||||
"contact_never": "Het heeft deze sessie helemaal niet gereageerd — mogelijk is het nooit gestart.",
|
||||
"dev": "Kan de lokale OmniVoice-backend niet bereiken. {{contact}} Bij `bun run dev` draait de backend met automatisch herladen: elke bestandswijziging — ook opslaan terwijl er een verzoek liep — herstart hem en verbreekt de verbinding. Probeer de actie eerst opnieuw; als het een herlaad was, werkt het gewoon. Blijft het mislukken, kijk dan in de terminal met `bun run dev` naar een Python-traceback of een exit-melding, en in omnivoice.log in uw OmniVoice-gegevensmap.",
|
||||
"server": "Kan de OmniVoice-backendserver niet bereiken. {{contact}} Controleer de serverlogs voor de oorzaak (bijv. `docker logs <container>` of `journalctl`) — en let op: als Docker deze pagina serveert, kan de pagina zelf samen met de backend uitvallen."
|
||||
"server": "Kan de OmniVoice-backendserver niet bereiken. {{contact}} Controleer de serverlogs voor de oorzaak (bijv. `docker logs <container>` of `journalctl`) — en let op: als Docker deze pagina serveert, kan de pagina zelf samen met de backend uitvallen.",
|
||||
"misrouted": "De server die {{url}} beantwoordt is geen OmniVoice-backend — hij gaf zijn eigen 404-pagina terug. API-verzoeken komen bij de verkeerde host terecht: controleer de Backend-URL in Instellingen → Delen of, als een reverse proxy deze interface serveert, de route voor API-paden."
|
||||
},
|
||||
"nav": {
|
||||
"clone": "Klonen",
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"contact_recent": "Jeszcze {{ago}} temu odpowiadał, a potem przestał — najprawdopodobniej uległ awarii lub został zakończony w trakcie żądania.",
|
||||
"contact_never": "W tej sesji nie odpowiedział ani razu — możliwe, że w ogóle się nie uruchomił.",
|
||||
"dev": "Nie można połączyć się z lokalnym backendem OmniVoice. {{contact}} W `bun run dev` backend działa z automatycznym przeładowaniem: każda zmiana pliku — w tym zapis w trakcie żądania — restartuje go i przerywa połączenie. Najpierw ponów akcję; jeśli to było przeładowanie, po prostu zadziała. Jeśli nadal się nie udaje, sprawdź terminal z `bun run dev` pod kątem tracebacku Pythona lub komunikatu wyjścia oraz omnivoice.log w folderze danych OmniVoice.",
|
||||
"server": "Nie można połączyć się z serwerem backendu OmniVoice. {{contact}} Sprawdź logi serwera, aby znaleźć przyczynę (np. `docker logs <kontener>` lub `journalctl`) — pamiętaj też, że jeśli Docker serwuje tę stronę, sama strona może przestać działać razem z backendem."
|
||||
"server": "Nie można połączyć się z serwerem backendu OmniVoice. {{contact}} Sprawdź logi serwera, aby znaleźć przyczynę (np. `docker logs <kontener>` lub `journalctl`) — pamiętaj też, że jeśli Docker serwuje tę stronę, sama strona może przestać działać razem z backendem.",
|
||||
"misrouted": "Serwer odpowiadający pod {{url}} nie jest backendem OmniVoice — zwrócił własną stronę 404. Żądania API trafiają na niewłaściwy host: sprawdź adres URL backendu w Ustawienia → Udostępnianie lub, jeśli ten interfejs serwuje reverse proxy, jego trasę dla ścieżek API."
|
||||
},
|
||||
"nav": {
|
||||
"clone": "Klonuj",
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"contact_recent": "Estava respondendo há {{ago}} e depois parou de responder — muito provavelmente travou ou foi encerrado no meio de uma requisição.",
|
||||
"contact_never": "Não respondeu nenhuma vez nesta sessão — pode ser que nunca tenha iniciado.",
|
||||
"dev": "Não é possível contactar o backend local do OmniVoice. {{contact}} Com `bun run dev` o backend corre com recarga automática: qualquer alteração de ficheiro — incluindo gravar enquanto havia um pedido em curso — reinicia-o e corta a ligação. Tente a ação novamente primeiro; se foi uma recarga, funciona. Se continuar a falhar, verifique o terminal com `bun run dev` para um traceback de Python ou uma mensagem de saída, e omnivoice.log na sua pasta de dados do OmniVoice.",
|
||||
"server": "Não foi possível conectar ao servidor backend do OmniVoice. {{contact}} Verifique os logs do servidor para encontrar a causa (ex.: `docker logs <container>` ou `journalctl`) — e note que, se o Docker serve esta página, a própria página pode cair junto com o backend."
|
||||
"server": "Não foi possível conectar ao servidor backend do OmniVoice. {{contact}} Verifique os logs do servidor para encontrar a causa (ex.: `docker logs <container>` ou `journalctl`) — e note que, se o Docker serve esta página, a própria página pode cair junto com o backend.",
|
||||
"misrouted": "O servidor que responde em {{url}} não é um backend do OmniVoice — ele devolveu a própria página 404. As requisições de API estão chegando ao host errado: verifique a URL do backend em Configurações → Compartilhamento ou, se um proxy reverso serve esta interface, a rota dele para os caminhos da API."
|
||||
},
|
||||
"nav": {
|
||||
"clone": "Clonar",
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"contact_recent": "Он отвечал {{ago}} назад, а затем перестал — скорее всего, он аварийно завершился или был остановлен посреди запроса.",
|
||||
"contact_never": "За эту сессию он не ответил ни разу — возможно, он вообще не запустился.",
|
||||
"dev": "Не удаётся связаться с локальным бэкендом OmniVoice. {{contact}} В `bun run dev` бэкенд работает с автоперезагрузкой: любое изменение файла — включая сохранение во время запроса — перезапускает его и обрывает соединение. Сначала повторите действие; если это была перезагрузка, всё заработает. Если ошибка повторяется, посмотрите в терминале с `bun run dev` трассировку Python или сообщение о выходе, а также omnivoice.log в папке данных OmniVoice.",
|
||||
"server": "Не удаётся подключиться к серверу бэкенда OmniVoice. {{contact}} Проверьте логи сервера, чтобы найти причину (например, `docker logs <container>` или `journalctl`) — и учтите: если эта страница обслуживается Docker, она может упасть вместе с бэкендом."
|
||||
"server": "Не удаётся подключиться к серверу бэкенда OmniVoice. {{contact}} Проверьте логи сервера, чтобы найти причину (например, `docker logs <container>` или `journalctl`) — и учтите: если эта страница обслуживается Docker, она может упасть вместе с бэкендом.",
|
||||
"misrouted": "Сервер, отвечающий по адресу {{url}}, не является бэкендом OmniVoice — он вернул собственную страницу 404. Запросы к API попадают не на тот хост: проверьте URL бэкенда в Настройки → Общий доступ или, если этот интерфейс отдаёт обратный прокси, его маршрут для путей API."
|
||||
},
|
||||
"nav": {
|
||||
"clone": "Клонировать",
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"contact_recent": "Den svarade för {{ago}} sedan och slutade sedan svara — den kraschade eller avslutades troligen mitt i en förfrågan.",
|
||||
"contact_never": "Den har inte svarat alls under den här sessionen — den kanske aldrig startade.",
|
||||
"dev": "Kan inte nå den lokala OmniVoice-backend. {{contact}} Med `bun run dev` körs backend med automatisk omladdning: varje filändring — även en sparning medan en förfrågan pågick — startar om den och bryter anslutningen. Försök med åtgärden igen först; var det en omladdning fungerar det. Om det fortsätter att misslyckas, titta i terminalen med `bun run dev` efter en Python-traceback eller ett avslutsmeddelande, och i omnivoice.log i din OmniVoice-datamapp.",
|
||||
"server": "Kan inte nå OmniVoice-backendservern. {{contact}} Kontrollera serverloggarna för orsaken (t.ex. `docker logs <container>` eller `journalctl`) — och observera att om Docker serverar den här sidan kan själva sidan gå ner tillsammans med backenden."
|
||||
"server": "Kan inte nå OmniVoice-backendservern. {{contact}} Kontrollera serverloggarna för orsaken (t.ex. `docker logs <container>` eller `journalctl`) — och observera att om Docker serverar den här sidan kan själva sidan gå ner tillsammans med backenden.",
|
||||
"misrouted": "Servern som svarar på {{url}} är inte en OmniVoice-backend — den returnerade sin egen 404-sida. API-anropen hamnar hos fel värd: kontrollera backend-URL:en under Inställningar → Delning eller, om en omvänd proxy serverar detta gränssnitt, dess rutt för API-sökvägar."
|
||||
},
|
||||
"nav": {
|
||||
"clone": "Klona",
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"contact_recent": "เมื่อ {{ago}} ก่อนยังตอบสนองอยู่ แล้วก็หยุดตอบสนอง — มีแนวโน้มสูงว่าแครชหรือถูกปิดกลางคันระหว่างคำขอ",
|
||||
"contact_never": "ยังไม่เคยตอบสนองเลยในเซสชันนี้ — อาจไม่เคยเริ่มทำงานเลย",
|
||||
"dev": "ไม่สามารถติดต่อแบ็กเอนด์ OmniVoice ในเครื่องได้ {{contact}} ใน `bun run dev` แบ็กเอนด์ทำงานแบบรีโหลดอัตโนมัติ การเปลี่ยนไฟล์ใด ๆ — รวมถึงการบันทึกขณะมีคำขอค้างอยู่ — จะรีสตาร์ตและตัดการเชื่อมต่อ ลองทำอีกครั้งก่อน หากเป็นการรีโหลดก็จะใช้งานได้ หากยังล้มเหลว ให้ดูเทอร์มินัลที่รัน `bun run dev` ว่ามี Python traceback หรือข้อความออกจากโปรแกรม และดู omnivoice.log ในโฟลเดอร์ข้อมูล OmniVoice",
|
||||
"server": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์แบ็กเอนด์ OmniVoice ได้ {{contact}} ตรวจสอบล็อกของเซิร์ฟเวอร์เพื่อหาสาเหตุ (เช่น `docker logs <container>` หรือ `journalctl`) — และโปรดทราบว่าถ้า Docker เป็นผู้ให้บริการหน้านี้ ตัวหน้าเว็บเองก็อาจล่มไปพร้อมกับแบ็กเอนด์"
|
||||
"server": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์แบ็กเอนด์ OmniVoice ได้ {{contact}} ตรวจสอบล็อกของเซิร์ฟเวอร์เพื่อหาสาเหตุ (เช่น `docker logs <container>` หรือ `journalctl`) — และโปรดทราบว่าถ้า Docker เป็นผู้ให้บริการหน้านี้ ตัวหน้าเว็บเองก็อาจล่มไปพร้อมกับแบ็กเอนด์",
|
||||
"misrouted": "เซิร์ฟเวอร์ที่ตอบกลับที่ {{url}} ไม่ใช่แบ็กเอนด์ของ OmniVoice — มันส่งหน้า 404 ของตัวเองกลับมา คำขอ API กำลังไปยังโฮสต์ที่ผิด: ตรวจสอบ Backend URL ใน การตั้งค่า → แชร์ข้อมูล หรือหากมีรีเวิร์สพร็อกซีให้บริการหน้านี้ ให้ตรวจสอบเส้นทางสำหรับพาธ API"
|
||||
},
|
||||
"nav": {
|
||||
"clone": "โคลนเสียง",
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"contact_recent": "{{ago}} önce yanıt veriyordu, sonra yanıt vermeyi bıraktı — büyük olasılıkla bir istek sırasında çöktü veya sonlandırıldı.",
|
||||
"contact_never": "Bu oturumda hiç yanıt vermedi — hiç başlamamış olabilir.",
|
||||
"dev": "Yerel OmniVoice arka ucuna ulaşılamıyor. {{contact}} `bun run dev` ile arka uç otomatik yeniden yükleme ile çalışır: herhangi bir dosya değişikliği — bir istek sürerken kaydetmek dâhil — onu yeniden başlatır ve bağlantıyı koparır. Önce işlemi yeniden deneyin; yeniden yükleme olduysa çalışacaktır. Hata sürerse `bun run dev` çalıştıran terminalde bir Python izini veya çıkış mesajını ve OmniVoice veri klasörünüzdeki omnivoice.log dosyasını kontrol edin.",
|
||||
"server": "OmniVoice arka uç sunucusuna ulaşılamıyor. {{contact}} Nedenini görmek için sunucu günlüklerini kontrol edin (ör. `docker logs <container>` veya `journalctl`) — ve bu sayfayı Docker sunuyorsa sayfanın kendisinin de arka uçla birlikte kapanabileceğini unutmayın."
|
||||
"server": "OmniVoice arka uç sunucusuna ulaşılamıyor. {{contact}} Nedenini görmek için sunucu günlüklerini kontrol edin (ör. `docker logs <container>` veya `journalctl`) — ve bu sayfayı Docker sunuyorsa sayfanın kendisinin de arka uçla birlikte kapanabileceğini unutmayın.",
|
||||
"misrouted": "{{url}} adresine yanıt veren sunucu bir OmniVoice arka ucu değil — kendi 404 sayfasını döndürdü. API istekleri yanlış sunucuya gidiyor: Ayarlar → Paylaşım bölümünden arka uç URL'sini kontrol edin veya bu arayüzü bir ters proxy sunuyorsa API yolları için tanımlı rotasını denetleyin."
|
||||
},
|
||||
"nav": {
|
||||
"clone": "Klonla",
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"contact_recent": "Він відповідав {{ago}} тому, а потім перестав — найімовірніше, він аварійно завершився або був примусово зупинений посеред запиту.",
|
||||
"contact_never": "У цій сесії він не відповів жодного разу — можливо, він узагалі не запустився.",
|
||||
"dev": "Не вдається зв'язатися з локальним бекендом OmniVoice. {{contact}} У `bun run dev` бекенд працює з автоперезавантаженням: будь-яка зміна файлу — зокрема збереження під час запиту — перезапускає його й обриває з'єднання. Спершу повторіть дію; якщо це було перезавантаження, усе спрацює. Якщо помилка повторюється, перевірте термінал із `bun run dev` на трасування Python або повідомлення про вихід, а також omnivoice.log у теці даних OmniVoice.",
|
||||
"server": "Не вдається з'єднатися із сервером бекенда OmniVoice. {{contact}} Перегляньте логи сервера, щоб знайти причину (наприклад, `docker logs <container>` або `journalctl`) — і майте на увазі: якщо цю сторінку обслуговує Docker, сама сторінка може впасти разом із бекендом."
|
||||
"server": "Не вдається з'єднатися із сервером бекенда OmniVoice. {{contact}} Перегляньте логи сервера, щоб знайти причину (наприклад, `docker logs <container>` або `journalctl`) — і майте на увазі: якщо цю сторінку обслуговує Docker, сама сторінка може впасти разом із бекендом.",
|
||||
"misrouted": "Сервер, що відповідає за адресою {{url}}, не є бекендом OmniVoice — він повернув власну сторінку 404. Запити до API потрапляють не на той хост: перевірте URL бекенда в Налаштування → Спільний доступ або, якщо цей інтерфейс віддає зворотний проксі, його маршрут для шляхів API."
|
||||
},
|
||||
"nav": {
|
||||
"clone": "Клонувати",
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"contact_recent": "Nó vẫn phản hồi cách đây {{ago}} rồi ngừng phản hồi — rất có thể nó đã gặp sự cố hoặc bị buộc dừng giữa một yêu cầu.",
|
||||
"contact_never": "Nó chưa hề phản hồi trong phiên này — có thể nó chưa bao giờ khởi động.",
|
||||
"dev": "Không thể kết nối tới backend OmniVoice cục bộ. {{contact}} Với `bun run dev`, backend chạy ở chế độ tự tải lại: bất kỳ thay đổi tệp nào — kể cả lưu khi đang có yêu cầu — sẽ khởi động lại nó và làm mất kết nối. Hãy thử lại thao tác trước; nếu là do tải lại thì sẽ chạy được. Nếu vẫn lỗi, xem terminal đang chạy `bun run dev` để tìm traceback Python hoặc thông báo thoát, và omnivoice.log trong thư mục dữ liệu OmniVoice.",
|
||||
"server": "Không thể kết nối tới máy chủ backend OmniVoice. {{contact}} Hãy kiểm tra log máy chủ để tìm nguyên nhân (ví dụ `docker logs <container>` hoặc `journalctl`) — và lưu ý nếu Docker phục vụ trang này thì chính trang này cũng có thể sập cùng backend."
|
||||
"server": "Không thể kết nối tới máy chủ backend OmniVoice. {{contact}} Hãy kiểm tra log máy chủ để tìm nguyên nhân (ví dụ `docker logs <container>` hoặc `journalctl`) — và lưu ý nếu Docker phục vụ trang này thì chính trang này cũng có thể sập cùng backend.",
|
||||
"misrouted": "Máy chủ trả lời tại {{url}} không phải là backend của OmniVoice — nó đã trả về trang 404 của chính nó. Các yêu cầu API đang đi tới sai máy chủ: hãy kiểm tra Backend URL trong Cài đặt → Chia sẻ, hoặc nếu một reverse proxy phục vụ giao diện này, hãy kiểm tra tuyến đường của nó cho các đường dẫn API."
|
||||
},
|
||||
"nav": {
|
||||
"clone": "Nhân bản",
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"contact_recent": "它在 {{ago}}前还在响应,随后停止了响应——很可能是在处理请求时崩溃或被强制终止。",
|
||||
"contact_never": "本次会话中它从未响应过——可能根本没有启动。",
|
||||
"dev": "无法连接本地 OmniVoice 后端。{{contact}} 在 `bun run dev` 下后端以自动重载方式运行,任何文件改动(包括请求进行中的保存)都会重启它并中断连接。请先重试该操作;如果是重载导致的,重试即可成功。若仍然失败,请查看运行 `bun run dev` 的终端是否有 Python 回溯或退出信息,以及 OmniVoice 数据文件夹中的 omnivoice.log。",
|
||||
"server": "无法连接 OmniVoice 后端服务器。{{contact}} 请查看服务器日志以找到原因(例如 `docker logs <容器>` 或 `journalctl`)——另外请注意:如果此页面由 Docker 提供,页面本身也可能随后端一起不可用。"
|
||||
"server": "无法连接 OmniVoice 后端服务器。{{contact}} 请查看服务器日志以找到原因(例如 `docker logs <容器>` 或 `journalctl`)——另外请注意:如果此页面由 Docker 提供,页面本身也可能随后端一起不可用。",
|
||||
"misrouted": "响应 {{url}} 的服务器不是 OmniVoice 后端——它返回了自己的 404 页面。API 请求发送到了错误的主机:请在 设置 → 分享 中检查后端 URL;如果此界面由反向代理提供,请检查它对 API 路径的路由配置。"
|
||||
},
|
||||
"stories": {
|
||||
"title": "故事编辑器",
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"contact_recent": "它在 {{ago}}前還有回應,之後停止回應——很可能是在處理請求時當機或被強制終止。",
|
||||
"contact_never": "本次工作階段中它從未回應——可能根本沒有啟動。",
|
||||
"dev": "無法連線本機 OmniVoice 後端。{{contact}} 在 `bun run dev` 下後端以自動重新載入方式執行,任何檔案變動(包括請求進行中的儲存)都會重啟它並中斷連線。請先重試該操作;若是重新載入造成的,重試即可成功。若仍然失敗,請查看執行 `bun run dev` 的終端機是否有 Python 追蹤訊息或結束訊息,以及 OmniVoice 資料夾中的 omnivoice.log。",
|
||||
"server": "無法連線 OmniVoice 後端伺服器。{{contact}} 請查看伺服器日誌以找出原因(例如 `docker logs <容器>` 或 `journalctl`)——另請注意:如果此頁面由 Docker 提供,頁面本身也可能隨後端一起停止運作。"
|
||||
"server": "無法連線 OmniVoice 後端伺服器。{{contact}} 請查看伺服器日誌以找出原因(例如 `docker logs <容器>` 或 `journalctl`)——另請注意:如果此頁面由 Docker 提供,頁面本身也可能隨後端一起停止運作。",
|
||||
"misrouted": "回應 {{url}} 的伺服器並非 OmniVoice 後端——它回傳了自己的 404 頁面。API 請求送到了錯誤的主機:請在 設定 → 網路共享 中檢查後端 URL;若此介面由反向代理提供,請檢查它對 API 路徑的路由設定。"
|
||||
},
|
||||
"nav": {
|
||||
"clone": "複製聲音",
|
||||
|
||||
@@ -86,6 +86,10 @@ const EN = {
|
||||
'thing the backend logged.',
|
||||
server:
|
||||
"Can't reach the OmniVoice backend server. {{contact}} Check the server logs for the cause (e.g. `docker logs <container>` or `journalctl`) — and note that if Docker serves this page, the page itself can go down with the backend.",
|
||||
misrouted:
|
||||
'The server answering {{url}} is not an OmniVoice backend — it returned its own 404 page. ' +
|
||||
'API requests are landing on the wrong host: check the Backend URL in Settings → Sharing, ' +
|
||||
'or, if a reverse proxy serves this UI, its route for API paths.',
|
||||
} as const;
|
||||
|
||||
function tr(key: string, vars: Record<string, string>, fallback: string): string {
|
||||
@@ -125,3 +129,14 @@ export function unreachableBackendMessage(
|
||||
if (m === 'dev') return tr('backendUnreachable.dev', { contact }, EN.dev);
|
||||
return tr('backendUnreachable.server', { contact }, EN.server);
|
||||
}
|
||||
|
||||
/**
|
||||
* A 404 answered by something that is not this backend (#1385): a rehosted UI
|
||||
* whose API requests land on its own static host, or a reverse proxy with no
|
||||
* route for API paths. Echoing that server's 404 page ("NOT_FOUND bom1::…")
|
||||
* sends the user chasing a page that never existed — name the routing problem
|
||||
* instead, and where to fix it.
|
||||
*/
|
||||
export function misroutedBackendMessage(url: string): string {
|
||||
return tr('backendUnreachable.misrouted', { url }, EN.misrouted);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Every response says "an OmniVoice backend answered this" (#1385).
|
||||
|
||||
A rehosted UI whose API requests land on a static host or a reverse proxy with
|
||||
no API route gets that host's own 404 page. The frontend used to echo it
|
||||
("NOT_FOUND bom1::…"), sending users to chase a page that never existed. It now
|
||||
says the request reached the wrong host — but only if it can TELL, and body
|
||||
shape alone cannot tell: a proxy can answer with JSON too.
|
||||
|
||||
Hence the marker header. Its presence is authoritative; its absence is what
|
||||
lets the client conclude the responder is not this backend. Two properties
|
||||
matter and both are pinned here: it is on *every* response (including the auth
|
||||
gates' rejections, which are generated by middleware rather than routes), and
|
||||
it is readable cross-origin (otherwise the one deployment that needs it — a
|
||||
browser UI on another origin — cannot see it).
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "backend"))
|
||||
|
||||
MARKER = "x-omnivoice-backend"
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client(monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("OMNIVOICE_DATA_DIR", str(tmp_path))
|
||||
import main
|
||||
|
||||
return TestClient(main.app), main
|
||||
|
||||
|
||||
def test_a_normal_response_carries_the_marker(client):
|
||||
c, _ = client
|
||||
res = c.get("/health")
|
||||
assert res.headers.get(MARKER), "no marker on a plain 200"
|
||||
|
||||
|
||||
def test_an_unrouted_path_carries_the_marker(client):
|
||||
# The exact case that matters: a 404. If OUR 404 were unmarked, the client
|
||||
# could not tell it from a foreign server's 404 and would tell the user
|
||||
# their routing is broken when it is not.
|
||||
c, _ = client
|
||||
res = c.get("/definitely-not-a-route-xyz")
|
||||
assert res.status_code == 404
|
||||
assert res.headers.get(MARKER)
|
||||
|
||||
|
||||
def test_the_marker_reports_the_running_version(client):
|
||||
c, main = client
|
||||
from core.version import APP_VERSION
|
||||
|
||||
assert c.get("/health").headers[MARKER] == APP_VERSION
|
||||
|
||||
|
||||
def test_the_marker_survives_the_auth_gates(monkeypatch, tmp_path):
|
||||
# The PIN/API-key middlewares answer 401 themselves, outside any route.
|
||||
# Those rejections are responses too, and a client that gets one has
|
||||
# certainly reached a backend — it must not read as "wrong host".
|
||||
monkeypatch.setenv("OMNIVOICE_DATA_DIR", str(tmp_path))
|
||||
monkeypatch.setenv("OMNIVOICE_API_KEY", "secret-key-for-this-test")
|
||||
import importlib
|
||||
|
||||
import main
|
||||
|
||||
importlib.reload(main)
|
||||
try:
|
||||
c = TestClient(main.app)
|
||||
res = c.get("/system/info", headers={"x-forwarded-for": "203.0.113.9"})
|
||||
# Assert the rejection first (CodeRabbit): a 200 here would mean the
|
||||
# gate never ran, and the test would prove nothing about middleware-
|
||||
# generated responses — the only kind that bypass every route.
|
||||
assert res.status_code == 401, (
|
||||
f"expected the API-key gate to reject this request, got {res.status_code}"
|
||||
)
|
||||
assert res.headers.get(MARKER), "auth-gate rejection lost the marker"
|
||||
finally:
|
||||
monkeypatch.delenv("OMNIVOICE_API_KEY", raising=False)
|
||||
importlib.reload(main)
|
||||
|
||||
|
||||
def test_the_marker_is_exposed_to_cross_origin_readers():
|
||||
# A browser can only read a response header that CORS exposes. The whole
|
||||
# point is the deployment where the UI is on another origin, so an
|
||||
# unexposed marker is an invisible one.
|
||||
import main
|
||||
|
||||
for mw in main.app.user_middleware:
|
||||
if mw.cls.__name__ == "CORSMiddleware":
|
||||
exposed = [h.lower() for h in mw.kwargs.get("expose_headers", [])]
|
||||
assert MARKER in exposed
|
||||
return
|
||||
pytest.fail("CORSMiddleware is not registered")
|
||||
Reference in New Issue
Block a user