hermes: expose Telegram operator setup
Some checks failed
Tests / Declarative: Post Actions testing.tests.test_hermes_chat_quality.test_compact_image_edit_resolves_latest_tenant_artifact failed
Some checks failed
Tests / Declarative: Post Actions testing.tests.test_hermes_chat_quality.test_compact_image_edit_resolves_latest_tenant_artifact failed
This commit is contained in:
parent
07acbfb396
commit
4cb0faae7f
@ -20,7 +20,7 @@ spec:
|
|||||||
app: hermes-chat-router
|
app: hermes-chat-router
|
||||||
annotations:
|
annotations:
|
||||||
ai.bstein.dev/role: privacy-preserving-chat-tenant-router
|
ai.bstein.dev/role: privacy-preserving-chat-tenant-router
|
||||||
ai.bstein.dev/config-rev: "20260812-session-sidebar"
|
ai.bstein.dev/config-rev: "20260813-telegram-setup"
|
||||||
vault.hashicorp.com/agent-inject: "true"
|
vault.hashicorp.com/agent-inject: "true"
|
||||||
vault.hashicorp.com/agent-pre-populate-only: "true"
|
vault.hashicorp.com/agent-pre-populate-only: "true"
|
||||||
vault.hashicorp.com/agent-init-first: "true"
|
vault.hashicorp.com/agent-init-first: "true"
|
||||||
@ -62,7 +62,7 @@ spec:
|
|||||||
values: [rpi5]
|
values: [rpi5]
|
||||||
containers:
|
containers:
|
||||||
- name: router
|
- name: router
|
||||||
image: registry.bstein.dev/bstein/hermes-chat-router@sha256:4e318a35353772cf16e39b2038209abd8b774cd065f39d7a99b6dcc6e28e2474
|
image: registry.bstein.dev/bstein/hermes-chat-router@sha256:5e8fbc8f9f57e33c6fe1828a60f6e4f8f49cc0b9410a3d54abcb4219717fb168
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
ports:
|
ports:
|
||||||
- {name: http, containerPort: 8080, protocol: TCP}
|
- {name: http, containerPort: 8080, protocol: TCP}
|
||||||
|
|||||||
@ -134,11 +134,37 @@ func TestRouterProxiesWebUIAndAddsTelegramShortcut(t *testing.T) {
|
|||||||
if !strings.Contains(asset, "hermes-files-sidebar") || !strings.Contains(asset, "hermes-telegram-sidebar") {
|
if !strings.Contains(asset, "hermes-files-sidebar") || !strings.Contains(asset, "hermes-telegram-sidebar") {
|
||||||
t.Fatal("Files and Telegram were not integrated into the existing sidebar")
|
t.Fatal("Files and Telegram were not integrated into the existing sidebar")
|
||||||
}
|
}
|
||||||
|
if !strings.Contains(asset, "document.querySelector('.rail')") || !strings.Contains(asset, "data-tooltip', 'Telegram") {
|
||||||
|
t.Fatal("Telegram was not integrated as a native WebUI navigation action")
|
||||||
|
}
|
||||||
|
if strings.Contains(asset, "if (!files) return") {
|
||||||
|
t.Fatal("Telegram navigation still depends on the removed legacy Files link")
|
||||||
|
}
|
||||||
if strings.Contains(asset, "position:fixed") || strings.Contains(asset, "hermes-chat-tools") {
|
if strings.Contains(asset, "position:fixed") || strings.Contains(asset, "hermes-chat-tools") {
|
||||||
t.Fatal("legacy floating chat controls remain in the mobile bridge")
|
t.Fatal("legacy floating chat controls remain in the mobile bridge")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTelegramPageExplainsOneTimeOperatorSetup(t *testing.T) {
|
||||||
|
router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return "" })
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "/telegram", nil)
|
||||||
|
request.Header.Set("X-Forwarded-User", "subject")
|
||||||
|
response := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(response, request)
|
||||||
|
if response.Code != http.StatusOK {
|
||||||
|
t.Fatalf("got status %d", response.Code)
|
||||||
|
}
|
||||||
|
body := response.Body.String()
|
||||||
|
for _, expected := range []string{"https://t.me/BotFather", "kv/atlas/hermes/chat-telegram", "bot_token", "relay_key"} {
|
||||||
|
if !strings.Contains(body, expected) {
|
||||||
|
t.Fatalf("Telegram operator setup omitted %q", expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRouterRedirectsNativeLoginToSafeChatDestination(t *testing.T) {
|
func TestRouterRedirectsNativeLoginToSafeChatDestination(t *testing.T) {
|
||||||
router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return "" })
|
router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return "" })
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -17,7 +17,7 @@ const telegramPage = `<!doctype html>
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>Hermes on Telegram</title>
|
<title>Hermes on Telegram</title>
|
||||||
<link rel="stylesheet" href="/hermes-chat-bridge.css?v=20260812-1">
|
<link rel="stylesheet" href="/hermes-chat-bridge.css?v=20260813-telegram-setup">
|
||||||
</head>
|
</head>
|
||||||
<body class="hermes-link-page">
|
<body class="hermes-link-page">
|
||||||
<main class="hermes-link-card" data-telegram-page>
|
<main class="hermes-link-card" data-telegram-page>
|
||||||
@ -30,9 +30,19 @@ const telegramPage = `<!doctype html>
|
|||||||
<button id="telegram-unlink" class="secondary" type="button">Unlink Telegram</button>
|
<button id="telegram-unlink" class="secondary" type="button">Unlink Telegram</button>
|
||||||
</div>
|
</div>
|
||||||
<section id="telegram-result" hidden></section>
|
<section id="telegram-result" hidden></section>
|
||||||
|
<section id="telegram-operator-setup" class="hermes-operator-setup" hidden>
|
||||||
|
<h2>Activate the shared bot (operator, once)</h2>
|
||||||
|
<ol>
|
||||||
|
<li><a class="hermes-button primary" href="https://t.me/BotFather" target="_blank" rel="noopener noreferrer">Open @BotFather</a> and send <code>/newbot</code>.</li>
|
||||||
|
<li>Choose the bot's display name and a username ending in <code>bot</code>, then copy the token BotFather returns.</li>
|
||||||
|
<li>Store the token in Vault at <code>kv/atlas/hermes/chat-telegram</code> under the <code>bot_token</code> key. Preserve the existing <code>relay_key</code>.</li>
|
||||||
|
</ol>
|
||||||
|
<pre>vault kv patch -mount=kv atlas/hermes/chat-telegram bot_token='<BOTFATHER_TOKEN>'</pre>
|
||||||
|
<p><strong>Do not paste the token into Hermes or send it in a message.</strong> After it is saved, ask the Hermes operator to apply the tracked router rollout, then return here to link your account.</p>
|
||||||
|
</section>
|
||||||
<p class="hermes-fine-print">Codes expire after 10 minutes. Only direct messages are accepted; group messages are ignored.</p>
|
<p class="hermes-fine-print">Codes expire after 10 minutes. Only direct messages are accepted; group messages are ignored.</p>
|
||||||
</main>
|
</main>
|
||||||
<script src="/hermes-chat-bridge.js?v=20260812-1" defer></script>
|
<script src="/hermes-chat-bridge.js?v=20260813-telegram-setup" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>`
|
</html>`
|
||||||
|
|
||||||
@ -42,7 +52,7 @@ const privateFilesPage = `<!doctype html>
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>Hermes Private Files</title>
|
<title>Hermes Private Files</title>
|
||||||
<link rel="stylesheet" href="/hermes-chat-bridge.css?v=20260812-1">
|
<link rel="stylesheet" href="/hermes-chat-bridge.css?v=20260813-telegram-setup">
|
||||||
</head>
|
</head>
|
||||||
<body class="hermes-files-page">
|
<body class="hermes-files-page">
|
||||||
<main class="hermes-files-shell" data-files-page>
|
<main class="hermes-files-shell" data-files-page>
|
||||||
@ -73,7 +83,7 @@ const privateFilesPage = `<!doctype html>
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<script src="/hermes-chat-bridge.js?v=20260812-1" defer></script>
|
<script src="/hermes-chat-bridge.js?v=20260813-telegram-setup" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>`
|
</html>`
|
||||||
|
|
||||||
@ -83,7 +93,7 @@ const bridgeCSS = `
|
|||||||
.hermes-link-card{width:min(620px,calc(100% - 40px));box-sizing:border-box;padding:32px;border:1px solid #334155;border-radius:18px;background:#111827;box-shadow:0 20px 60px #0006}
|
.hermes-link-card{width:min(620px,calc(100% - 40px));box-sizing:border-box;padding:32px;border:1px solid #334155;border-radius:18px;background:#111827;box-shadow:0 20px 60px #0006}
|
||||||
.hermes-link-card h1{margin:.6rem 0}.hermes-back{color:#7dd3fc}.hermes-link-actions{display:flex;gap:12px;flex-wrap:wrap;margin:24px 0}
|
.hermes-link-card h1{margin:.6rem 0}.hermes-back{color:#7dd3fc}.hermes-link-actions{display:flex;gap:12px;flex-wrap:wrap;margin:24px 0}
|
||||||
.hermes-link-card button{border:0;border-radius:10px;padding:11px 16px;background:#229ed9;color:#fff;font-weight:700;cursor:pointer}.hermes-link-card button.secondary{background:#334155}.hermes-link-card button:disabled{cursor:not-allowed;opacity:.45}
|
.hermes-link-card button{border:0;border-radius:10px;padding:11px 16px;background:#229ed9;color:#fff;font-weight:700;cursor:pointer}.hermes-link-card button.secondary{background:#334155}.hermes-link-card button:disabled{cursor:not-allowed;opacity:.45}
|
||||||
#telegram-result{padding:16px;border-radius:10px;background:#1e293b;overflow-wrap:anywhere}#telegram-result a{color:#7dd3fc}.hermes-fine-print{color:#94a3b8;font-size:13px}
|
#telegram-result{padding:16px;border-radius:10px;background:#1e293b;overflow-wrap:anywhere}#telegram-result a{color:#7dd3fc}.hermes-fine-print{color:#94a3b8;font-size:13px}.hermes-operator-setup{margin:24px 0;padding:18px;border:1px solid #334155;border-radius:12px;background:#0b1220}.hermes-operator-setup h2{margin:0 0 12px;font-size:19px}.hermes-operator-setup li{margin:10px 0}.hermes-operator-setup code,.hermes-operator-setup pre{font-family:ui-monospace,SFMono-Regular,Consolas,monospace}.hermes-operator-setup pre{padding:12px;border-radius:8px;background:#020617;color:#cbd5e1;white-space:pre-wrap;overflow-wrap:anywhere}.hermes-button{display:inline-block;padding:8px 12px;border-radius:8px;background:#334155;color:#e5e7eb;text-decoration:none}.hermes-button.primary{background:#229ed9;color:#fff;font-weight:700}
|
||||||
.hermes-files-page{margin:0;min-height:100vh;background:#0b1020;color:#e5e7eb;font:15px/1.5 system-ui,sans-serif}.hermes-files-shell{width:min(1500px,calc(100% - 36px));margin:auto;padding:28px 0}.hermes-files-header{display:flex;justify-content:space-between;gap:28px;align-items:end;border-bottom:1px solid #293249;padding-bottom:18px}.hermes-files-header h1{margin:.4rem 0 0}.hermes-files-header p{margin:.25rem 0;color:#9ca3af}.hermes-files-header label{display:grid;gap:6px;color:#9ca3af}.hermes-files-header select{min-width:260px;background:#151b2e;color:#e5e7eb;border:1px solid #39445f;border-radius:8px;padding:9px}.hermes-breadcrumbs{display:flex;gap:6px;flex-wrap:wrap;margin:18px 0}.hermes-breadcrumbs button{border:0;background:transparent;color:#7dd3fc;cursor:pointer;padding:4px}.hermes-files-grid{display:grid;grid-template-columns:minmax(280px,38%) 1fr;gap:18px}.hermes-files-grid>section{border:1px solid #293249;border-radius:12px;background:#11172a;min-height:65vh;overflow:hidden}.hermes-files-toolbar{min-height:42px;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 14px;border-bottom:1px solid #293249}.hermes-button{padding:6px 10px;border-radius:7px;background:#334155;color:#e5e7eb;text-decoration:none}.hermes-file-list{list-style:none;margin:0;padding:8px}.hermes-file-list button{width:100%;display:grid;grid-template-columns:1fr auto;gap:14px;text-align:left;border:0;border-radius:7px;padding:9px 10px;background:transparent;color:#e5e7eb;cursor:pointer}.hermes-file-list button:hover,.hermes-file-list button:focus{background:#202941}.hermes-file-meta{color:#8d98ad;font-size:12px}.hermes-file-viewer pre{box-sizing:border-box;margin:0;padding:18px;max-height:calc(65vh - 64px);overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;color:#d9e2f1;font:13px/1.55 ui-monospace,SFMono-Regular,Consolas,monospace}#files-status{color:#9ca3af}@media(max-width:800px){.hermes-files-header{display:block}.hermes-files-header label{margin-top:14px}.hermes-files-header select{width:100%;min-width:0}.hermes-files-grid{grid-template-columns:1fr}.hermes-files-grid>section{min-height:38vh}}
|
.hermes-files-page{margin:0;min-height:100vh;background:#0b1020;color:#e5e7eb;font:15px/1.5 system-ui,sans-serif}.hermes-files-shell{width:min(1500px,calc(100% - 36px));margin:auto;padding:28px 0}.hermes-files-header{display:flex;justify-content:space-between;gap:28px;align-items:end;border-bottom:1px solid #293249;padding-bottom:18px}.hermes-files-header h1{margin:.4rem 0 0}.hermes-files-header p{margin:.25rem 0;color:#9ca3af}.hermes-files-header label{display:grid;gap:6px;color:#9ca3af}.hermes-files-header select{min-width:260px;background:#151b2e;color:#e5e7eb;border:1px solid #39445f;border-radius:8px;padding:9px}.hermes-breadcrumbs{display:flex;gap:6px;flex-wrap:wrap;margin:18px 0}.hermes-breadcrumbs button{border:0;background:transparent;color:#7dd3fc;cursor:pointer;padding:4px}.hermes-files-grid{display:grid;grid-template-columns:minmax(280px,38%) 1fr;gap:18px}.hermes-files-grid>section{border:1px solid #293249;border-radius:12px;background:#11172a;min-height:65vh;overflow:hidden}.hermes-files-toolbar{min-height:42px;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 14px;border-bottom:1px solid #293249}.hermes-button{padding:6px 10px;border-radius:7px;background:#334155;color:#e5e7eb;text-decoration:none}.hermes-file-list{list-style:none;margin:0;padding:8px}.hermes-file-list button{width:100%;display:grid;grid-template-columns:1fr auto;gap:14px;text-align:left;border:0;border-radius:7px;padding:9px 10px;background:transparent;color:#e5e7eb;cursor:pointer}.hermes-file-list button:hover,.hermes-file-list button:focus{background:#202941}.hermes-file-meta{color:#8d98ad;font-size:12px}.hermes-file-viewer pre{box-sizing:border-box;margin:0;padding:18px;max-height:calc(65vh - 64px);overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;color:#d9e2f1;font:13px/1.55 ui-monospace,SFMono-Regular,Consolas,monospace}#files-status{color:#9ca3af}@media(max-width:800px){.hermes-files-header{display:block}.hermes-files-header label{margin-top:14px}.hermes-files-header select{width:100%;min-width:0}.hermes-files-grid{grid-template-columns:1fr}.hermes-files-grid>section{min-height:38vh}}
|
||||||
`
|
`
|
||||||
|
|
||||||
@ -123,14 +133,22 @@ const bridgeJS = `(() => {
|
|||||||
try { return new URL(link.href, location.href).pathname === '/files'; } catch (_) { return false; }
|
try { return new URL(link.href, location.href).pathname === '/files'; } catch (_) { return false; }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!files) return;
|
if (files) labelLink(files, 'hermes-files-sidebar', '/private-files' + (match ? '?session_id=' + encodeURIComponent(match[1]) : ''), 'Files', 'Browse private Hermes files');
|
||||||
labelLink(files, 'hermes-files-sidebar', '/private-files' + (match ? '?session_id=' + encodeURIComponent(match[1]) : ''), 'Files', 'Browse private Hermes files');
|
|
||||||
let telegram = document.getElementById('hermes-telegram-sidebar');
|
let telegram = document.getElementById('hermes-telegram-sidebar');
|
||||||
if (!telegram) {
|
if (!telegram) {
|
||||||
telegram = files.cloneNode(true);
|
const rail = document.querySelector('.rail');
|
||||||
files.after(telegram);
|
if (!rail) return;
|
||||||
|
telegram = document.createElement('button');
|
||||||
|
telegram.type = 'button';
|
||||||
|
telegram.id = 'hermes-telegram-sidebar';
|
||||||
|
telegram.className = 'rail-btn nav-tab has-tooltip';
|
||||||
|
telegram.setAttribute('data-tooltip', 'Telegram');
|
||||||
|
telegram.setAttribute('aria-label', 'Connect this account to the shared Hermes Telegram bot');
|
||||||
|
telegram.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 2 9.5 14.5"/><path d="m22 2-7 20-4-9-9-4Z"/></svg>';
|
||||||
|
telegram.addEventListener('click', () => { location.assign('/telegram'); });
|
||||||
|
const anchor = rail.querySelector('[data-panel="logs"]') || rail.querySelector('.rail-spacer');
|
||||||
|
rail.insertBefore(telegram, anchor || null);
|
||||||
}
|
}
|
||||||
labelLink(telegram, 'hermes-telegram-sidebar', '/telegram', 'Telegram', 'Connect this account to the shared Hermes Telegram bot');
|
|
||||||
};
|
};
|
||||||
let scheduled = false;
|
let scheduled = false;
|
||||||
const observer = new MutationObserver(() => {
|
const observer = new MutationObserver(() => {
|
||||||
@ -240,6 +258,7 @@ const bridgeJS = `(() => {
|
|||||||
const result = document.getElementById('telegram-result');
|
const result = document.getElementById('telegram-result');
|
||||||
const linkButton = document.getElementById('telegram-link');
|
const linkButton = document.getElementById('telegram-link');
|
||||||
const unlinkButton = document.getElementById('telegram-unlink');
|
const unlinkButton = document.getElementById('telegram-unlink');
|
||||||
|
const operatorSetup = document.getElementById('telegram-operator-setup');
|
||||||
const action = async (path) => {
|
const action = async (path) => {
|
||||||
const response = await fetch(path, {method:'POST',headers:{'Content-Type':'application/json','X-Hermes-Action':'telegram-link'},body:'{}'});
|
const response = await fetch(path, {method:'POST',headers:{'Content-Type':'application/json','X-Hermes-Action':'telegram-link'},body:'{}'});
|
||||||
const payload = await response.json();
|
const payload = await response.json();
|
||||||
@ -254,8 +273,10 @@ const bridgeJS = `(() => {
|
|||||||
status.textContent = 'Telegram is not active yet: the operator must add the BotFather bot token before account links can be created.';
|
status.textContent = 'Telegram is not active yet: the operator must add the BotFather bot token before account links can be created.';
|
||||||
linkButton.hidden = true;
|
linkButton.hidden = true;
|
||||||
unlinkButton.hidden = true;
|
unlinkButton.hidden = true;
|
||||||
|
operatorSetup.hidden = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
operatorSetup.hidden = true;
|
||||||
linkButton.hidden = false;
|
linkButton.hidden = false;
|
||||||
linkButton.disabled = false;
|
linkButton.disabled = false;
|
||||||
status.textContent = payload.linked ? 'Telegram is linked to this private account.' : 'Telegram is ready to link.';
|
status.textContent = payload.linked ? 'Telegram is linked to this private account.' : 'Telegram is ready to link.';
|
||||||
@ -410,8 +431,8 @@ func injectChatBridge(response *http.Response) error {
|
|||||||
_ = response.Body.Close()
|
_ = response.Body.Close()
|
||||||
content := string(body)
|
content := string(body)
|
||||||
if !strings.Contains(content, "hermes-chat-bridge.js") {
|
if !strings.Contains(content, "hermes-chat-bridge.js") {
|
||||||
content = strings.Replace(content, "</head>", `<link rel="stylesheet" href="/hermes-chat-bridge.css?v=20260812-1"></head>`, 1)
|
content = strings.Replace(content, "</head>", `<link rel="stylesheet" href="/hermes-chat-bridge.css?v=20260813-telegram-setup"></head>`, 1)
|
||||||
content = strings.Replace(content, "</body>", `<script src="/hermes-chat-bridge.js?v=20260812-1" defer></script></body>`, 1)
|
content = strings.Replace(content, "</body>", `<script src="/hermes-chat-bridge.js?v=20260813-telegram-setup" defer></script></body>`, 1)
|
||||||
}
|
}
|
||||||
response.Body = io.NopCloser(strings.NewReader(content))
|
response.Body = io.NopCloser(strings.NewReader(content))
|
||||||
response.ContentLength = int64(len(content))
|
response.ContentLength = int64(len(content))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user