Complete sign-in with the same account, then return to this tab and choose Check status / Retry. Keep this tab open: your conversation, unsent text and attachments stay here.
We’ll reconnect automatically. An interrupted message will not be sent again; check its result before resending.
';
document.body.append(dialog);
let lastFocus = null;
let state = 'waiting';
@@ -13,10 +14,10 @@ export function installOutageView() {
let statusInFlight = false;
let status = { record: null, cached: true };
let lastChecked = null;
+ let lastErrorKind = null;
+ let signInAttempted = false;
const updateText = () => {
- dialog.querySelector('[data-connection]').textContent = navigator.onLine === false
- ? 'Your device reports that it is offline. Check your connection; Von’s status is unknown.'
- : 'Cannot reach Von. The cause is unknown.';
+ dialog.querySelector('[data-connection]').textContent = connectionMessage(lastErrorKind);
dialog.querySelector('[data-maintenance]').textContent = maintenanceMessage(status.record, status);
dialog.querySelector('[data-checked]').textContent = lastChecked ? `Last checked: ${new Date(lastChecked).toLocaleTimeString()}` : 'No check completed yet.';
};
@@ -35,11 +36,16 @@ export function installOutageView() {
const detail = event.detail || {};
const previous = state;
state = detail.state;
+ lastErrorKind = detail.lastErrorKind || detail.latestErrorKind || null;
lastChecked = Date.now();
updateText();
if (state === 'healthy') {
dismissed = false;
close();
+ if (previous === 'down' || signInAttempted) {
+ signInAttempted = false;
+ document.dispatchEvent(new CustomEvent('von:connectionRestored'));
+ }
} else if (state === 'down' && !detail.isThinkingActive && !dismissed && !dialog.open) {
lastFocus = document.activeElement;
dialog.showModal();
@@ -51,12 +57,22 @@ export function installOutageView() {
void refreshStatus();
});
dialog.querySelector('[data-return]').addEventListener('click', () => { dismissed = true; close(); });
+ // A normal user-initiated navigation can complete an edge sign-in that fetch
+ // cannot. Never navigate/reload the working tab or pass draft data to the new one.
+ dialog.querySelector('[data-sign-in]').addEventListener('click', () => { signInAttempted = true; });
dialog.addEventListener('cancel', event => { event.preventDefault(); dismissed = true; close(); });
window.addEventListener('online', () => {
document.dispatchEvent(new CustomEvent('von:requestHealthCheck'));
if (state === 'down') void refreshStatus();
});
window.addEventListener('offline', updateText);
+ const checkOnReturn = () => {
+ if (document.visibilityState !== 'hidden' && (state === 'down' || signInAttempted)) {
+ document.dispatchEvent(new CustomEvent('von:requestHealthCheck'));
+ }
+ };
+ window.addEventListener('focus', checkOnReturn);
+ document.addEventListener('visibilitychange', checkOnReturn);
// Fetch before an outage so the last-known attributed record can survive it.
void refreshStatus();
setInterval(() => { if (dialog.open) updateText(); }, 10000);
diff --git a/src/frontend/web/von_interface/static/outage/health.js b/src/frontend/web/von_interface/static/outage/health.js
new file mode 100644
index 00000000..c29b4f58
--- /dev/null
+++ b/src/frontend/web/von_interface/static/outage/health.js
@@ -0,0 +1,38 @@
+// A failed health request describes this browser's connection, not necessarily
+// backend downtime. In particular, Access redirects can be opaque to fetch.
+export async function fetchVonHealth({ signal } = {}) {
+ const response = await fetch('/health', {
+ cache: 'no-store', credentials: 'same-origin', redirect: 'manual', signal
+ });
+ const fail = (kind, message) => { throw Object.assign(new Error(message), { healthErrorKind: kind }); };
+ if (response.type === 'opaqueredirect' || response.redirected ||
+ (response.status >= 300 && response.status < 400)) {
+ fail('redirect', 'Health request was redirected');
+ }
+ if (response.status === 401 || response.status === 403) {
+ fail('access', `HTTP ${response.status}`);
+ }
+ if (!response.ok) fail('http', `HTTP ${response.status}`);
+ if (!response.headers.get('content-type')?.includes('application/json')) {
+ fail('unexpected_response', 'Expected a Von health response');
+ }
+ let data;
+ try { data = await response.json(); }
+ catch { fail('unexpected_response', 'Invalid Von health response'); }
+ if (data?.status !== 'healthy' || typeof data.start_time !== 'string') {
+ fail('unexpected_response', 'Invalid Von health response');
+ }
+ return data;
+}
+
+export function healthErrorKind(error) {
+ return error?.healthErrorKind || (error?.name === 'AbortError' ? 'timeout' : 'network_or_unknown');
+}
+
+export function connectionMessage(kind, online = navigator.onLine) {
+ if (online === false) return 'Your device reports that it is offline. Check your connection; Von’s status is unknown.';
+ if (kind === 'redirect') return 'The connection was redirected. You may need to sign in again; this does not confirm a Von outage.';
+ if (kind === 'access') return 'Access was refused. Try signing in again. If access is still refused, check your account’s access to Von.';
+ if (kind === 'http') return 'Von or its gateway returned an error. We’ll keep checking the connection.';
+ return 'The connection could not be checked. Your sign-in may have expired, or there may be a network or server problem.';
+}
diff --git a/src/frontend/web/von_interface/static/outage/offline.html b/src/frontend/web/von_interface/static/outage/offline.html
index f10c2ac4..b62e76a4 100644
--- a/src/frontend/web/von_interface/static/outage/offline.html
+++ b/src/frontend/web/von_interface/static/outage/offline.html
@@ -1,3 +1,3 @@
Von — reconnecting
-
Cannot reach Von
Sorry, Von cannot connect right now.
The cause is unknown. We’ll keep checking.
Return time unknown.
No check completed yet.
This page reconnects automatically. It will never resend a message. If a previous send was interrupted, check the conversation before sending again.
+
Connection to Von interrupted
Your sign-in may have expired, or there may be a network or server problem. We’ll keep checking.