diff --git a/public/js/admin.js b/public/js/admin.js index 9aefb68..db66105 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -194,11 +194,16 @@ function showListView() { el('dash-list-view').classList.remove('d-none'); el('dash-edit-view').classList.add('d-none'); } +/* Keep both. The pane scrolls on a wide window, the document on a phone. */ +function scrollSettingsTop() { + q('.cp')?.scrollTo?.(0, 0); + scrollTo(0, 0); +} + function showEditView() { el('dash-list-view').classList.add('d-none'); el('dash-edit-view').classList.remove('d-none'); - el('cp')?.scrollTo?.(0, 0); - q('.cp')?.scrollTo?.(0, 0); + scrollSettingsTop(); } /* Read at draw time. The catalog is not loaded when this module evaluates. */ @@ -616,7 +621,8 @@ function initNav() { const STORE = 'admin_sec'; const sections = qa('.sec', document).map(s => s.id.replace(/^sec-/, '')); - function show(requested) { + /* Do not scroll on the restoring first call. Only a picked section. */ + function show(requested, picked) { const id = resolveAdminSection(requested, sections); if (id === null) return; if (requested && id !== requested) console.warn('admin: unknown section', requested, '- showing', id); @@ -626,9 +632,10 @@ function initNav() { links.forEach(l => l.classList.toggle('active', l.dataset.sec === id)); syncGlideSelect(); localStorage.setItem(STORE, id); + if (picked) scrollSettingsTop(); } - links.forEach(l => l.addEventListener('click', () => show(l.dataset.sec))); - addEventListener('hashchange', () => show(location.hash.slice(1))); + links.forEach(l => l.addEventListener('click', () => show(l.dataset.sec, true))); + addEventListener('hashchange', () => show(location.hash.slice(1), true)); show(location.hash.slice(1) || localStorage.getItem(STORE)); initGlideSelect(); } diff --git a/public/widgets/connections/data.js b/public/widgets/connections/data.js index 2dabe99..c615769 100644 --- a/public/widgets/connections/data.js +++ b/public/widgets/connections/data.js @@ -333,15 +333,21 @@ async function mapView(ctx) { o.connected = total; o.limit = 0; } else if (s.type === 'umami') { - const lg = await fetchJSON(base + '/api/auth/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ username: s.username || '', password: s.password || '' }), - }); - if (authErr(lg)) ctx.fail('Auth required — check username/password', { kind: ctx.KIND.AUTH }); - if (lg.status >= 400) ctx.fail('Login HTTP ' + lg.status); - const token = lg.data && lg.data.token; - if (!token) ctx.fail('Login failed', { kind: ctx.KIND.AUTH }); + const usingKey = !!s.apiKey; + let token = s.apiKey || ''; + if (!token) { + if (!s.username && !s.password) + ctx.fail('Enter an API key, or a username and password', { kind: ctx.KIND.INVALID }); + const lg = await fetchJSON(base + '/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username: s.username || '', password: s.password || '' }), + }); + if (authErr(lg)) ctx.fail('Auth required — check username/password', { kind: ctx.KIND.AUTH }); + if (lg.status >= 400) ctx.fail('Login HTTP ' + lg.status); + token = (lg.data && lg.data.token) || ''; + if (!token) ctx.fail('Login failed', { kind: ctx.KIND.AUTH }); + } const end = Date.now(), start = end - 7 * 24 * 3600 * 1000; const r = await fetchJSON( @@ -354,6 +360,10 @@ async function mapView(ctx) { end, { headers: { Authorization: 'Bearer ' + token } }, ); + if (authErr(r)) + ctx.fail(usingKey ? 'Auth rejected — check the API key' : 'Auth rejected — check username/password', { + kind: ctx.KIND.AUTH, + }); if (r.status >= 400) ctx.fail('HTTP ' + r.status); const rows = Array.isArray(r.data) ? r.data : []; const regions = {}; diff --git a/public/widgets/connections/i18n/de.json b/public/widgets/connections/i18n/de.json index f83e44d..bcd41df 100644 --- a/public/widgets/connections/i18n/de.json +++ b/public/widgets/connections/i18n/de.json @@ -44,6 +44,8 @@ "services.siteId.label": "Site-ID (Domain)", "services.siteId.placeholder": "example.com", "services.apiKey.label": "Statistik-API-Schlüssel", + "services.apiKey.label.umami": "API-Schlüssel", + "services.apiKey.hint.umami": "Ab Umami 3.4.0. Leer lassen, um sich mit Benutzername und Passwort anzumelden.", "services.websiteId.label": "Website-ID", "services.websiteId.placeholder": "8dc7… (UUID)", "services.username.label": "Benutzername", diff --git a/public/widgets/connections/i18n/en.json b/public/widgets/connections/i18n/en.json index 20c74fe..490506a 100644 --- a/public/widgets/connections/i18n/en.json +++ b/public/widgets/connections/i18n/en.json @@ -44,6 +44,8 @@ "services.siteId.label": "Site ID (domain)", "services.siteId.placeholder": "example.com", "services.apiKey.label": "Stats API key", + "services.apiKey.label.umami": "API key", + "services.apiKey.hint.umami": "Umami 3.4.0 and later. Leave empty to sign in with a username and password.", "services.websiteId.label": "Website ID", "services.websiteId.placeholder": "8dc7… (UUID)", "services.username.label": "Username", diff --git a/public/widgets/connections/i18n/es.json b/public/widgets/connections/i18n/es.json index 2da613b..baef866 100644 --- a/public/widgets/connections/i18n/es.json +++ b/public/widgets/connections/i18n/es.json @@ -44,6 +44,8 @@ "services.siteId.label": "ID del sitio (dominio)", "services.siteId.placeholder": "example.com", "services.apiKey.label": "Clave de API de estadísticas", + "services.apiKey.label.umami": "Clave de API", + "services.apiKey.hint.umami": "Umami 3.4.0 o posterior. Déjalo vacío para iniciar sesión con usuario y contraseña.", "services.websiteId.label": "ID del sitio web", "services.websiteId.placeholder": "8dc7… (UUID)", "services.username.label": "Usuario", diff --git a/public/widgets/connections/i18n/fa.json b/public/widgets/connections/i18n/fa.json index 96e7dbd..6696c51 100644 --- a/public/widgets/connections/i18n/fa.json +++ b/public/widgets/connections/i18n/fa.json @@ -44,6 +44,8 @@ "services.siteId.label": "شناسهٔ سایت (دامنه)", "services.siteId.placeholder": "example.com", "services.apiKey.label": "کلید API آمار", + "services.apiKey.label.umami": "کلید API", + "services.apiKey.hint.umami": "از Umami نسخه ۳.۴.۰ به بعد. برای ورود با نام کاربری و رمز عبور، خالی بگذارید.", "services.websiteId.label": "شناسهٔ وب‌سایت", "services.websiteId.placeholder": "8dc7… (UUID)", "services.username.label": "نام کاربری", diff --git a/public/widgets/connections/i18n/fr.json b/public/widgets/connections/i18n/fr.json index 379307b..d076d91 100644 --- a/public/widgets/connections/i18n/fr.json +++ b/public/widgets/connections/i18n/fr.json @@ -44,6 +44,8 @@ "services.siteId.label": "Identifiant du site (domaine)", "services.siteId.placeholder": "example.com", "services.apiKey.label": "Clé d’API des statistiques", + "services.apiKey.label.umami": "Clé d’API", + "services.apiKey.hint.umami": "Umami 3.4.0 et versions ultérieures. Laissez vide pour vous connecter avec un nom d’utilisateur et un mot de passe.", "services.websiteId.label": "Identifiant du site web", "services.websiteId.placeholder": "8dc7… (UUID)", "services.username.label": "Nom d’utilisateur", diff --git a/public/widgets/connections/i18n/zh-Hans.json b/public/widgets/connections/i18n/zh-Hans.json index d6639f6..053387f 100644 --- a/public/widgets/connections/i18n/zh-Hans.json +++ b/public/widgets/connections/i18n/zh-Hans.json @@ -44,6 +44,8 @@ "services.siteId.label": "站点 ID(域名)", "services.siteId.placeholder": "example.com", "services.apiKey.label": "统计 API 密钥", + "services.apiKey.label.umami": "API 密钥", + "services.apiKey.hint.umami": "Umami 3.4.0 及以上版本。留空则使用用户名和密码登录。", "services.websiteId.label": "网站 ID", "services.websiteId.placeholder": "8dc7… (UUID)", "services.username.label": "用户名", diff --git a/public/widgets/connections/widget.json b/public/widgets/connections/widget.json index 2f303fd..c3a1870 100644 --- a/public/widgets/connections/widget.json +++ b/public/widgets/connections/widget.json @@ -76,7 +76,8 @@ "type": "text", "label": "vpn.name.label", "placeholder": "vpn.name.placeholder", - "optional": true + "optional": true, + "cosmetic": true }, { "key": "url", @@ -128,7 +129,8 @@ "key": "color", "type": "color", "label": "vpn.color.label", - "default": "#30d158" + "default": "#30d158", + "cosmetic": true } ] }, @@ -174,7 +176,8 @@ "type": "text", "label": "services.name.label", "placeholder": "services.name.placeholder", - "optional": true + "optional": true, + "cosmetic": true }, { "key": "url", @@ -265,11 +268,23 @@ "equals": "umami" } }, + { + "key": "apiKey", + "type": "secret", + "label": "services.apiKey.label.umami", + "hint": "services.apiKey.hint.umami", + "optional": true, + "showIf": { + "field": "type", + "equals": "umami" + } + }, { "key": "username", "type": "text", "label": "services.username.label", "placeholder": "services.username.placeholder", + "optional": true, "showIf": { "field": "type", "equals": "umami" @@ -279,6 +294,7 @@ "key": "password", "type": "secret", "label": "services.password.label", + "optional": true, "showIf": { "field": "type", "equals": "umami" @@ -295,7 +311,8 @@ "key": "color", "type": "color", "label": "services.color.label", - "default": "#30d158" + "default": "#30d158", + "cosmetic": true } ] },