2026-05-31 23:58:26 +09:00
// static/js/admin.js — Admin panel module (ES6)
// Admin-only: users, endpoints, MCP, RAG, embeddings, tokens, webhooks, features
import uiModule from './ui.js' ;
import settingsModule from './settings.js' ;
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
import { providerLogo , providerLogoFromUrl } from './providers.js' ;
2026-06-01 15:26:10 +10:00
import { sortModelObjects } from './modelSort.js' ;
feat: add ChatGPT Subscription provider (#2876)
* feat: Add ChatGPT Subscription support and related features
- Introduced a new provider option for ChatGPT Subscription in the endpoint selection UI.
- Implemented OAuth flow for ChatGPT Subscription sign-in, including polling for authorization status.
- Updated admin interface to handle ChatGPT Subscription, including disabling API key input and providing user guidance.
- Enhanced cost tracking logic to differentiate between subscription and non-subscription endpoints.
- Added new slash commands for managing skills, including listing, searching, and invoking skills.
- Implemented caching for skill catalog to optimize performance.
- Updated tests to cover new ChatGPT Subscription functionality and ensure proper endpoint probing.
- Refactored existing code to accommodate new features and improve maintainability.
* refactor: share provider device-flow setup
- reuse one device-flow backend for Copilot and ChatGPT Subscription
- add one frontend device-flow helper for Settings and /setup
- put GitHub Copilot back into Add Models, now as a dropdown option
- make provider selection just select; clicking Add starts sign-in
- stop ChatGPT Subscription setup from opening auth tabs automatically
- make /setup copilot and /setup chatgpt-subscription work from chat
- show ChatGPT Subscription in the /setup suggestions
- show the real error message when setup fails
- add focused tests for the shared flow and setup UI
* feat(chatgpt-subscription): harden credential lifecycle and streamline auth UX
Backend:
- Resolve runtime bearer for provider-auth endpoints at probe time via a
shared _resolve_probe_key() that delegates to resolve_endpoint_runtime,
applied across all probe/refresh call sites.
- Skip live completion probes and health pings for discovery-only providers
(centralized behind _is_discovery_only_provider) — the Codex/Responses API
has no such endpoints, so status is derived from cached models.
- Never persist the short lived ChatGPT bearer to the plaintext sessions
table; proactively clear any stale bearer left by an earlier code path.
- Revoke orphaned ProviderAuthSession credentials when the last endpoint
backing them is deleted (_delete_orphaned_provider_auth), surfaced via
cleared_provider_auth in the delete response.
Frontend (admin.js):
- Auto-start the device-auth flow on provider selection so the authorization
panel (code + Authorize) shows immediately instead of behind a "Sign in" click.
- Remove the redundant top button for device auth providers, move retry
into the panel via an inline "Try again".
- Drop the self-evident hint text and add an execCommand clipboard fallback so
Copy works in non-secure (HTTP/LAN) contexts.
* fix: harden chatgpt subscription provider
* chore: remove PR media from branch
* Fix chatgpt subscription recovery and token handling
---------
Co-authored-by: 5p00kyy <admin@5p00ky.dev>
2026-06-08 18:19:18 +10:00
import { PROVIDER _DEVICE _FLOWS , formatDeviceFlowError , runProviderDeviceFlow } from './providerDeviceFlow.js' ;
2026-05-31 23:58:26 +09:00
let initialized = false ;
let modalEl = null ;
// When the user adds an endpoint, store its id so the next render of
// the endpoints list can flash a glow on that row. Cleared once the
// animation fires.
let _recentlyAddedEpId = null ;
2026-06-16 02:52:15 -05:00
let _authPolicy = { password _min _length : 8 , reserved _usernames : [ ] } ;
2026-05-31 23:58:26 +09:00
function el ( id ) { return document . getElementById ( id ) ; }
function esc ( s ) { return uiModule . esc ( s ) ; }
/ * ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═
USERS TAB
═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ * /
const PRIV _LABELS = {
can _use _agent : 'Agent mode' ,
can _use _browser : 'Browser automation' ,
can _use _bash : 'Shell / Python / Files' ,
can _use _documents : 'Document editor' ,
can _use _research : 'Deep research' ,
can _generate _images : 'Image generation' ,
can _manage _memory : 'Memory & skills' ,
} ;
async function loadUsers ( ) {
const list = el ( 'adm-userList' ) ;
try {
const res = await fetch ( '/api/auth/users' , { credentials : 'same-origin' } ) ;
if ( res . status === 401 || res . status === 403 ) { list . innerHTML = '<div class="admin-empty">Access denied</div>' ; return ; }
const data = await res . json ( ) ;
if ( ! data . users || data . users . length === 0 ) { list . innerHTML = '<div class="admin-empty">No users found</div>' ; return ; }
list . innerHTML = '' ;
data . users . forEach ( u => {
const row = document . createElement ( 'div' ) ;
row . className = 'admin-user-row' ;
// Header: name + badges + delete
const header = document . createElement ( 'div' ) ;
header . style . cssText = 'display:flex;align-items:center;justify-content:space-between;cursor:pointer;padding:4px 0;' ;
const initial = u . username . charAt ( 0 ) . toUpperCase ( ) ;
header . innerHTML = `
< div class = "admin-user-info" >
< div style = "width:28px;height:28px;border-radius:50%;background:color-mix(in srgb, var(--accent) 20%, var(--panel));display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;flex-shrink:0;color:var(--accent);" > $ { esc ( initial ) } < / d i v >
< div >
< span class = "admin-user-name" > $ { esc ( u . username ) } < / s p a n >
$ { u . is _admin ? '<span class="admin-badge" style="margin-left:6px;">ADMIN</span>' : '<span style="font-size:10px;opacity:0.4;display:block;">Click to manage privileges</span>' }
< / d i v >
< / d i v >
< div style = "display:flex;gap:8px;align-items:center;" >
2026-06-15 16:44:27 +06:00
< button class = "admin-btn-sm" data - adm - toggle - admin = "${esc(u.username)}" data - make - admin = "${u.is_admin ? '0' : '1'}" style = "font-size:11px;" > $ { u . is _admin ? 'Revoke admin' : 'Make admin' } < / b u t t o n >
2026-06-01 12:52:58 +09:00
< button class = "admin-btn-sm" data - adm - rename - user = "${esc(u.username)}" style = "font-size:11px;" > Rename < / b u t t o n >
2026-05-31 23:58:26 +09:00
$ { u . is _admin ? '' : ` <button class="admin-btn-delete" data-adm-del-user=" ${ esc ( u . username ) } " style="font-size:11px;">Remove</button> ` }
$ { u . is _admin ? '' : '<svg class="admin-user-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="opacity:0.3;transition:transform 0.2s,opacity 0.2s;"><polyline points="6 9 12 15 18 9"/></svg>' }
< / d i v >
` ;
row . appendChild ( header ) ;
// Privileges panel (hidden by default, not for admins)
if ( ! u . is _admin ) {
const privPanel = document . createElement ( 'div' ) ;
privPanel . className = 'admin-priv-panel hidden' ;
privPanel . style . cssText = 'padding:8px 0 4px;border-top:1px solid var(--border);margin-top:8px;' ;
// Boolean toggles
let html = '<div style="font-size:10px;text-transform:uppercase;letter-spacing:0.5px;opacity:0.35;font-weight:600;margin-bottom:4px;">Features</div>' ;
for ( const [ key , label ] of Object . entries ( PRIV _LABELS ) ) {
const checked = u . privileges && u . privileges [ key ] ? 'checked' : '' ;
html += ` <div style="display:flex;align-items:center;justify-content:space-between;padding:4px 0;">
< span style = "font-size:12px;" > $ { label } < / s p a n >
< label class = "admin-switch" style = "transform:scale(0.85);" > < input type = "checkbox" data - priv = "${key}" data - user = "${esc(u.username)}" $ { checked } > < span class = "admin-slider" > < / s p a n > < / l a b e l >
< / d i v > ` ;
}
// Rate limit
html += '<div style="font-size:10px;text-transform:uppercase;letter-spacing:0.5px;opacity:0.35;font-weight:600;margin:10px 0 4px;">Limits</div>' ;
const maxMsg = ( u . privileges && u . privileges . max _messages _per _day ) || 0 ;
html += ` <div style="display:flex;align-items:center;justify-content:space-between;padding:4px 0;">
< div >
< span style = "font-size:12px;" > Daily message limit < / s p a n >
< div style = "font-size:10px;opacity:0.4;" > 0 = no limit < / d i v >
< / d i v >
< input type = "number" min = "0" value = "${maxMsg}" data - priv = "max_messages_per_day" data - user = "${esc(u.username)}" style = "width:70px;padding:4px 6px;background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--fg);font-size:12px;text-align:center;" >
< / d i v > ` ;
// Allowed models — checkbox list
2026-06-05 13:27:10 -05:00
const allowedModels = Array . isArray ( u . privileges && u . privileges . allowed _models )
? u . privileges . allowed _models
: [ ] ;
const allowedSet = new Set ( allowedModels ) ;
const modelsRestricted = ! ! ( u . privileges && u . privileges . allowed _models _restricted ) ;
2026-06-08 17:52:39 -03:00
const blockAllModels = ! ! ( u . privileges && u . privileges . block _all _models ) ;
2026-05-31 23:58:26 +09:00
html += ` <div style="padding:4px 0;">
< div style = "display:flex;align-items:center;justify-content:space-between;" >
< span style = "font-size:12px;" > Allowed models < / s p a n >
< div style = "display:flex;gap:8px;" >
< a href = "#" class = "priv-models-all" data - user = "${esc(u.username)}" style = "font-size:10px;opacity:0.5;" > All < / a >
< a href = "#" class = "priv-models-none" data - user = "${esc(u.username)}" style = "font-size:10px;opacity:0.5;" > None < / a >
< / d i v >
< / d i v >
2026-06-08 17:52:39 -03:00
< div style = "font-size:10px;opacity:0.4;margin-bottom:4px;" > $ { blockAllModels ? 'No models allowed' : ( ! modelsRestricted ? 'All models allowed (no restrictions)' : ( allowedSet . size === 0 ? 'No models allowed' : allowedSet . size + ' model(s) allowed' ) ) } < / d i v >
2026-05-31 23:58:26 +09:00
< div class = "priv-models-list" data - user = "${esc(u.username)}" >
< span style = "opacity:0.4;font-size:11px;" > Loading models ... < / s p a n >
< / d i v >
< / d i v > ` ;
privPanel . innerHTML = html ;
row . appendChild ( privPanel ) ;
// Toggle panel visibility + rotate chevron + load models
let _modelsLoaded = false ;
header . addEventListener ( 'click' , ( e ) => {
2026-06-15 16:44:27 +06:00
if ( e . target . closest ( '.admin-btn-delete, [data-adm-rename-user], [data-adm-toggle-admin]' ) ) return ;
2026-05-31 23:58:26 +09:00
privPanel . classList . toggle ( 'hidden' ) ;
const chevron = header . querySelector ( '.admin-user-chevron' ) ;
if ( chevron ) {
const isOpen = ! privPanel . classList . contains ( 'hidden' ) ;
chevron . style . transform = isOpen ? 'rotate(180deg)' : '' ;
chevron . style . opacity = isOpen ? '0.7' : '0.3' ;
}
// Load models list on first expand
if ( ! _modelsLoaded && ! privPanel . classList . contains ( 'hidden' ) ) {
_modelsLoaded = true ;
2026-06-08 17:52:39 -03:00
_loadModelsForUser ( u . username , allowedSet , modelsRestricted , blockAllModels , privPanel ) ;
2026-05-31 23:58:26 +09:00
}
} ) ;
// Wire privilege changes (boolean + number inputs, not model checkboxes)
privPanel . querySelectorAll ( '[data-priv]' ) . forEach ( input => {
const handler = async ( ) => {
const username = input . dataset . user ;
const key = input . dataset . priv ;
let value ;
if ( input . type === 'checkbox' ) value = input . checked ;
else if ( input . type === 'number' ) value = parseInt ( input . value ) || 0 ;
else value = input . value ;
try {
await fetch ( ` /api/auth/users/ ${ encodeURIComponent ( username ) } /privileges ` , {
method : 'PUT' , credentials : 'same-origin' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { [ key ] : value } ) ,
} ) ;
} catch ( e ) { uiModule . showError ( 'Failed to update privilege' ) ; }
} ;
if ( input . type === 'checkbox' ) input . addEventListener ( 'change' , handler ) ;
else input . addEventListener ( 'change' , handler ) ;
} ) ;
}
2026-06-01 12:52:58 +09:00
// Rename button
const renameBtn = row . querySelector ( '[data-adm-rename-user]' ) ;
if ( renameBtn ) {
renameBtn . addEventListener ( 'click' , async ( e ) => {
e . stopPropagation ( ) ;
const oldUsername = renameBtn . dataset . admRenameUser ;
const next = await uiModule . styledPrompt ( ` Rename " ${ oldUsername } " ` , {
defaultValue : oldUsername ,
placeholder : 'New username' ,
confirmText : 'Rename' ,
} ) ;
const username = ( next || '' ) . trim ( ) ;
if ( ! username || username === oldUsername ) return ;
try {
const res = await fetch ( ` /api/auth/users/ ${ encodeURIComponent ( oldUsername ) } /rename ` , {
method : 'PUT' ,
credentials : 'same-origin' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { username } ) ,
} ) ;
const data = await res . json ( ) . catch ( ( ) => ( { } ) ) ;
if ( ! res . ok ) {
uiModule . showError ( data . detail || 'Failed to rename user' ) ;
return ;
}
if ( data . renamed _self ) {
window . location . reload ( ) ;
return ;
}
loadUsers ( ) ;
} catch ( err ) {
uiModule . showError ( 'Failed to rename user' ) ;
}
} ) ;
}
2026-05-31 23:58:26 +09:00
// Delete button
const delBtn = row . querySelector ( '[data-adm-del-user]' ) ;
if ( delBtn ) {
delBtn . addEventListener ( 'click' , async ( e ) => {
e . stopPropagation ( ) ;
const username = delBtn . dataset . admDelUser ;
if ( ! await uiModule . styledConfirm ( ` Remove user " ${ username } "? ` , { confirmText : 'Remove' , danger : true } ) ) return ;
const res = await fetch ( '/api/auth/users' , { method : 'DELETE' , credentials : 'same-origin' , headers : { 'Content-Type' : 'application/json' } , body : JSON . stringify ( { username } ) } ) ;
if ( res . ok ) loadUsers ( ) ;
else uiModule . showError ( 'Failed to delete user' ) ;
} ) ;
}
2026-06-15 16:44:27 +06:00
// Promote / demote (admin toggle) — present on every row
const adminToggleBtn = row . querySelector ( '[data-adm-toggle-admin]' ) ;
if ( adminToggleBtn ) {
adminToggleBtn . addEventListener ( 'click' , async ( e ) => {
e . stopPropagation ( ) ;
const username = adminToggleBtn . dataset . admToggleAdmin ;
const makeAdmin = adminToggleBtn . dataset . makeAdmin === '1' ;
const confirmMsg = makeAdmin
? ` Grant admin rights to " ${ username } "? They'll get full access to all settings and users — including the power to demote or remove other admins (you included). `
: ` Revoke admin rights from " ${ username } "? They'll lose access to the admin panel. ` ;
if ( ! await uiModule . styledConfirm ( confirmMsg , { confirmText : makeAdmin ? 'Make admin' : 'Revoke admin' , danger : ! makeAdmin } ) ) return ;
adminToggleBtn . disabled = true ;
try {
const res = await fetch ( ` /api/auth/users/ ${ encodeURIComponent ( username ) } /admin ` , {
method : 'PUT' ,
credentials : 'same-origin' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { is _admin : makeAdmin } ) ,
} ) ;
const data = await res . json ( ) . catch ( ( ) => ( { } ) ) ;
if ( ! res . ok ) {
uiModule . showError ( data . detail || 'Failed to change admin status' ) ;
adminToggleBtn . disabled = false ;
return ;
}
// Demoting yourself drops your own admin access — reload into the
// normal-user view (mirrors the rename-self reload above).
if ( data . self ) { window . location . reload ( ) ; return ; }
loadUsers ( ) ;
} catch ( err ) {
uiModule . showError ( 'Failed to change admin status' ) ;
adminToggleBtn . disabled = false ;
}
} ) ;
}
2026-05-31 23:58:26 +09:00
list . appendChild ( row ) ;
} ) ;
} catch ( e ) { list . innerHTML = '<div class="admin-error">Failed to load users</div>' ; }
}
2026-06-08 17:52:39 -03:00
async function _loadModelsForUser ( username , allowedSet , modelsRestricted , blockAllModels , privPanel ) {
2026-05-31 23:58:26 +09:00
const listEl = privPanel . querySelector ( ` .priv-models-list[data-user=" ${ username } "] ` ) ;
if ( ! listEl ) return ;
try {
2026-06-08 17:52:39 -03:00
// Use /api/model-endpoints rather than /api/models — the latter is
// backed by `cached_models`, so endpoints that haven't been probed yet
// (e.g. a freshly-added cloud API like DeepSeek) simply don't show up
// until some other endpoint happens to trigger a cache refresh. The
// endpoints listing always reflects every configured endpoint.
const res = await fetch ( '/api/model-endpoints' , { credentials : 'same-origin' } ) ;
2026-05-31 23:58:26 +09:00
const data = await res . json ( ) ;
const allModels = [ ] ;
2026-06-08 17:52:39 -03:00
( Array . isArray ( data ) ? data : [ ] ) . forEach ( ep => {
if ( ! ep . online ) return ;
( ep . models || [ ] ) . forEach ( mid => {
allModels . push ( { mid , epName : ep . name || '' , display : mid . split ( '/' ) . pop ( ) } ) ;
2026-05-31 23:58:26 +09:00
} ) ;
} ) ;
if ( ! allModels . length ) {
listEl . innerHTML = '<span style="opacity:0.4;font-size:11px;">No models available</span>' ;
return ;
}
2026-06-05 13:27:10 -05:00
let restricted = modelsRestricted ;
2026-06-08 17:52:39 -03:00
let blockAll = blockAllModels ;
2026-06-01 15:26:10 +10:00
listEl . innerHTML = sortModelObjects ( allModels ) . map ( m => {
2026-06-08 17:52:39 -03:00
const checked = ! blockAll && ( ! restricted || allowedSet . has ( m . mid ) ) ? 'checked' : '' ;
2026-05-31 23:58:26 +09:00
return ` <label>
< input type = "checkbox" class = "priv-model-cb" data - mid = "${esc(m.mid)}" $ { checked } >
< span > $ { esc ( m . display ) } < / s p a n >
< span style = "opacity:0.3;font-size:10px;margin-left:auto;" > $ { esc ( m . epName ) } < / s p a n >
< / l a b e l > ` ;
} ) . join ( '' ) ;
// Save on change
function _saveModels ( ) {
const checked = [ ] ;
listEl . querySelectorAll ( '.priv-model-cb' ) . forEach ( cb => {
if ( cb . checked ) checked . push ( cb . dataset . mid ) ;
} ) ;
2026-06-08 17:52:39 -03:00
// Three distinct states the backend must be able to tell apart:
// - all checked -> no restriction (allowed_models: [], block_all_models: false)
// - none checked -> block everything (allowed_models: [], block_all_models: true)
// - some checked -> allowlist (allowed_models: checked, block_all_models: false)
let value , hintText ;
if ( checked . length === allModels . length ) {
restricted = false ;
blockAll = false ;
value = [ ] ;
hintText = 'All models allowed (no restrictions)' ;
} else if ( checked . length === 0 ) {
restricted = true ;
blockAll = true ;
value = [ ] ;
hintText = 'No models allowed' ;
} else {
restricted = true ;
blockAll = false ;
value = checked ;
hintText = value . length + ' model(s) allowed' ;
}
2026-05-31 23:58:26 +09:00
const hint = privPanel . querySelector ( '.priv-models-list[data-user]' ) ? . previousElementSibling ? . querySelector ( 'div[style*="opacity"]' ) ;
2026-06-08 17:52:39 -03:00
if ( hint ) hint . textContent = hintText ;
2026-05-31 23:58:26 +09:00
fetch ( ` /api/auth/users/ ${ encodeURIComponent ( username ) } /privileges ` , {
method : 'PUT' , credentials : 'same-origin' ,
headers : { 'Content-Type' : 'application/json' } ,
2026-06-08 17:52:39 -03:00
body : JSON . stringify ( { allowed _models : value , allowed _models _restricted : restricted , block _all _models : blockAll } ) ,
2026-05-31 23:58:26 +09:00
} ) . catch ( ( ) => { } ) ;
}
listEl . querySelectorAll ( '.priv-model-cb' ) . forEach ( cb => cb . addEventListener ( 'change' , _saveModels ) ) ;
// All / None buttons
privPanel . querySelector ( ` .priv-models-all[data-user=" ${ username } "] ` ) ? . addEventListener ( 'click' , ( e ) => {
e . preventDefault ( ) ;
listEl . querySelectorAll ( '.priv-model-cb' ) . forEach ( cb => cb . checked = true ) ;
_saveModels ( ) ;
} ) ;
privPanel . querySelector ( ` .priv-models-none[data-user=" ${ username } "] ` ) ? . addEventListener ( 'click' , ( e ) => {
e . preventDefault ( ) ;
listEl . querySelectorAll ( '.priv-model-cb' ) . forEach ( cb => cb . checked = false ) ;
_saveModels ( ) ;
} ) ;
} catch ( e ) {
listEl . innerHTML = '<span style="opacity:0.4;font-size:11px;">Failed to load models</span>' ;
}
}
function initSignupToggle ( ) {
const toggle = el ( 'adm-signupToggle' ) ;
fetch ( '/api/auth/status' , { credentials : 'same-origin' } )
. then ( r => r . json ( ) )
. then ( d => { toggle . checked = ! ! d . signup _enabled ; } )
. catch ( e => console . warn ( 'Auth status fetch failed:' , e ) ) ;
toggle . addEventListener ( 'change' , async ( ) => {
try {
const res = await fetch ( '/api/auth/signup-toggle' , { method : 'POST' , credentials : 'same-origin' } ) ;
const data = await res . json ( ) ;
toggle . checked = data . signup _enabled ;
} catch ( e ) { toggle . checked = ! toggle . checked ; }
} ) ;
}
2026-06-23 23:06:45 +02:00
function initShareDefaultsToggle ( ) {
const toggle = el ( 'adm-shareDefaultsToggle' ) ;
fetch ( '/api/auth/settings' , { credentials : 'same-origin' } )
. then ( r => r . json ( ) )
. then ( d => { toggle . checked = ! ! d . share _defaults _with _users ; } )
. catch ( e => console . warn ( 'Settings fetch failed:' , e ) ) ;
toggle . addEventListener ( 'change' , async ( ) => {
try {
const res = await fetch ( '/api/auth/settings' , {
method : 'POST' ,
credentials : 'same-origin' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { share _defaults _with _users : toggle . checked } ) ,
} ) ;
const data = await res . json ( ) ;
toggle . checked = ! ! data . share _defaults _with _users ;
} catch ( e ) {
toggle . checked = ! toggle . checked ;
}
} ) ;
}
2026-05-31 23:58:26 +09:00
function initAddUser ( ) {
2026-06-16 02:52:15 -05:00
fetch ( '/api/auth/policy' , { credentials : 'same-origin' } )
. then ( r => r . ok ? r . json ( ) : null )
. then ( policy => {
if ( ! policy ) return ;
_authPolicy = policy ;
const admPw = el ( 'adm-newPassword' ) ;
if ( admPw ) admPw . placeholder = ` Password (min ${ policy . password _min _length } ) ` ;
} )
. catch ( ( ) => { } ) ;
2026-05-31 23:58:26 +09:00
el ( 'adm-addBtn' ) . addEventListener ( 'click' , async ( ) => {
const msg = el ( 'adm-addMsg' ) ;
msg . textContent = '' ; msg . className = '' ;
const username = el ( 'adm-newUsername' ) . value . trim ( ) ;
const password = el ( 'adm-newPassword' ) . value ;
const is _admin = el ( 'adm-newIsAdmin' ) . checked ;
if ( ! username ) { msg . textContent = 'Username required' ; msg . className = 'admin-error' ; return ; }
2026-06-16 02:52:15 -05:00
if ( password . length < _authPolicy . password _min _length ) { msg . textContent = ` Password must be at least ${ _authPolicy . password _min _length } characters ` ; msg . className = 'admin-error' ; return ; }
if ( _authPolicy . reserved _usernames . includes ( username . toLowerCase ( ) ) ) { msg . textContent = 'This username is reserved' ; msg . className = 'admin-error' ; return ; }
2026-05-31 23:58:26 +09:00
el ( 'adm-addBtn' ) . disabled = true ;
try {
const res = await fetch ( '/api/auth/users' , { method : 'POST' , credentials : 'same-origin' , headers : { 'Content-Type' : 'application/json' } , body : JSON . stringify ( { username , password , is _admin } ) } ) ;
const data = await res . json ( ) ;
if ( res . ok ) { msg . textContent = 'User created' ; msg . className = 'admin-success' ; el ( 'adm-newUsername' ) . value = '' ; el ( 'adm-newPassword' ) . value = '' ; el ( 'adm-newIsAdmin' ) . checked = false ; loadUsers ( ) ; }
else { msg . textContent = data . detail || 'Failed' ; msg . className = 'admin-error' ; }
} catch ( e ) { msg . textContent = 'Request failed' ; msg . className = 'admin-error' ; }
el ( 'adm-addBtn' ) . disabled = false ;
} ) ;
}
/ * ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═
SERVICES TAB — Endpoints
═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ * /
function _isLocalEndpoint ( url ) {
if ( ! url ) return false ;
try {
const u = new URL ( url ) ;
const h = u . hostname . toLowerCase ( ) ;
if ( h === 'localhost' || h === '127.0.0.1' || h === '0.0.0.0' ) return true ;
if ( h . endsWith ( '.local' ) ) return true ;
if ( /^10\./ . test ( h ) ) return true ;
if ( /^192\.168\./ . test ( h ) ) return true ;
if ( /^172\.(1[6-9]|2[0-9]|3[01])\./ . test ( h ) ) return true ;
// Tailscale CGNAT range (100.64.0.0/10 → 100.64.x– 100.127.x). Servers
// found via "Scan for Servers" come back as tailnet IPs, which are still
// your own machines, so group them under Local rather than API.
if ( /^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./ . test ( h ) ) return true ;
// Single-label hostnames are LAN by convention.
if ( ! h . includes ( '.' ) ) return true ;
return false ;
} catch { return false ; }
}
2026-06-01 10:00:15 +09:00
async function _refreshAfterEndpointChange ( deletedEndpointId ) {
try {
const sm = window . sessionModule ;
const pending = sm && sm . getPendingChat ? sm . getPendingChat ( ) : null ;
if ( deletedEndpointId && pending && String ( pending . endpointId || '' ) === String ( deletedEndpointId ) ) {
if ( sm . setPendingChat ) sm . setPendingChat ( null ) ;
}
} catch ( _ ) { }
try {
if ( window . modelsModule && window . modelsModule . refreshModels ) {
await window . modelsModule . refreshModels ( true ) ;
}
} catch ( _ ) { }
try {
window . dispatchEvent ( new CustomEvent ( 'ge:model-endpoints-updated' , {
detail : { deletedEndpointId : deletedEndpointId || null }
} ) ) ;
} catch ( _ ) { }
try {
if ( window . sessionModule && window . sessionModule . updateModelPicker ) {
window . sessionModule . updateModelPicker ( ) ;
}
} catch ( _ ) { }
}
async function _selectAddedModelInChat ( endpoint ) {
const modelId = endpoint && Array . isArray ( endpoint . models ) ? endpoint . models [ 0 ] : '' ;
if ( ! modelId ) return ;
try {
if ( window . modelsModule && window . modelsModule . refreshModels ) {
await window . modelsModule . refreshModels ( true ) ;
}
} catch ( _ ) { }
try {
document . dispatchEvent ( new CustomEvent ( 'odysseus:auto-select-model' , {
detail : {
endpointId : endpoint . id || '' ,
endpointName : endpoint . name || '' ,
modelId ,
url : endpoint . base _url || '' ,
}
} ) ) ;
} catch ( _ ) { }
}
2026-05-31 23:58:26 +09:00
async function loadEndpoints ( ) {
const listLocal = el ( 'adm-epList-local' ) ;
const listApi = el ( 'adm-epList-api' ) ;
// Fallback to the legacy single list if the split containers don't exist
// (older HTML or third-party embedding).
const listLegacy = el ( 'adm-epList' ) ;
// Refresh model picker so new endpoints show up in chat
if ( window . modelsModule && window . modelsModule . refreshModels ) {
2026-06-04 00:56:11 -03:00
window . modelsModule . refreshModels ( ) ;
2026-05-31 23:58:26 +09:00
setTimeout ( ( ) => {
if ( window . sessionModule && window . sessionModule . updateModelPicker ) {
window . sessionModule . updateModelPicker ( ) ;
}
} , 1500 ) ;
}
2026-06-01 15:26:10 +10:00
if ( settingsModule && typeof settingsModule . refreshAiModelEndpoints === 'function' ) {
settingsModule . refreshAiModelEndpoints ( ) ;
}
2026-05-31 23:58:26 +09:00
try {
const res = await fetch ( '/api/model-endpoints' , { credentials : 'same-origin' } ) ;
// Treat a non-OK response (e.g. 401/403 for non-admins, or backend
// returning an error envelope) the same as "no endpoints yet": show the
// empty state, not "Failed to load". The user just installed the app —
// there's literally nothing to load, so the error read as broken UI.
let data = [ ] ;
if ( res . ok ) {
try { data = await res . json ( ) ; } catch { data = [ ] ; }
}
if ( ! Array . isArray ( data ) || data . length === 0 ) {
2026-06-01 10:00:15 +09:00
const empty = '<div class="admin-empty">None</div>' ;
2026-05-31 23:58:26 +09:00
if ( listLocal ) listLocal . innerHTML = empty ;
if ( listApi ) listApi . innerHTML = '<div class="admin-empty">None</div>' ;
if ( listLegacy ) listLegacy . innerHTML = empty ;
return ;
}
const rowHtml = data . map ( ep => {
2026-06-28 05:29:25 +00:00
const epModels = Array . isArray ( ep . models ) ? ep . models : [ ] ;
const visibleCount = epModels . length ;
2026-05-31 23:58:26 +09:00
const totalCount = visibleCount + ( ep . hidden _count || 0 ) ;
// `ep.models` is the *visible* set — when every model is hidden it's
// empty, but we still need to render the expand panel so the user can
// un-hide them. Gate on the total instead.
const hasModels = ep . online && totalCount > 0 ;
2026-06-01 10:00:15 +09:00
const statusBadge = ep . status === 'empty'
? '<span class="admin-badge">no models</span>'
: ep . online
? ` <span class="admin-badge"> ${ visibleCount } / ${ totalCount } models enabled</span> `
: '<span class="admin-badge admin-badge-off">offline</span>' ;
2026-05-31 23:58:26 +09:00
const justAddedClass = ( _recentlyAddedEpId && String ( ep . id ) === _recentlyAddedEpId ) ? ' adm-ep-just-added' : '' ;
2026-06-04 00:56:11 -03:00
const category = ep . category || ( _isLocalEndpoint ( ep . base _url ) ? 'local' : 'api' ) ;
const kindLabel = ep . endpoint _kind && ep . endpoint _kind !== 'auto' ? ep . endpoint _kind . toUpperCase ( ) : '' ;
2026-06-05 15:12:14 -04:00
const keyLabel = ep . has _key
? ( ep . api _key _fingerprint ? ` (key ${ esc ( ep . api _key _fingerprint ) } ) ` : ' (key set)' )
: '' ;
2026-05-31 23:58:26 +09:00
return `
< div class = "admin-user-row${ep.is_enabled ? '' : ' admin-ep-disabled'}${justAddedClass}" data - adm - ep - id = "${ep.id}" >
< div style = "display:flex;align-items:center;justify-content:space-between;${hasModels ? 'cursor:pointer;' : ''}padding:4px 0;" data - adm - ep - header = "${ep.id}" >
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
< div class = "admin-user-info" style = "flex:1;flex-wrap:wrap;gap:0.3rem;align-items:center;" >
< span class = "adm-ep-row-logo" style = "display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;flex-shrink:0;opacity:0.9;" > $ { providerLogoFromUrl ( ep . base _url ) || '' } < / s p a n >
2026-05-31 23:58:26 +09:00
< span class = "admin-user-name" > $ { esc ( ep . name ) } < / s p a n >
$ { ep . model _type === 'image' ? '<span class="admin-badge" style="background:color-mix(in srgb, var(--accent) 20%, transparent);color:var(--accent);">Image</span>' : '' }
2026-06-04 00:56:11 -03:00
$ { kindLabel ? ` <span class="admin-badge"> ${ esc ( kindLabel ) } </span> ` : '' }
2026-05-31 23:58:26 +09:00
$ { statusBadge }
$ { ep . is _enabled ? '' : '<span class="admin-badge admin-badge-off">disabled</span>' }
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
$ { hasModels ? ` <span style="font-size:10px;opacity:0.4; ${ category === 'api' ? 'flex-basis:100%;' : '' } ">Click to manage models</span> ` : '' }
2026-05-31 23:58:26 +09:00
< / d i v >
2026-06-08 21:46:01 +09:00
< div style = "display:flex;gap:4px;align-items:center;" >
2026-05-31 23:58:26 +09:00
< button class = "admin-btn-sm" data - adm - toggle - ep = "${ep.id}" > $ { ep . is _enabled ? 'Disable' : 'Enable' } < / b u t t o n >
< button class = "admin-btn-delete" data - adm - del - ep = "${ep.id}" data - adm - ep - online = "${ep.online ? '1' : '0'}" > Delete < / b u t t o n >
$ { hasModels ? '<svg class="admin-user-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="opacity:0.3;transition:transform 0.2s,opacity 0.2s;"><polyline points="6 9 12 15 18 9"/></svg>' : '' }
< / d i v >
< / d i v >
2026-06-05 15:12:14 -04:00
< div class = "admin-ep-detail" > $ { esc ( ep . base _url ) } $ { category === 'local' ? ` <button type="button" class="admin-ep-copy-btn" data-adm-copy-url=" ${ esc ( ep . base _url ) } " title="Copy URL" aria-label="Copy URL" style="background:none;border:none;padding:0 2px;margin-left:6px;cursor:pointer;color:inherit;opacity:0.45;vertical-align:-2px;line-height:1;"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button> ` : '' } $ { keyLabel } < / d i v >
2026-05-31 23:58:26 +09:00
$ { hasModels ? ` <div class="mcp-tools-panel hidden" data-adm-ep-models-panel=" ${ ep . id } "></div> ` : '' }
< / d i v > ` ;
} ) ;
// Partition rows into Local vs API for the split sections.
// Subsections without any rows are hidden entirely (heading + all)
// so empty groups don't take up vertical real estate.
const _renderInto = ( container , indices ) => {
if ( ! container ) return ;
const section = container . closest ( '.adm-ep-section' ) ;
if ( ! indices . length ) {
if ( section ) section . style . display = 'none' ;
container . innerHTML = '' ;
return ;
}
if ( section ) section . style . display = '' ;
container . innerHTML = indices . map ( i => rowHtml [ i ] ) . join ( '' ) ;
} ;
const localIdx = [ ] , apiIdx = [ ] ;
2026-06-04 00:56:11 -03:00
data . forEach ( ( ep , i ) => ( ( ep . category || ( _isLocalEndpoint ( ep . base _url ) ? 'local' : 'api' ) ) === 'local' ? localIdx : apiIdx ) . push ( i ) ) ;
2026-05-31 23:58:26 +09:00
// Sort each section: enabled endpoints first, disabled at the bottom.
// Preserve original order within each group via stable sort.
const _sortByEnabled = ( a , b ) => Number ( ! ! data [ b ] . is _enabled ) - Number ( ! ! data [ a ] . is _enabled ) ;
localIdx . sort ( _sortByEnabled ) ;
apiIdx . sort ( _sortByEnabled ) ;
_renderInto ( listLocal , localIdx ) ;
_renderInto ( listApi , apiIdx ) ;
if ( listLegacy ) listLegacy . innerHTML = rowHtml . join ( '' ) ;
// Iterate matching nodes across both containers.
const queryAll = ( sel ) => {
const out = [ ] ;
[ listLocal , listApi , listLegacy ] . forEach ( c => {
if ( c ) c . querySelectorAll ( sel ) . forEach ( n => out . push ( n ) ) ;
} ) ;
return out ;
} ;
queryAll ( '[data-adm-toggle-ep]' ) . forEach ( btn => {
btn . addEventListener ( 'click' , async ( e ) => { e . stopPropagation ( ) ; await fetch ( ` /api/model-endpoints/ ${ btn . dataset . admToggleEp } ` , { method : 'PATCH' } ) ; loadEndpoints ( ) ; } ) ;
} ) ;
queryAll ( '[data-adm-copy-url]' ) . forEach ( btn => {
btn . addEventListener ( 'click' , ( e ) => {
e . stopPropagation ( ) ;
const url = btn . dataset . admCopyUrl || '' ;
if ( ! url ) return ;
uiModule . copyToClipboard ( url ) . then ( ( ) => {
// Brief icon swap to a checkmark so the user gets feedback that
// the copy actually happened. Reverts after ~1.4s.
const prev = btn . innerHTML ;
btn . innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>' ;
btn . style . opacity = '1' ;
setTimeout ( ( ) => { btn . innerHTML = prev ; btn . style . opacity = '' ; } , 1400 ) ;
} ) . catch ( ( ) => { } ) ;
} ) ;
} ) ;
queryAll ( '[data-adm-del-ep]' ) . forEach ( btn => {
btn . addEventListener ( 'click' , async ( e ) => {
e . stopPropagation ( ) ;
var epId = btn . dataset . admDelEp ;
var isOffline = btn . dataset . admEpOnline === '0' ;
// Offline endpoints are already broken — skip the confirm dialog
// entirely and delete immediately. The optimistic UI removal makes
// the action feel instant.
if ( ! isOffline ) {
var deps = [ ] ;
try {
var depRes = await fetch ( '/api/model-endpoints/' + epId + '/dependents' , { credentials : 'same-origin' } ) ;
var depData = await depRes . json ( ) ;
deps = depData . dependents || [ ] ;
} catch ( e ) { /* proceed without warning */ }
var msg = 'Delete this endpoint?' ;
if ( deps . length ) {
msg += '\n\nThe following settings use this endpoint and will be reset:\n— ' + deps . join ( '\n— ' ) ;
}
if ( ! await uiModule . styledConfirm ( msg , { confirmText : 'Delete' , danger : true } ) ) return ;
}
// Optimistic: remove from UI immediately
const row = btn . closest ( '[data-adm-ep-id]' ) ;
if ( row ) row . remove ( ) ;
2026-06-01 10:00:15 +09:00
fetch ( '/api/model-endpoints/' + epId , { method : 'DELETE' } )
. then ( ( ) => _refreshAfterEndpointChange ( epId ) )
. then ( ( ) => loadEndpoints ( ) )
. catch ( ( ) => loadEndpoints ( ) ) ;
2026-05-31 23:58:26 +09:00
} ) ;
} ) ;
// Clear the just-added marker now that the row has been rendered
// with the animation class — keeps the glow from re-firing on every
// subsequent loadEndpoints() call (e.g. when toggling a model).
if ( _recentlyAddedEpId ) _recentlyAddedEpId = null ;
// Models expand/collapse (click anywhere on card)
queryAll ( '[data-adm-ep-id]' ) . forEach ( row => {
const header = row . querySelector ( '[data-adm-ep-header]' ) ;
if ( ! header ) return ;
let _modelsLoaded = false ;
row . style . cursor = 'pointer' ;
row . addEventListener ( 'click' , async ( e ) => {
// Don't let interactions inside the expanded panel re-fire the
// expand/collapse handler — the search box was getting closed
// because clicking it bubbled up to here.
if ( e . target . closest ( '.admin-btn-sm, .admin-btn-delete, .mcp-tools-list, .mcp-tools-header, .mcp-tools-search, input, label' ) ) return ;
const epId = header . dataset . admEpHeader ;
const panel = row . querySelector ( ` [data-adm-ep-models-panel=" ${ epId } "] ` ) ;
if ( ! panel ) return ;
panel . classList . toggle ( 'hidden' ) ;
const chevron = row . querySelector ( '.admin-user-chevron' ) ;
const isOpen = ! panel . classList . contains ( 'hidden' ) ;
if ( chevron ) {
chevron . style . transform = isOpen ? 'rotate(180deg)' : '' ;
chevron . style . opacity = isOpen ? '0.7' : '0.3' ;
}
if ( ! _modelsLoaded && isOpen ) {
_modelsLoaded = true ;
// Our shared whirlpool spinner (consistent with the rest of the app).
panel . innerHTML = '' ;
let _modelsSpin = null ;
const _ld = document . createElement ( 'span' ) ;
_ld . style . cssText = 'opacity:0.55;font-size:11px;display:inline-flex;align-items:center;gap:8px;' ;
_ld . appendChild ( document . createTextNode ( 'Loading models…' ) ) ;
try {
const _sp = ( await import ( './spinner.js' ) ) . default ;
_modelsSpin = _sp . createWhirlpool ( 14 ) ;
_modelsSpin . element . style . cssText = 'width:14px;height:14px;margin:0;display:inline-block;' ;
_ld . appendChild ( _modelsSpin . element ) ;
} catch ( _ ) { }
panel . appendChild ( _ld ) ;
const _stopSpin = ( ) => { try { _modelsSpin && _modelsSpin . stop ( ) ; } catch ( _ ) { } } ;
2026-06-04 00:56:11 -03:00
const _loadingHtml = ( label ) => ` <span style="opacity:0.55;font-size:11px;display:inline-flex;align-items:center;gap:8px;"> ${ esc ( label ) } </span> ` ;
const renderModels = ( models , warning = '' ) => {
2026-06-01 15:26:10 +10:00
const sortedModels = sortModelObjects ( models ) ;
2026-06-04 00:56:11 -03:00
const warningHtml = warning ? ` <div class="admin-error" style="font-size:11px;margin:6px 0;"> ${ esc ( warning ) } </div> ` : '' ;
const attachRefresh = ( ) => {
panel . querySelector ( ` [data-ep-refresh-models=" ${ epId } "] ` ) ? . addEventListener ( 'click' , async ( e ) => {
e . preventDefault ( ) ;
panel . innerHTML = _loadingHtml ( 'Refreshing models...' ) ;
try {
const res = await fetch ( ` /api/model-endpoints/ ${ epId } /models?refresh=true&refresh_timeout=60 ` , { credentials : 'same-origin' } ) ;
const refreshWarning = res . headers . get ( 'X-Model-Refresh-Warning' ) || '' ;
if ( ! res . ok ) throw new Error ( ` HTTP ${ res . status } ` ) ;
const refreshedModels = await res . json ( ) ;
renderModels ( refreshedModels , refreshWarning ) ;
if ( refreshWarning && uiModule ? . showToast ) uiModule . showToast ( refreshWarning , 6000 ) ;
} catch ( _ ) {
renderModels ( sortedModels , 'Model refresh failed; kept cached models.' ) ;
}
} ) ;
} ;
if ( ! sortedModels . length ) {
panel . innerHTML = ` <div class="mcp-tools-header">
< span > Models < / s p a n >
< span style = "display:flex;gap:8px;align-items:center;" >
< span class = "mcp-tools-count" > 0 / 0 enabled < / s p a n >
< a href = "#" data - ep - refresh - models = "${epId}" > Refresh < / a >
< / s p a n >
< / d i v > $ { w a r n i n g H t m l } < s p a n s t y l e = " o p a c i t y : 0 . 5 ; f o n t - s i z e : 1 1 p x ; " > N o m o d e l s < / s p a n > ` ;
attachRefresh ( ) ;
return ;
}
2026-06-01 15:26:10 +10:00
const hiddenSet = new Set ( sortedModels . filter ( m => m . is _hidden ) . map ( m => m . id ) ) ;
const showSearch = sortedModels . length >= 8 ;
2026-05-31 23:58:26 +09:00
panel . innerHTML = ` <div class="mcp-tools-header">
< span > Models < / s p a n >
< span style = "display:flex;gap:8px;align-items:center;" >
2026-06-01 15:26:10 +10:00
< span class = "mcp-tools-count" > $ { sortedModels . length - hiddenSet . size } / $ { sortedModels . length } enabled < / s p a n >
2026-06-04 00:56:11 -03:00
< a href = "#" data - ep - refresh - models = "${epId}" > Refresh < / a >
2026-05-31 23:58:26 +09:00
< a href = "#" data - ep - select - all = "${epId}" > All < / a >
< a href = "#" data - ep - select - none = "${epId}" > None < / a >
< / s p a n >
2026-06-04 00:56:11 -03:00
< / d i v > $ { w a r n i n g H t m l } $ { s h o w S e a r c h ? ` < i n p u t t y p e = " s e a r c h " c l a s s = " m c p - t o o l s - s e a r c h " p l a c e h o l d e r = " S e a r c h $ { s o r t e d M o d e l s . l e n g t h } m o d e l s . . . " d a t a - e p - s e a r c h = " $ { e p I d } " > ` : ' ' } < d i v c l a s s = " m c p - t o o l s - l i s t " > ` + s o r t e d M o d e l s . m a p ( m = >
2026-05-31 23:58:26 +09:00
` <label title=" ${ esc ( m . id ) } " data-ep-model-row data-search=" ${ esc ( ( m . display + ' ' + m . id ) . toLowerCase ( ) ) } " class="adm-model-row">
< input type = "checkbox" class = "adm-cb-hidden" data - ep - model - id = "${esc(m.id)}" $ { ! m . is _hidden ? 'checked' : '' } >
< span class = "adm-check-dot" aria - hidden = "true" > < / s p a n >
< span > $ { esc ( m . display ) } < / s p a n >
< / l a b e l > `
) . join ( '' ) + '</div>' ;
const filterRows = ( q ) => {
const needle = q . trim ( ) . toLowerCase ( ) ;
panel . querySelectorAll ( '[data-ep-model-row]' ) . forEach ( row => {
row . style . display = ( ! needle || row . dataset . search . includes ( needle ) ) ? '' : 'none' ;
} ) ;
} ;
2026-06-04 00:56:11 -03:00
attachRefresh ( ) ;
2026-05-31 23:58:26 +09:00
panel . querySelector ( ` [data-ep-search=" ${ epId } "] ` ) ? . addEventListener ( 'input' , ( e ) => filterRows ( e . target . value ) ) ;
panel . querySelector ( ` [data-ep-select-all=" ${ epId } "] ` ) ? . addEventListener ( 'click' , ( e ) => {
e . preventDefault ( ) ;
panel . querySelectorAll ( '[data-ep-model-row]' ) . forEach ( row => {
if ( row . style . display !== 'none' ) row . querySelector ( 'input[type=checkbox]' ) . checked = true ;
} ) ;
_saveEpModelState ( epId , panel ) ;
} ) ;
panel . querySelector ( ` [data-ep-select-none=" ${ epId } "] ` ) ? . addEventListener ( 'click' , ( e ) => {
e . preventDefault ( ) ;
panel . querySelectorAll ( '[data-ep-model-row]' ) . forEach ( row => {
if ( row . style . display !== 'none' ) row . querySelector ( 'input[type=checkbox]' ) . checked = false ;
} ) ;
_saveEpModelState ( epId , panel ) ;
} ) ;
panel . querySelectorAll ( 'input[type=checkbox]' ) . forEach ( cb => {
cb . addEventListener ( 'change' , ( ) => _saveEpModelState ( epId , panel ) ) ;
} ) ;
2026-06-04 00:56:11 -03:00
} ;
try {
const res = await fetch ( ` /api/model-endpoints/ ${ epId } /models ` , { credentials : 'same-origin' } ) ;
if ( ! res . ok ) throw new Error ( ` HTTP ${ res . status } ` ) ;
const models = await res . json ( ) ;
_stopSpin ( ) ;
renderModels ( models ) ;
2026-05-31 23:58:26 +09:00
} catch ( e ) { _stopSpin ( ) ; panel . innerHTML = '<span class="admin-error" style="font-size:11px;">Failed to load models</span>' ; }
}
} ) ;
} ) ;
} catch ( e ) {
const err = '<div class="admin-error">Failed to load</div>' ;
[ listLocal , listApi , listLegacy ] . forEach ( c => { if ( c ) c . innerHTML = err ; } ) ;
}
}
async function _saveEpModelState ( epId , panel ) {
const hidden = [ ] ;
panel . querySelectorAll ( 'input[type=checkbox]' ) . forEach ( cb => {
if ( ! cb . checked ) hidden . push ( cb . dataset . epModelId ) ;
} ) ;
const total = panel . querySelectorAll ( 'input[type=checkbox]' ) . length ;
try {
await fetch ( ` /api/model-endpoints/ ${ epId } /models ` , {
method : 'PATCH' ,
headers : { 'Content-Type' : 'application/json' } ,
credentials : 'same-origin' ,
body : JSON . stringify ( { hidden } ) ,
} ) ;
const countLabel = panel . querySelector ( '.mcp-tools-count' ) ;
if ( countLabel ) countLabel . textContent = ` ${ total - hidden . length } / ${ total } enabled ` ;
const row = panel . closest ( '[data-adm-ep-id]' ) ;
if ( row ) {
const badge = row . querySelector ( '.admin-badge' ) ;
if ( badge && ! badge . classList . contains ( 'admin-badge-off' ) ) badge . textContent = ` ${ total - hidden . length } / ${ total } models enabled ` ;
}
2026-06-01 15:26:10 +10:00
if ( settingsModule && typeof settingsModule . refreshAiModelEndpoints === 'function' ) {
settingsModule . refreshAiModelEndpoints ( ) ;
}
2026-05-31 23:58:26 +09:00
} catch ( e ) { /* silent */ }
}
function initEndpointForm ( ) {
const provider = el ( 'adm-epProvider' ) ;
const urlInput = el ( 'adm-epUrl' ) ;
2026-06-04 00:56:11 -03:00
const kindSel = el ( 'adm-epKind' ) ;
2026-05-31 23:58:26 +09:00
// Custom provider picker — mirrors the (now hidden) <select id="adm-epProvider">
// so the rest of this function (which reads provider.value and dispatches
// change events) keeps working unchanged.
const picker = el ( 'adm-provider-picker' ) ;
const pickerBtn = el ( 'adm-provider-btn' ) ;
const pickerMenu = el ( 'adm-provider-menu' ) ;
const pickerCurrent = picker ? picker . querySelector ( '.adm-provider-current' ) : null ;
feat: add ChatGPT Subscription provider (#2876)
* feat: Add ChatGPT Subscription support and related features
- Introduced a new provider option for ChatGPT Subscription in the endpoint selection UI.
- Implemented OAuth flow for ChatGPT Subscription sign-in, including polling for authorization status.
- Updated admin interface to handle ChatGPT Subscription, including disabling API key input and providing user guidance.
- Enhanced cost tracking logic to differentiate between subscription and non-subscription endpoints.
- Added new slash commands for managing skills, including listing, searching, and invoking skills.
- Implemented caching for skill catalog to optimize performance.
- Updated tests to cover new ChatGPT Subscription functionality and ensure proper endpoint probing.
- Refactored existing code to accommodate new features and improve maintainability.
* refactor: share provider device-flow setup
- reuse one device-flow backend for Copilot and ChatGPT Subscription
- add one frontend device-flow helper for Settings and /setup
- put GitHub Copilot back into Add Models, now as a dropdown option
- make provider selection just select; clicking Add starts sign-in
- stop ChatGPT Subscription setup from opening auth tabs automatically
- make /setup copilot and /setup chatgpt-subscription work from chat
- show ChatGPT Subscription in the /setup suggestions
- show the real error message when setup fails
- add focused tests for the shared flow and setup UI
* feat(chatgpt-subscription): harden credential lifecycle and streamline auth UX
Backend:
- Resolve runtime bearer for provider-auth endpoints at probe time via a
shared _resolve_probe_key() that delegates to resolve_endpoint_runtime,
applied across all probe/refresh call sites.
- Skip live completion probes and health pings for discovery-only providers
(centralized behind _is_discovery_only_provider) — the Codex/Responses API
has no such endpoints, so status is derived from cached models.
- Never persist the short lived ChatGPT bearer to the plaintext sessions
table; proactively clear any stale bearer left by an earlier code path.
- Revoke orphaned ProviderAuthSession credentials when the last endpoint
backing them is deleted (_delete_orphaned_provider_auth), surfaced via
cleared_provider_auth in the delete response.
Frontend (admin.js):
- Auto-start the device-auth flow on provider selection so the authorization
panel (code + Authorize) shows immediately instead of behind a "Sign in" click.
- Remove the redundant top button for device auth providers, move retry
into the panel via an inline "Try again".
- Drop the self-evident hint text and add an execCommand clipboard fallback so
Copy works in non-secure (HTTP/LAN) contexts.
* fix: harden chatgpt subscription provider
* chore: remove PR media from branch
* Fix chatgpt subscription recovery and token handling
---------
Co-authored-by: 5p00kyy <admin@5p00ky.dev>
2026-06-08 18:19:18 +10:00
const DEVICE _AUTH _PROVIDER _VALUES = new Set ( Object . keys ( PROVIDER _DEVICE _FLOWS ) ) ;
let deviceAuthPolling = false ;
function _selectedProviderOption ( ) {
return provider && provider . selectedOptions ? provider . selectedOptions [ 0 ] : null ;
}
function _selectedDeviceAuthProvider ( ) {
const opt = _selectedProviderOption ( ) ;
const flow = opt && opt . dataset ? opt . dataset . authFlow : '' ;
if ( flow && DEVICE _AUTH _PROVIDER _VALUES . has ( flow ) ) return flow ;
return DEVICE _AUTH _PROVIDER _VALUES . has ( provider . value ) ? provider . value : '' ;
}
function _isDeviceAuthSelected ( ) {
return ! ! _selectedDeviceAuthProvider ( ) ;
}
function _setApiFormForProvider ( ) {
const deviceAuthProvider = _selectedDeviceAuthProvider ( ) ;
const deviceAuthConfig = PROVIDER _DEVICE _FLOWS [ deviceAuthProvider ] || null ;
const apiKey = el ( 'adm-epApiKey' ) ;
const testBtn = el ( 'adm-epApiTestBtn' ) ;
const addBtn = el ( 'adm-epAddBtn' ) ;
const status = el ( 'adm-deviceAuthStatus' ) ;
const msg = _endpointMsg ( 'api' ) ;
if ( deviceAuthConfig ) {
urlInput . value = '' ;
urlInput . placeholder = deviceAuthProvider === 'copilot'
? 'GitHub Copilot uses GitHub account sign-in'
: 'ChatGPT Subscription uses OpenAI account sign-in' ;
urlInput . readOnly = true ;
if ( apiKey ) {
apiKey . value = '' ;
apiKey . placeholder = 'No API key needed' ;
apiKey . disabled = true ;
}
if ( testBtn ) {
testBtn . disabled = true ;
testBtn . style . opacity = '0.45' ;
testBtn . style . cursor = 'not-allowed' ;
}
if ( addBtn ) {
addBtn . disabled = false ;
addBtn . textContent = 'Add' ;
addBtn . style . width = '55px' ;
addBtn . style . display = '' ;
}
if ( kindSel ) kindSel . value = 'api' ;
if ( msg ) {
msg . textContent = '' ;
msg . className = '' ;
}
} else {
urlInput . placeholder = 'Base URL or pick provider' ;
urlInput . readOnly = false ;
if ( apiKey ) {
apiKey . placeholder = 'API key' ;
apiKey . disabled = false ;
}
if ( testBtn ) {
testBtn . disabled = false ;
testBtn . style . opacity = '' ;
testBtn . style . cursor = '' ;
}
if ( addBtn ) {
addBtn . disabled = false ;
addBtn . textContent = 'Add' ;
addBtn . style . width = '55px' ;
addBtn . style . display = '' ;
}
if ( msg ) {
msg . textContent = '' ;
msg . className = '' ;
}
if ( ! deviceAuthPolling && status ) status . textContent = '' ;
}
}
2026-05-31 23:58:26 +09:00
function _renderPickerMenu ( ) {
if ( ! pickerMenu ) return ;
pickerMenu . innerHTML = Array . from ( provider . options ) . map ( o => {
const logo = o . dataset . logo ? ( providerLogo ( o . dataset . logo ) || '' ) : '' ;
const active = o . value === provider . value ? ' active' : '' ;
return ` <div class="adm-provider-item ${ active } " role="option" data-value=" ${ o . value . replace ( /"/g , '"' ) } ">
< span class = "adm-provider-logo" > $ { logo } < / s p a n >
< span > $ { o . textContent } < / s p a n >
< / d i v > ` ;
} ) . join ( '' ) ;
}
function _syncPickerCurrent ( ) {
if ( ! pickerCurrent ) return ;
const opt = provider . selectedOptions [ 0 ] || provider . options [ 0 ] ;
const logo = opt . dataset . logo ? ( providerLogo ( opt . dataset . logo ) || '' ) : '' ;
pickerCurrent . querySelector ( '.adm-provider-logo' ) . innerHTML = logo ;
pickerCurrent . querySelector ( '.adm-provider-name' ) . textContent = opt . textContent ;
}
if ( picker && pickerBtn && pickerMenu && pickerCurrent ) {
_renderPickerMenu ( ) ;
_syncPickerCurrent ( ) ;
2026-06-01 10:00:15 +09:00
if ( provider . value && ! urlInput . value ) urlInput . value = provider . value ;
2026-05-31 23:58:26 +09:00
pickerBtn . addEventListener ( 'click' , ( e ) => {
e . stopPropagation ( ) ;
pickerMenu . classList . toggle ( 'hidden' ) ;
} ) ;
pickerMenu . addEventListener ( 'click' , ( e ) => {
const item = e . target . closest ( '.adm-provider-item' ) ;
if ( ! item ) return ;
provider . value = item . dataset . value ;
provider . dispatchEvent ( new Event ( 'change' , { bubbles : true } ) ) ;
pickerMenu . classList . add ( 'hidden' ) ;
_renderPickerMenu ( ) ;
_syncPickerCurrent ( ) ;
} ) ;
document . addEventListener ( 'click' , ( e ) => {
if ( ! picker . contains ( e . target ) ) pickerMenu . classList . add ( 'hidden' ) ;
} ) ;
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
// Capture-phase Esc: dismiss the picker menu without bubbling to the
// settings-modal handler that would otherwise close the whole modal.
document . addEventListener ( 'keydown' , ( e ) => {
if ( e . key !== 'Escape' ) return ;
if ( pickerMenu . classList . contains ( 'hidden' ) ) return ;
e . stopPropagation ( ) ;
pickerMenu . classList . add ( 'hidden' ) ;
} , { capture : true } ) ;
2026-05-31 23:58:26 +09:00
}
provider . addEventListener ( 'change' , ( ) => {
feat: add ChatGPT Subscription provider (#2876)
* feat: Add ChatGPT Subscription support and related features
- Introduced a new provider option for ChatGPT Subscription in the endpoint selection UI.
- Implemented OAuth flow for ChatGPT Subscription sign-in, including polling for authorization status.
- Updated admin interface to handle ChatGPT Subscription, including disabling API key input and providing user guidance.
- Enhanced cost tracking logic to differentiate between subscription and non-subscription endpoints.
- Added new slash commands for managing skills, including listing, searching, and invoking skills.
- Implemented caching for skill catalog to optimize performance.
- Updated tests to cover new ChatGPT Subscription functionality and ensure proper endpoint probing.
- Refactored existing code to accommodate new features and improve maintainability.
* refactor: share provider device-flow setup
- reuse one device-flow backend for Copilot and ChatGPT Subscription
- add one frontend device-flow helper for Settings and /setup
- put GitHub Copilot back into Add Models, now as a dropdown option
- make provider selection just select; clicking Add starts sign-in
- stop ChatGPT Subscription setup from opening auth tabs automatically
- make /setup copilot and /setup chatgpt-subscription work from chat
- show ChatGPT Subscription in the /setup suggestions
- show the real error message when setup fails
- add focused tests for the shared flow and setup UI
* feat(chatgpt-subscription): harden credential lifecycle and streamline auth UX
Backend:
- Resolve runtime bearer for provider-auth endpoints at probe time via a
shared _resolve_probe_key() that delegates to resolve_endpoint_runtime,
applied across all probe/refresh call sites.
- Skip live completion probes and health pings for discovery-only providers
(centralized behind _is_discovery_only_provider) — the Codex/Responses API
has no such endpoints, so status is derived from cached models.
- Never persist the short lived ChatGPT bearer to the plaintext sessions
table; proactively clear any stale bearer left by an earlier code path.
- Revoke orphaned ProviderAuthSession credentials when the last endpoint
backing them is deleted (_delete_orphaned_provider_auth), surfaced via
cleared_provider_auth in the delete response.
Frontend (admin.js):
- Auto-start the device-auth flow on provider selection so the authorization
panel (code + Authorize) shows immediately instead of behind a "Sign in" click.
- Remove the redundant top button for device auth providers, move retry
into the panel via an inline "Try again".
- Drop the self-evident hint text and add an execCommand clipboard fallback so
Copy works in non-secure (HTTP/LAN) contexts.
* fix: harden chatgpt subscription provider
* chore: remove PR media from branch
* Fix chatgpt subscription recovery and token handling
---------
Co-authored-by: 5p00kyy <admin@5p00ky.dev>
2026-06-08 18:19:18 +10:00
if ( _isDeviceAuthSelected ( ) ) {
_setApiFormForProvider ( ) ;
_renderPickerMenu ( ) ;
_syncPickerCurrent ( ) ;
return ;
}
2026-05-31 23:58:26 +09:00
if ( provider . value ) urlInput . value = provider . value ;
else urlInput . value = '' ;
2026-06-04 00:56:11 -03:00
if ( kindSel ) kindSel . value = provider . value ? 'api' : 'proxy' ;
feat: add ChatGPT Subscription provider (#2876)
* feat: Add ChatGPT Subscription support and related features
- Introduced a new provider option for ChatGPT Subscription in the endpoint selection UI.
- Implemented OAuth flow for ChatGPT Subscription sign-in, including polling for authorization status.
- Updated admin interface to handle ChatGPT Subscription, including disabling API key input and providing user guidance.
- Enhanced cost tracking logic to differentiate between subscription and non-subscription endpoints.
- Added new slash commands for managing skills, including listing, searching, and invoking skills.
- Implemented caching for skill catalog to optimize performance.
- Updated tests to cover new ChatGPT Subscription functionality and ensure proper endpoint probing.
- Refactored existing code to accommodate new features and improve maintainability.
* refactor: share provider device-flow setup
- reuse one device-flow backend for Copilot and ChatGPT Subscription
- add one frontend device-flow helper for Settings and /setup
- put GitHub Copilot back into Add Models, now as a dropdown option
- make provider selection just select; clicking Add starts sign-in
- stop ChatGPT Subscription setup from opening auth tabs automatically
- make /setup copilot and /setup chatgpt-subscription work from chat
- show ChatGPT Subscription in the /setup suggestions
- show the real error message when setup fails
- add focused tests for the shared flow and setup UI
* feat(chatgpt-subscription): harden credential lifecycle and streamline auth UX
Backend:
- Resolve runtime bearer for provider-auth endpoints at probe time via a
shared _resolve_probe_key() that delegates to resolve_endpoint_runtime,
applied across all probe/refresh call sites.
- Skip live completion probes and health pings for discovery-only providers
(centralized behind _is_discovery_only_provider) — the Codex/Responses API
has no such endpoints, so status is derived from cached models.
- Never persist the short lived ChatGPT bearer to the plaintext sessions
table; proactively clear any stale bearer left by an earlier code path.
- Revoke orphaned ProviderAuthSession credentials when the last endpoint
backing them is deleted (_delete_orphaned_provider_auth), surfaced via
cleared_provider_auth in the delete response.
Frontend (admin.js):
- Auto-start the device-auth flow on provider selection so the authorization
panel (code + Authorize) shows immediately instead of behind a "Sign in" click.
- Remove the redundant top button for device auth providers, move retry
into the panel via an inline "Try again".
- Drop the self-evident hint text and add an execCommand clipboard fallback so
Copy works in non-secure (HTTP/LAN) contexts.
* fix: harden chatgpt subscription provider
* chore: remove PR media from branch
* Fix chatgpt subscription recovery and token handling
---------
Co-authored-by: 5p00kyy <admin@5p00ky.dev>
2026-06-08 18:19:18 +10:00
_setApiFormForProvider ( ) ;
2026-05-31 23:58:26 +09:00
} ) ;
2026-06-01 10:00:15 +09:00
urlInput . addEventListener ( 'input' , ( ) => {
if ( provider . value && urlInput . value . trim ( ) !== provider . value ) {
provider . value = '' ;
Settings polish: /setup provider subs, Add API defaults to api kind, picker shows offline endpoints, doc library tracks sub-tab
- /setup gains explicit provider subcommands (deepseek, openai,
anthropic, openrouter, groq, gemini, xai, ollama, copilot, local,
endpoint) so the autocomplete popup surfaces "/setup de…" suggestions
with format hints, and bare-provider invocations still prompt for
the key.
- Add API endpoint defaults to kind=api (auto-refresh /v1/models)
instead of kind=proxy. Proxy was a frequent footgun for OpenAI-
compatible endpoints that DO serve /v1/models — the user got an
empty model list and had to flip the dropdown.
- Model picker now includes offline endpoints with stale:true so a
briefly-down local server doesn't vanish from the picker (it dims
and shows the offline pill, clickable anyway). Dedup prefers the
online entry when the same model is exposed by both.
- Document library modal header reflects the active sub-tab via
_TAB_HEADERS so it no longer shows the wrong section name when
switching between Documents / Skills / Templates.
2026-06-05 14:41:54 +09:00
if ( kindSel ) kindSel . value = 'api' ;
2026-06-01 10:00:15 +09:00
_renderPickerMenu ( ) ;
_syncPickerCurrent ( ) ;
}
} ) ;
Settings polish: /setup provider subs, Add API defaults to api kind, picker shows offline endpoints, doc library tracks sub-tab
- /setup gains explicit provider subcommands (deepseek, openai,
anthropic, openrouter, groq, gemini, xai, ollama, copilot, local,
endpoint) so the autocomplete popup surfaces "/setup de…" suggestions
with format hints, and bare-provider invocations still prompt for
the key.
- Add API endpoint defaults to kind=api (auto-refresh /v1/models)
instead of kind=proxy. Proxy was a frequent footgun for OpenAI-
compatible endpoints that DO serve /v1/models — the user got an
empty model list and had to flip the dropdown.
- Model picker now includes offline endpoints with stale:true so a
briefly-down local server doesn't vanish from the picker (it dims
and shows the offline pill, clickable anyway). Dedup prefers the
online entry when the same model is exposed by both.
- Document library modal header reflects the active sub-tab via
_TAB_HEADERS so it no longer shows the wrong section name when
switching between Documents / Skills / Templates.
2026-06-05 14:41:54 +09:00
if ( kindSel ) kindSel . value = kindSel . value || 'api' ;
2026-06-04 00:56:11 -03:00
function _apiEndpointKind ( ) {
Settings polish: /setup provider subs, Add API defaults to api kind, picker shows offline endpoints, doc library tracks sub-tab
- /setup gains explicit provider subcommands (deepseek, openai,
anthropic, openrouter, groq, gemini, xai, ollama, copilot, local,
endpoint) so the autocomplete popup surfaces "/setup de…" suggestions
with format hints, and bare-provider invocations still prompt for
the key.
- Add API endpoint defaults to kind=api (auto-refresh /v1/models)
instead of kind=proxy. Proxy was a frequent footgun for OpenAI-
compatible endpoints that DO serve /v1/models — the user got an
empty model list and had to flip the dropdown.
- Model picker now includes offline endpoints with stale:true so a
briefly-down local server doesn't vanish from the picker (it dims
and shows the offline pill, clickable anyway). Dedup prefers the
online entry when the same model is exposed by both.
- Document library modal header reflects the active sub-tab via
_TAB_HEADERS so it no longer shows the wrong section name when
switching between Documents / Skills / Templates.
2026-06-05 14:41:54 +09:00
return ( kindSel && kindSel . value ) ? kindSel . value : 'api' ;
2026-06-04 00:56:11 -03:00
}
2026-05-31 23:58:26 +09:00
function _normalizeBaseUrl ( raw ) {
let u = raw . trim ( ) ;
// Fix common protocol typos
u = u . replace ( /^https?:\/(?!\/)/ , m => m + '/' ) ; // https:/ → https://
u = u . replace ( /^htp:/ , 'http:' ) . replace ( /^htps:/ , 'https:' ) ;
u = u . replace ( /^http:\/\/\// , 'http://' ) ; // http:/// → http://
u = u . replace ( /^https:\/\/\// , 'https://' ) ;
// Add http:// if no protocol
if ( ! /^https?:\/\// . test ( u ) ) u = 'http://' + u ;
// Strip trailing slashes
u = u . replace ( /\/+$/ , '' ) ;
// Strip trailing paths that shouldn't be in a base URL
u = u . replace ( /\/v1\/(models|chat\/completions|completions|messages)\/?$/i , '/v1' ) ;
u = u . replace ( /\/(models|chat\/completions|completions|v1\/messages)\/?$/i , '' ) ;
2026-06-01 15:26:10 +10:00
u = u . replace ( /\/api\/(chat|tags|generate)\/?$/i , '/api' ) ;
2026-05-31 23:58:26 +09:00
// Fix double /v1/v1
u = u . replace ( /\/v1\/v1$/ , '/v1' ) ;
// Strip query params and fragments
u = u . split ( '?' ) [ 0 ] . split ( '#' ) [ 0 ] ;
2026-06-01 15:26:10 +10:00
try {
const parsed = new URL ( u ) ;
if ( parsed . hostname . endsWith ( 'ollama.com' ) ) {
u = 'https://ollama.com/api' ;
}
} catch ( e ) { }
2026-05-31 23:58:26 +09:00
// Ensure /v1 suffix for bare host:port URLs (not cloud providers)
2026-06-07 17:43:00 +03:00
if ( ! u . includes ( 'api.' ) && ! u . includes ( 'openrouter' ) && ! u . includes ( 'opencode.ai' ) && ! u . includes ( 'ollama.com' ) && ! u . endsWith ( '/v1' ) ) {
2026-05-31 23:58:26 +09:00
try {
const parsed = new URL ( u ) ;
if ( ! parsed . pathname || parsed . pathname === '/' ) {
u += '/v1' ;
}
} catch ( e ) { }
}
return u ;
}
2026-06-01 10:00:15 +09:00
async function _defaultOllamaUrl ( ) {
try {
const res = await fetch ( '/api/runtime' , { credentials : 'same-origin' } ) ;
if ( res . ok ) {
const data = await res . json ( ) ;
if ( data && data . ollama _base _url ) return data . ollama _base _url ;
}
} catch ( _ ) { }
return 'http://127.0.0.1:11434/v1' ;
}
function _renderEndpointTestResult ( msg , res , d ) {
if ( res . ok && d . status === 'empty' ) {
msg . textContent = 'Online — no models found' ;
msg . className = 'admin-success' ;
return ;
}
if ( res . ok && d . online ) {
const models = d . models || [ ] ;
const preview = models . slice ( 0 , 3 ) . map ( m => esc ( String ( m ) . split ( '/' ) . pop ( ) ) ) . join ( ', ' ) ;
msg . innerHTML = ` Online — found ${ models . length } model ${ models . length !== 1 ? 's' : '' } ${ preview ? ` : ${ preview } ${ models . length > 3 ? ', …' : '' } ` : '' } ` ;
msg . className = 'admin-success' ;
return ;
}
msg . textContent = ( d && d . detail ) || ( d && d . ping _error ? ` Offline — ${ d . ping _error } ` : 'Offline' ) ;
msg . className = 'admin-error' ;
}
function _endpointMsg ( kind ) {
return el ( kind === 'local' ? 'adm-epLocalMsg' : 'adm-epApiMsg' ) || el ( 'adm-epMsg' ) ;
}
let apiTestController = null ;
const apiTestBtn = el ( 'adm-epApiTestBtn' ) ;
const apiCancelTestBtn = el ( 'adm-epApiCancelTestBtn' ) ;
if ( apiTestBtn ) {
apiTestBtn . addEventListener ( 'click' , async ( ) => {
feat: add ChatGPT Subscription provider (#2876)
* feat: Add ChatGPT Subscription support and related features
- Introduced a new provider option for ChatGPT Subscription in the endpoint selection UI.
- Implemented OAuth flow for ChatGPT Subscription sign-in, including polling for authorization status.
- Updated admin interface to handle ChatGPT Subscription, including disabling API key input and providing user guidance.
- Enhanced cost tracking logic to differentiate between subscription and non-subscription endpoints.
- Added new slash commands for managing skills, including listing, searching, and invoking skills.
- Implemented caching for skill catalog to optimize performance.
- Updated tests to cover new ChatGPT Subscription functionality and ensure proper endpoint probing.
- Refactored existing code to accommodate new features and improve maintainability.
* refactor: share provider device-flow setup
- reuse one device-flow backend for Copilot and ChatGPT Subscription
- add one frontend device-flow helper for Settings and /setup
- put GitHub Copilot back into Add Models, now as a dropdown option
- make provider selection just select; clicking Add starts sign-in
- stop ChatGPT Subscription setup from opening auth tabs automatically
- make /setup copilot and /setup chatgpt-subscription work from chat
- show ChatGPT Subscription in the /setup suggestions
- show the real error message when setup fails
- add focused tests for the shared flow and setup UI
* feat(chatgpt-subscription): harden credential lifecycle and streamline auth UX
Backend:
- Resolve runtime bearer for provider-auth endpoints at probe time via a
shared _resolve_probe_key() that delegates to resolve_endpoint_runtime,
applied across all probe/refresh call sites.
- Skip live completion probes and health pings for discovery-only providers
(centralized behind _is_discovery_only_provider) — the Codex/Responses API
has no such endpoints, so status is derived from cached models.
- Never persist the short lived ChatGPT bearer to the plaintext sessions
table; proactively clear any stale bearer left by an earlier code path.
- Revoke orphaned ProviderAuthSession credentials when the last endpoint
backing them is deleted (_delete_orphaned_provider_auth), surfaced via
cleared_provider_auth in the delete response.
Frontend (admin.js):
- Auto-start the device-auth flow on provider selection so the authorization
panel (code + Authorize) shows immediately instead of behind a "Sign in" click.
- Remove the redundant top button for device auth providers, move retry
into the panel via an inline "Try again".
- Drop the self-evident hint text and add an execCommand clipboard fallback so
Copy works in non-secure (HTTP/LAN) contexts.
* fix: harden chatgpt subscription provider
* chore: remove PR media from branch
* Fix chatgpt subscription recovery and token handling
---------
Co-authored-by: 5p00kyy <admin@5p00ky.dev>
2026-06-08 18:19:18 +10:00
if ( _isDeviceAuthSelected ( ) ) {
const msg = _endpointMsg ( 'api' ) ;
msg . textContent = '' ;
msg . className = '' ;
return ;
}
2026-06-01 10:00:15 +09:00
const msg = _endpointMsg ( 'api' ) ;
msg . textContent = '' ; msg . className = '' ;
const rawUrl = ( urlInput . value || provider . value ) . trim ( ) ;
const apiKey = el ( 'adm-epApiKey' ) . value . trim ( ) ;
if ( ! rawUrl ) { msg . textContent = 'Select a provider or enter a base URL' ; msg . className = 'admin-error' ; return ; }
if ( provider . value && ! apiKey ) { msg . textContent = 'API key is required for cloud providers' ; msg . className = 'admin-error' ; return ; }
const url = provider . value && rawUrl === provider . value ? rawUrl : _normalizeBaseUrl ( rawUrl ) ;
apiTestController = new AbortController ( ) ;
apiTestBtn . disabled = true ;
apiTestBtn . textContent = 'Testing...' ;
if ( apiCancelTestBtn ) apiCancelTestBtn . classList . remove ( 'hidden' ) ;
try {
const fd = new FormData ( ) ;
fd . append ( 'base_url' , url ) ;
2026-06-04 00:56:11 -03:00
fd . append ( 'endpoint_kind' , _apiEndpointKind ( ) ) ;
fd . append ( 'model_refresh_timeout' , '30' ) ;
2026-06-01 10:00:15 +09:00
if ( apiKey ) fd . append ( 'api_key' , apiKey ) ;
const res = await fetch ( '/api/model-endpoints/test' , {
method : 'POST' ,
body : fd ,
credentials : 'same-origin' ,
signal : apiTestController . signal ,
} ) ;
const d = await res . json ( ) ;
_renderEndpointTestResult ( msg , res , d ) ;
} catch ( e ) {
if ( e && e . name === 'AbortError' ) {
msg . textContent = 'Test canceled' ;
msg . className = '' ;
} else {
msg . textContent = 'Test failed: ' + ( e && e . message ? e . message : 'request failed' ) ;
msg . className = 'admin-error' ;
}
}
apiTestController = null ;
apiTestBtn . disabled = false ;
apiTestBtn . textContent = 'Test' ;
if ( apiCancelTestBtn ) apiCancelTestBtn . classList . add ( 'hidden' ) ;
} ) ;
}
if ( apiCancelTestBtn ) {
apiCancelTestBtn . addEventListener ( 'click' , ( ) => {
if ( apiTestController ) apiTestController . abort ( ) ;
} ) ;
}
2026-05-31 23:58:26 +09:00
el ( 'adm-epAddBtn' ) . addEventListener ( 'click' , async ( ) => {
feat: add ChatGPT Subscription provider (#2876)
* feat: Add ChatGPT Subscription support and related features
- Introduced a new provider option for ChatGPT Subscription in the endpoint selection UI.
- Implemented OAuth flow for ChatGPT Subscription sign-in, including polling for authorization status.
- Updated admin interface to handle ChatGPT Subscription, including disabling API key input and providing user guidance.
- Enhanced cost tracking logic to differentiate between subscription and non-subscription endpoints.
- Added new slash commands for managing skills, including listing, searching, and invoking skills.
- Implemented caching for skill catalog to optimize performance.
- Updated tests to cover new ChatGPT Subscription functionality and ensure proper endpoint probing.
- Refactored existing code to accommodate new features and improve maintainability.
* refactor: share provider device-flow setup
- reuse one device-flow backend for Copilot and ChatGPT Subscription
- add one frontend device-flow helper for Settings and /setup
- put GitHub Copilot back into Add Models, now as a dropdown option
- make provider selection just select; clicking Add starts sign-in
- stop ChatGPT Subscription setup from opening auth tabs automatically
- make /setup copilot and /setup chatgpt-subscription work from chat
- show ChatGPT Subscription in the /setup suggestions
- show the real error message when setup fails
- add focused tests for the shared flow and setup UI
* feat(chatgpt-subscription): harden credential lifecycle and streamline auth UX
Backend:
- Resolve runtime bearer for provider-auth endpoints at probe time via a
shared _resolve_probe_key() that delegates to resolve_endpoint_runtime,
applied across all probe/refresh call sites.
- Skip live completion probes and health pings for discovery-only providers
(centralized behind _is_discovery_only_provider) — the Codex/Responses API
has no such endpoints, so status is derived from cached models.
- Never persist the short lived ChatGPT bearer to the plaintext sessions
table; proactively clear any stale bearer left by an earlier code path.
- Revoke orphaned ProviderAuthSession credentials when the last endpoint
backing them is deleted (_delete_orphaned_provider_auth), surfaced via
cleared_provider_auth in the delete response.
Frontend (admin.js):
- Auto-start the device-auth flow on provider selection so the authorization
panel (code + Authorize) shows immediately instead of behind a "Sign in" click.
- Remove the redundant top button for device auth providers, move retry
into the panel via an inline "Try again".
- Drop the self-evident hint text and add an execCommand clipboard fallback so
Copy works in non-secure (HTTP/LAN) contexts.
* fix: harden chatgpt subscription provider
* chore: remove PR media from branch
* Fix chatgpt subscription recovery and token handling
---------
Co-authored-by: 5p00kyy <admin@5p00ky.dev>
2026-06-08 18:19:18 +10:00
const deviceAuthProvider = _selectedDeviceAuthProvider ( ) ;
if ( deviceAuthProvider ) {
await _startProviderDeviceAuth ( deviceAuthProvider , el ( 'adm-epAddBtn' ) ) ;
return ;
}
2026-06-01 10:00:15 +09:00
const msg = _endpointMsg ( 'api' ) ;
2026-05-31 23:58:26 +09:00
msg . textContent = '' ; msg . className = '' ;
2026-06-01 10:00:15 +09:00
const rawUrl = ( urlInput . value || provider . value ) . trim ( ) ;
2026-05-31 23:58:26 +09:00
const apiKey = el ( 'adm-epApiKey' ) . value . trim ( ) ;
if ( ! rawUrl ) { msg . textContent = 'Select a provider or enter a base URL' ; msg . className = 'admin-error' ; return ; }
if ( provider . value && ! apiKey ) { msg . textContent = 'API key is required for cloud providers' ; msg . className = 'admin-error' ; return ; }
// Normalize URL (fix typos, add /v1, strip wrong paths)
2026-06-01 10:00:15 +09:00
const url = provider . value && rawUrl === provider . value ? rawUrl : _normalizeBaseUrl ( rawUrl ) ;
2026-05-31 23:58:26 +09:00
const btn = el ( 'adm-epAddBtn' ) ;
btn . disabled = true ; btn . textContent = 'Adding...' ;
try {
const fd = new FormData ( ) ;
fd . append ( 'base_url' , url ) ;
2026-06-04 00:56:11 -03:00
const endpointKind = _apiEndpointKind ( ) ;
fd . append ( 'endpoint_kind' , endpointKind ) ;
fd . append ( 'model_refresh_mode' , endpointKind === 'proxy' ? 'manual' : 'auto' ) ;
fd . append ( 'model_refresh_timeout' , '30' ) ;
2026-05-31 23:58:26 +09:00
if ( apiKey ) fd . append ( 'api_key' , apiKey ) ;
2026-06-01 15:26:10 +10:00
if ( provider . value && provider . selectedOptions && provider . selectedOptions [ 0 ] ) {
fd . append ( 'name' , provider . selectedOptions [ 0 ] . textContent . trim ( ) ) ;
}
2026-05-31 23:58:26 +09:00
const epType = el ( 'adm-epType' ) ;
if ( epType ) fd . append ( 'model_type' , epType . value ) ;
2026-06-01 15:26:10 +10:00
if ( provider . value && /openrouter\.ai|ollama\.com/i . test ( provider . value ) ) fd . append ( 'require_models' , 'true' ) ;
else fd . append ( 'skip_probe' , 'false' ) ;
2026-05-31 23:58:26 +09:00
const res = await fetch ( '/api/model-endpoints' , { method : 'POST' , body : fd , credentials : 'same-origin' } ) ;
const d = await res . json ( ) ;
if ( res . ok ) {
const count = d . models ? d . models . length : 0 ;
urlInput . value = '' ; urlInput . style . display = '' ;
el ( 'adm-epApiKey' ) . value = '' ; provider . value = '' ;
2026-06-04 00:56:11 -03:00
if ( kindSel ) kindSel . value = 'proxy' ;
2026-05-31 23:58:26 +09:00
if ( epType ) epType . value = 'llm' ;
if ( d . id ) _recentlyAddedEpId = String ( d . id ) ;
2026-06-01 10:00:15 +09:00
await loadEndpoints ( ) ;
await _selectAddedModelInChat ( d ) ;
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
const goLink = ' <a href="#" data-go-added-models style="margin-left:6px;text-decoration:underline;color:inherit;font-weight:600;">Added Models →</a>' ;
2026-05-31 23:58:26 +09:00
if ( ! d . online ) {
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
msg . innerHTML = 'Added (endpoint offline — will retry on next load)' + goLink ;
2026-05-31 23:58:26 +09:00
msg . className = 'admin-error' ;
2026-06-01 10:00:15 +09:00
} else if ( d . status === 'empty' ) {
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
msg . innerHTML = 'Added — endpoint reachable, no models found' + goLink ;
2026-06-01 10:00:15 +09:00
msg . className = 'admin-success' ;
2026-05-31 23:58:26 +09:00
} else {
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
msg . innerHTML = ` Added — found ${ count } model ${ count !== 1 ? 's' : '' } ` + goLink ;
2026-05-31 23:58:26 +09:00
msg . className = 'admin-success' ;
}
} else { msg . textContent = d . detail || 'Failed' ; msg . className = 'admin-error' ; }
} catch ( e ) { msg . textContent = 'Request failed' ; msg . className = 'admin-error' ; }
btn . disabled = false ; btn . textContent = 'Add' ;
} ) ;
feat: add ChatGPT Subscription provider (#2876)
* feat: Add ChatGPT Subscription support and related features
- Introduced a new provider option for ChatGPT Subscription in the endpoint selection UI.
- Implemented OAuth flow for ChatGPT Subscription sign-in, including polling for authorization status.
- Updated admin interface to handle ChatGPT Subscription, including disabling API key input and providing user guidance.
- Enhanced cost tracking logic to differentiate between subscription and non-subscription endpoints.
- Added new slash commands for managing skills, including listing, searching, and invoking skills.
- Implemented caching for skill catalog to optimize performance.
- Updated tests to cover new ChatGPT Subscription functionality and ensure proper endpoint probing.
- Refactored existing code to accommodate new features and improve maintainability.
* refactor: share provider device-flow setup
- reuse one device-flow backend for Copilot and ChatGPT Subscription
- add one frontend device-flow helper for Settings and /setup
- put GitHub Copilot back into Add Models, now as a dropdown option
- make provider selection just select; clicking Add starts sign-in
- stop ChatGPT Subscription setup from opening auth tabs automatically
- make /setup copilot and /setup chatgpt-subscription work from chat
- show ChatGPT Subscription in the /setup suggestions
- show the real error message when setup fails
- add focused tests for the shared flow and setup UI
* feat(chatgpt-subscription): harden credential lifecycle and streamline auth UX
Backend:
- Resolve runtime bearer for provider-auth endpoints at probe time via a
shared _resolve_probe_key() that delegates to resolve_endpoint_runtime,
applied across all probe/refresh call sites.
- Skip live completion probes and health pings for discovery-only providers
(centralized behind _is_discovery_only_provider) — the Codex/Responses API
has no such endpoints, so status is derived from cached models.
- Never persist the short lived ChatGPT bearer to the plaintext sessions
table; proactively clear any stale bearer left by an earlier code path.
- Revoke orphaned ProviderAuthSession credentials when the last endpoint
backing them is deleted (_delete_orphaned_provider_auth), surfaced via
cleared_provider_auth in the delete response.
Frontend (admin.js):
- Auto-start the device-auth flow on provider selection so the authorization
panel (code + Authorize) shows immediately instead of behind a "Sign in" click.
- Remove the redundant top button for device auth providers, move retry
into the panel via an inline "Try again".
- Drop the self-evident hint text and add an execCommand clipboard fallback so
Copy works in non-secure (HTTP/LAN) contexts.
* fix: harden chatgpt subscription provider
* chore: remove PR media from branch
* Fix chatgpt subscription recovery and token handling
---------
Co-authored-by: 5p00kyy <admin@5p00ky.dev>
2026-06-08 18:19:18 +10:00
async function _startProviderDeviceAuth ( providerKey , triggerEl = null ) {
if ( deviceAuthPolling ) return ;
const config = PROVIDER _DEVICE _FLOWS [ providerKey ] ;
if ( ! config ) return ;
const status = el ( 'adm-deviceAuthStatus' ) || _endpointMsg ( 'api' ) ;
if ( ! status ) return ;
const triggerText = triggerEl ? triggerEl . textContent : '' ;
// Render an error with an inline "Try again" (the top button is hidden for
// device-auth providers, so retry lives here). Built with DOM methods, not
// innerHTML. Call reset() first so the deviceAuthPolling guard is cleared.
const showAuthError = ( text ) => {
status . className = 'admin-error' ;
status . textContent = text + ' ' ;
const retry = document . createElement ( 'button' ) ;
retry . type = 'button' ;
retry . className = 'admin-btn-sm' ;
retry . textContent = 'Try again' ;
retry . addEventListener ( 'click' , ( ) => { _startProviderDeviceAuth ( providerKey , triggerEl ) ; } ) ;
status . appendChild ( retry ) ;
} ;
const reset = ( ) => {
if ( triggerEl ) {
triggerEl . disabled = false ;
triggerEl . textContent = triggerText || 'Add' ;
}
deviceAuthPolling = false ;
_setApiFormForProvider ( ) ;
} ;
status . textContent = '' ;
status . className = 'adm-ep-inline-msg' ;
if ( triggerEl ) {
triggerEl . disabled = true ;
triggerEl . textContent = 'Starting...' ;
}
deviceAuthPolling = true ;
_setApiFormForProvider ( ) ;
status . textContent = ` Starting ${ config . label } sign-in... ` ;
feat(provider): add GitHub Copilot provider with device-flow auth (#1480)
* feat(provider): add GitHub Copilot provider with device-flow auth
Adds GitHub Copilot as a model provider, so Copilot models (gpt-4o/4.1/5,
Claude, Gemini, …) work through the normal chat + agent loop, incl. native
tool calling and vision.
Auth is one-click via the GitHub OAuth device flow; the access token is stored
as the endpoint's (encrypted) api_key and sent directly as `Authorization:
Bearer` (no Copilot-token exchange, no refresh — matching how editors talk to
the Copilot API). Copilot is a normal ModelEndpoint detected by host; the only
provider-specific behaviour is a small set of required request headers,
injected centrally.
Sign-in is available from Settings → model endpoints ("Connect GitHub
Copilot") and from chat via `/setup copilot`.
- src/copilot.py (new), routes/copilot_routes.py (new): constants, header
builders, device-flow start/poll, model discovery, owner-scoped endpoint
provisioning.
- src/llm_core.py, src/endpoint_resolver.py: detect `copilot`, inject headers,
per-request x-initiator/vision.
- src/agent_loop.py: allowlist api.githubcopilot.com for native tool schemas.
- src/model_context.py: known context windows for Copilot (no unauthenticated
/models probe).
- static/, README, tests/test_copilot*.py.
* Tidy copilot_routes: clarify supports_tools, note _PENDING is per-process
2026-06-04 21:13:14 +02:00
feat: add ChatGPT Subscription provider (#2876)
* feat: Add ChatGPT Subscription support and related features
- Introduced a new provider option for ChatGPT Subscription in the endpoint selection UI.
- Implemented OAuth flow for ChatGPT Subscription sign-in, including polling for authorization status.
- Updated admin interface to handle ChatGPT Subscription, including disabling API key input and providing user guidance.
- Enhanced cost tracking logic to differentiate between subscription and non-subscription endpoints.
- Added new slash commands for managing skills, including listing, searching, and invoking skills.
- Implemented caching for skill catalog to optimize performance.
- Updated tests to cover new ChatGPT Subscription functionality and ensure proper endpoint probing.
- Refactored existing code to accommodate new features and improve maintainability.
* refactor: share provider device-flow setup
- reuse one device-flow backend for Copilot and ChatGPT Subscription
- add one frontend device-flow helper for Settings and /setup
- put GitHub Copilot back into Add Models, now as a dropdown option
- make provider selection just select; clicking Add starts sign-in
- stop ChatGPT Subscription setup from opening auth tabs automatically
- make /setup copilot and /setup chatgpt-subscription work from chat
- show ChatGPT Subscription in the /setup suggestions
- show the real error message when setup fails
- add focused tests for the shared flow and setup UI
* feat(chatgpt-subscription): harden credential lifecycle and streamline auth UX
Backend:
- Resolve runtime bearer for provider-auth endpoints at probe time via a
shared _resolve_probe_key() that delegates to resolve_endpoint_runtime,
applied across all probe/refresh call sites.
- Skip live completion probes and health pings for discovery-only providers
(centralized behind _is_discovery_only_provider) — the Codex/Responses API
has no such endpoints, so status is derived from cached models.
- Never persist the short lived ChatGPT bearer to the plaintext sessions
table; proactively clear any stale bearer left by an earlier code path.
- Revoke orphaned ProviderAuthSession credentials when the last endpoint
backing them is deleted (_delete_orphaned_provider_auth), surfaced via
cleared_provider_auth in the delete response.
Frontend (admin.js):
- Auto-start the device-auth flow on provider selection so the authorization
panel (code + Authorize) shows immediately instead of behind a "Sign in" click.
- Remove the redundant top button for device auth providers, move retry
into the panel via an inline "Try again".
- Drop the self-evident hint text and add an execCommand clipboard fallback so
Copy works in non-secure (HTTP/LAN) contexts.
* fix: harden chatgpt subscription provider
* chore: remove PR media from branch
* Fix chatgpt subscription recovery and token handling
---------
Co-authored-by: 5p00kyy <admin@5p00ky.dev>
2026-06-08 18:19:18 +10:00
try {
const result = await runProviderDeviceFlow ( providerKey , {
openWindow : ( ) => { } ,
onStart : ( { start , authUrl } ) => {
if ( triggerEl ) triggerEl . textContent = 'Waiting...' ;
status . className = '' ;
const authLabel = providerKey === 'copilot' ? 'Authorize on GitHub' : 'Authorize with OpenAI' ;
const waitLabel = providerKey === 'copilot' ? 'Waiting for GitHub authorization...' : 'Waiting for ChatGPT authorization...' ;
status . innerHTML =
'<div class="adm-copilot-panel">' +
'<div class="adm-copilot-wait"><span class="admin-spinner"></span>' +
'<span>' + esc ( waitLabel ) + '</span></div>' +
'<div class="adm-copilot-coderow">' +
'<span class="adm-copilot-code-label">Code</span>' +
'<code class="adm-copilot-code">' + esc ( start . user _code ) + '</code>' +
'<button type="button" class="admin-btn-sm adm-device-auth-copy">Copy</button>' +
'</div>' +
'<a class="admin-btn-add adm-copilot-auth" href="' + encodeURI ( authUrl || '' ) + '" target="_blank" rel="noopener">' + esc ( authLabel ) + ' ↗</a>' +
'</div>' ;
const copyBtn = status . querySelector ( '.adm-device-auth-copy' ) ;
if ( copyBtn ) copyBtn . addEventListener ( 'click' , async ( ) => {
const code = start . user _code || '' ;
let ok = false ;
try {
if ( navigator . clipboard && window . isSecureContext ) {
await navigator . clipboard . writeText ( code ) ;
ok = true ;
}
} catch ( e ) { }
if ( ! ok ) {
// navigator.clipboard is unavailable in non-secure contexts (HTTP
// self-host over a LAN IP), so fall back to execCommand('copy').
const ta = document . createElement ( 'textarea' ) ;
ta . value = code ;
ta . style . cssText = 'position:fixed;top:0;left:0;width:1px;height:1px;padding:0;border:0;opacity:0;font-size:16px;' ;
document . body . appendChild ( ta ) ;
ta . focus ( ) ;
ta . select ( ) ;
try { ta . setSelectionRange ( 0 , code . length ) ; } catch ( e ) { }
try { ok = document . execCommand ( 'copy' ) ; } catch ( e ) { }
ta . remove ( ) ;
}
copyBtn . textContent = ok ? 'Copied' : 'Failed' ;
setTimeout ( ( ) => { copyBtn . textContent = 'Copy' ; } , 1500 ) ;
} ) ;
} ,
feat(provider): add GitHub Copilot provider with device-flow auth (#1480)
* feat(provider): add GitHub Copilot provider with device-flow auth
Adds GitHub Copilot as a model provider, so Copilot models (gpt-4o/4.1/5,
Claude, Gemini, …) work through the normal chat + agent loop, incl. native
tool calling and vision.
Auth is one-click via the GitHub OAuth device flow; the access token is stored
as the endpoint's (encrypted) api_key and sent directly as `Authorization:
Bearer` (no Copilot-token exchange, no refresh — matching how editors talk to
the Copilot API). Copilot is a normal ModelEndpoint detected by host; the only
provider-specific behaviour is a small set of required request headers,
injected centrally.
Sign-in is available from Settings → model endpoints ("Connect GitHub
Copilot") and from chat via `/setup copilot`.
- src/copilot.py (new), routes/copilot_routes.py (new): constants, header
builders, device-flow start/poll, model discovery, owner-scoped endpoint
provisioning.
- src/llm_core.py, src/endpoint_resolver.py: detect `copilot`, inject headers,
per-request x-initiator/vision.
- src/agent_loop.py: allowlist api.githubcopilot.com for native tool schemas.
- src/model_context.py: known context windows for Copilot (no unauthenticated
/models probe).
- static/, README, tests/test_copilot*.py.
* Tidy copilot_routes: clarify supports_tools, note _PENDING is per-process
2026-06-04 21:13:14 +02:00
} ) ;
feat: add ChatGPT Subscription provider (#2876)
* feat: Add ChatGPT Subscription support and related features
- Introduced a new provider option for ChatGPT Subscription in the endpoint selection UI.
- Implemented OAuth flow for ChatGPT Subscription sign-in, including polling for authorization status.
- Updated admin interface to handle ChatGPT Subscription, including disabling API key input and providing user guidance.
- Enhanced cost tracking logic to differentiate between subscription and non-subscription endpoints.
- Added new slash commands for managing skills, including listing, searching, and invoking skills.
- Implemented caching for skill catalog to optimize performance.
- Updated tests to cover new ChatGPT Subscription functionality and ensure proper endpoint probing.
- Refactored existing code to accommodate new features and improve maintainability.
* refactor: share provider device-flow setup
- reuse one device-flow backend for Copilot and ChatGPT Subscription
- add one frontend device-flow helper for Settings and /setup
- put GitHub Copilot back into Add Models, now as a dropdown option
- make provider selection just select; clicking Add starts sign-in
- stop ChatGPT Subscription setup from opening auth tabs automatically
- make /setup copilot and /setup chatgpt-subscription work from chat
- show ChatGPT Subscription in the /setup suggestions
- show the real error message when setup fails
- add focused tests for the shared flow and setup UI
* feat(chatgpt-subscription): harden credential lifecycle and streamline auth UX
Backend:
- Resolve runtime bearer for provider-auth endpoints at probe time via a
shared _resolve_probe_key() that delegates to resolve_endpoint_runtime,
applied across all probe/refresh call sites.
- Skip live completion probes and health pings for discovery-only providers
(centralized behind _is_discovery_only_provider) — the Codex/Responses API
has no such endpoints, so status is derived from cached models.
- Never persist the short lived ChatGPT bearer to the plaintext sessions
table; proactively clear any stale bearer left by an earlier code path.
- Revoke orphaned ProviderAuthSession credentials when the last endpoint
backing them is deleted (_delete_orphaned_provider_auth), surfaced via
cleared_provider_auth in the delete response.
Frontend (admin.js):
- Auto-start the device-auth flow on provider selection so the authorization
panel (code + Authorize) shows immediately instead of behind a "Sign in" click.
- Remove the redundant top button for device auth providers, move retry
into the panel via an inline "Try again".
- Drop the self-evident hint text and add an execCommand clipboard fallback so
Copy works in non-secure (HTTP/LAN) contexts.
* fix: harden chatgpt subscription provider
* chore: remove PR media from branch
* Fix chatgpt subscription recovery and token handling
---------
Co-authored-by: 5p00kyy <admin@5p00ky.dev>
2026-06-08 18:19:18 +10:00
if ( result . status === 'authorized' ) {
const endpoint = result . endpoint || { } ;
const n = ( ( endpoint && endpoint . models ) || [ ] ) . length ;
status . className = 'admin-success' ;
status . textContent = 'Connected - ' + n + ' ' + config . label + ' model' + ( n !== 1 ? 's' : '' ) + ' available.' ;
if ( endpoint && endpoint . id ) _recentlyAddedEpId = String ( endpoint . id ) ;
await loadEndpoints ( ) ;
await _selectAddedModelInChat ( endpoint || { } ) ;
reset ( ) ;
return ;
}
if ( result . status === 'failed' ) {
reset ( ) ;
showAuthError ( 'Authorization failed (' + ( result . error || 'denied' ) + ').' ) ;
return ;
}
if ( result . status === 'expired' ) {
reset ( ) ;
showAuthError ( 'Authorization expired.' ) ;
return ;
}
} catch ( e ) {
reset ( ) ;
showAuthError ( formatDeviceFlowError ( e ) ) ;
}
feat(provider): add GitHub Copilot provider with device-flow auth (#1480)
* feat(provider): add GitHub Copilot provider with device-flow auth
Adds GitHub Copilot as a model provider, so Copilot models (gpt-4o/4.1/5,
Claude, Gemini, …) work through the normal chat + agent loop, incl. native
tool calling and vision.
Auth is one-click via the GitHub OAuth device flow; the access token is stored
as the endpoint's (encrypted) api_key and sent directly as `Authorization:
Bearer` (no Copilot-token exchange, no refresh — matching how editors talk to
the Copilot API). Copilot is a normal ModelEndpoint detected by host; the only
provider-specific behaviour is a small set of required request headers,
injected centrally.
Sign-in is available from Settings → model endpoints ("Connect GitHub
Copilot") and from chat via `/setup copilot`.
- src/copilot.py (new), routes/copilot_routes.py (new): constants, header
builders, device-flow start/poll, model discovery, owner-scoped endpoint
provisioning.
- src/llm_core.py, src/endpoint_resolver.py: detect `copilot`, inject headers,
per-request x-initiator/vision.
- src/agent_loop.py: allowlist api.githubcopilot.com for native tool schemas.
- src/model_context.py: known context windows for Copilot (no unauthenticated
/models probe).
- static/, README, tests/test_copilot*.py.
* Tidy copilot_routes: clarify supports_tools, note _PENDING is per-process
2026-06-04 21:13:14 +02:00
}
Cookbook UI: Ollama browser, advanced serve fold, API tokens form, diagnosis toolbar, polish
Surface a lot of accumulated cookbook + UI work as a single non-agent
commit so the agent rework lands cleanly.
Highlights:
- Ollama as a first-class backend in the Cookbook:
* Download input accepts ollama-style names (name:tag) → backend=ollama
* /api/cookbook/ollama/library (cached scrape of ollama.com + curated
fallback so classic models like qwen2.5 stay reachable)
* "Browse Ollama library" toggle below Download with size chips
* Engine=Ollama in hwfit toolbar merges the Ollama library into the
main scan list as per-tag rows with the same Fit/Param/Quant/VRAM
columns; click → fills Download input
- API Tokens form added to Integrations panel (matching wired
loadTokens()/initTokenForm() that had no HTML)
- Serve panel polish: Advanced fold tightening (-8px nudges on vLLM
checks, Extra args, Spec row), n_cpu_moe + Split Mode controls
pulled up 8px to align with the row's checkboxes, GGUF File dropdown
exposed for Ollama backend, GPU re-render on Edit serve restore,
_forceBackend flag so saved serveState wins over backend detection,
cookbook:servers-changed CustomEvent so panels don't need refresh
- Models page redesign: Add Models row (URL + hidden API key reveal +
Type select + Scan/Ollama/Key/Test/Add icon buttons), Probe All +
Clear-offline buttons in Added Models toolbar, offline-pill removed
(opacity already conveys state), Engine dropdown gains Ollama option
- _ping_endpoint probes /v1/models then base, accepts 4xx as
reachable (vLLM returns 404 on bare /v1, fully working endpoints
were showing offline)
- Diagnosis card: × dismiss + Copy bundle buttons restored on the
serve error feedback card
- Orphan tmux sweep re-enabled behind a 60s rate-limit + background
Thread (off the main event loop) so dead serves get discovered
- cookbook_routes auto-register watchdog: drops the endpoint if the
serve session exits non-zero within the first ~3min
- ollama-rocm sidecar awareness in download wrapper (`docker exec
ollama-rocm ollama pull` when host ollama isn't installed)
- Skill extractor sets initial_status="published" when
auto_approve_skills pref is on (audit demotes later)
- Skill list / model list / cookbook scan misc polish
2026-06-08 22:38:49 +09:00
// API Key reveal toggle. The key inputs are hidden by default so the Add
// form reads as a single action row; the Key button toggles the input row
// and flips aria-expanded for screen readers / CSS pseudo-classes.
const _wireKeyToggle = ( btnId , rowId ) => {
const btn = el ( btnId ) ;
const row = el ( rowId ) ;
if ( ! btn || ! row ) return ;
btn . addEventListener ( 'click' , ( ) => {
const showing = row . style . display !== 'none' ;
row . style . display = showing ? 'none' : '' ;
btn . setAttribute ( 'aria-expanded' , showing ? 'false' : 'true' ) ;
btn . style . opacity = showing ? '0.75' : '1' ;
if ( ! showing ) {
const inp = row . querySelector ( 'input' ) ;
if ( inp ) inp . focus ( ) ;
}
} ) ;
} ;
_wireKeyToggle ( 'adm-epLocalKeyBtn' , 'adm-epLocalApiKey-row' ) ;
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
// Delegated link handler for jumping between settings tabs.
// [data-go-added-models] → quick shortcut for the Added Models tab
// [data-go-settings-tab="X"] → any tab whose nav button has data-settings-tab="X"
// [data-go-scroll-to="#elementId"] → after switching, scroll the element into view
document . addEventListener ( 'click' , ( e ) => {
const explicit = e . target . closest ( '[data-go-settings-tab]' ) ;
if ( explicit ) {
e . preventDefault ( ) ;
const tab = explicit . getAttribute ( 'data-go-settings-tab' ) ;
const scrollTo = explicit . getAttribute ( 'data-go-scroll-to' ) ;
const btn = document . querySelector ( ` [data-settings-tab=" ${ tab } "] ` ) ;
if ( btn ) btn . click ( ) ;
if ( scrollTo ) {
// Defer to the next frame so the panel has actually become visible
// before we try to scroll into it.
requestAnimationFrame ( ( ) => {
const target = document . querySelector ( scrollTo ) ;
if ( target ) target . scrollIntoView ( { behavior : 'smooth' , block : 'start' } ) ;
} ) ;
}
return ;
}
const link = e . target . closest ( '[data-go-added-models]' ) ;
if ( ! link ) return ;
e . preventDefault ( ) ;
const btn = document . querySelector ( '[data-settings-tab="added-models"]' ) ;
if ( btn ) btn . click ( ) ;
} ) ;
// Generic open/close helper for the kebab dropdowns in this card.
// Both the Local and API cards use the same shape: an h2-anchored button
// with id "<prefix>MoreBtn" toggles a sibling menu with id "<prefix>MoreMenu".
// Global Esc handler: close any currently-open kebab menu in the admin
// panel regardless of which _wireKebab instance owns it. Belt-and-braces
// backup for the per-instance handler below — registered once.
if ( ! document . _admKebabEscWired ) {
document . _admKebabEscWired = true ;
document . addEventListener ( 'keydown' , ( e ) => {
if ( e . key !== 'Escape' ) return ;
// Any visible kebab dropdown in the admin panel — match by id pattern
// so adding a new kebab elsewhere automatically benefits.
const menus = document . querySelectorAll (
'#adm-epLocalMoreMenu, #adm-epApiMoreMenu'
) ;
let closed = false ;
menus . forEach ( ( m ) => {
if ( m && m . style . display !== 'none' ) {
m . style . display = 'none' ;
// Sync the associated button's aria-expanded when we can find it.
const btn = document . getElementById ( m . id . replace ( 'Menu' , 'Btn' ) ) ;
if ( btn ) btn . setAttribute ( 'aria-expanded' , 'false' ) ;
closed = true ;
}
} ) ;
if ( closed ) e . stopPropagation ( ) ;
} , { capture : true } ) ;
}
const _wireKebab = ( btnId , menuId , onItem ) => {
const btn = el ( btnId ) ;
const menu = el ( menuId ) ;
if ( ! btn || ! menu ) return ;
const isOpen = ( ) => menu . style . display !== 'none' ;
const close = ( ) => { menu . style . display = 'none' ; btn . setAttribute ( 'aria-expanded' , 'false' ) ; } ;
const open = ( ) => { menu . style . display = 'flex' ; btn . setAttribute ( 'aria-expanded' , 'true' ) ; } ;
btn . addEventListener ( 'click' , ( e ) => {
e . stopPropagation ( ) ;
if ( isOpen ( ) ) close ( ) ; else open ( ) ;
} ) ;
menu . addEventListener ( 'click' , ( e ) => {
const item = e . target . closest ( '.adm-more-item' ) ;
if ( ! item ) return ;
if ( onItem ) onItem ( item , e ) ;
close ( ) ;
} ) ;
document . addEventListener ( 'click' , ( e ) => {
if ( ! isOpen ( ) ) return ;
if ( e . target . closest ( '#' + menuId + ', #' + btnId ) ) return ;
close ( ) ;
} ) ;
// Use capture phase so this fires before the settings-modal Esc handler
// (which is in bubble phase). stopPropagation prevents the modal from
// closing when the user only meant to dismiss this menu.
document . addEventListener ( 'keydown' , ( e ) => {
if ( e . key === 'Escape' && isOpen ( ) ) {
e . stopPropagation ( ) ;
close ( ) ;
}
} , { capture : true } ) ;
} ;
// API card "..." menu: contains the Proxy/API connection-mode toggle.
// Sync the visible checkmarks with the hidden #adm-epKind select so
// downstream code (which reads kindSel.value) keeps working.
( function wireApiKindMenu ( ) {
const kind = el ( 'adm-epKind' ) ;
if ( ! kind ) return ;
const opts = document . querySelectorAll ( '#adm-epApiMoreMenu .adm-kind-opt' ) ;
const sync = ( ) => {
opts . forEach ( ( o ) => {
const check = o . querySelector ( '.adm-kind-check' ) ;
if ( check ) check . style . visibility = ( o . dataset . kind === kind . value ) ? 'visible' : 'hidden' ;
} ) ;
} ;
sync ( ) ;
kind . addEventListener ( 'change' , sync ) ;
_wireKebab ( 'adm-epApiMoreBtn' , 'adm-epApiMoreMenu' , ( item ) => {
const k = item . dataset . kind ;
if ( ! k ) return ;
kind . value = k ;
kind . dispatchEvent ( new Event ( 'change' ) ) ;
} ) ;
} ) ( ) ;
// Local card "..." kebab: holds Scan network / Ollama / API key reveal.
// Item buttons keep their own click handlers; the helper just handles
// open/close + outside-click + Esc.
_wireKebab ( 'adm-epLocalMoreBtn' , 'adm-epLocalMoreMenu' ) ;
Cookbook UI: Ollama browser, advanced serve fold, API tokens form, diagnosis toolbar, polish
Surface a lot of accumulated cookbook + UI work as a single non-agent
commit so the agent rework lands cleanly.
Highlights:
- Ollama as a first-class backend in the Cookbook:
* Download input accepts ollama-style names (name:tag) → backend=ollama
* /api/cookbook/ollama/library (cached scrape of ollama.com + curated
fallback so classic models like qwen2.5 stay reachable)
* "Browse Ollama library" toggle below Download with size chips
* Engine=Ollama in hwfit toolbar merges the Ollama library into the
main scan list as per-tag rows with the same Fit/Param/Quant/VRAM
columns; click → fills Download input
- API Tokens form added to Integrations panel (matching wired
loadTokens()/initTokenForm() that had no HTML)
- Serve panel polish: Advanced fold tightening (-8px nudges on vLLM
checks, Extra args, Spec row), n_cpu_moe + Split Mode controls
pulled up 8px to align with the row's checkboxes, GGUF File dropdown
exposed for Ollama backend, GPU re-render on Edit serve restore,
_forceBackend flag so saved serveState wins over backend detection,
cookbook:servers-changed CustomEvent so panels don't need refresh
- Models page redesign: Add Models row (URL + hidden API key reveal +
Type select + Scan/Ollama/Key/Test/Add icon buttons), Probe All +
Clear-offline buttons in Added Models toolbar, offline-pill removed
(opacity already conveys state), Engine dropdown gains Ollama option
- _ping_endpoint probes /v1/models then base, accepts 4xx as
reachable (vLLM returns 404 on bare /v1, fully working endpoints
were showing offline)
- Diagnosis card: × dismiss + Copy bundle buttons restored on the
serve error feedback card
- Orphan tmux sweep re-enabled behind a 60s rate-limit + background
Thread (off the main event loop) so dead serves get discovered
- cookbook_routes auto-register watchdog: drops the endpoint if the
serve session exits non-zero within the first ~3min
- ollama-rocm sidecar awareness in download wrapper (`docker exec
ollama-rocm ollama pull` when host ollama isn't installed)
- Skill extractor sets initial_status="published" when
auto_approve_skills pref is on (audit demotes later)
- Skill list / model list / cookbook scan misc polish
2026-06-08 22:38:49 +09:00
// ── Added Models toolbar: Probe + Clear offline ────────────────────
// Both buttons act over the currently-rendered endpoint list. The
// online/offline marker is stamped on each row's [data-adm-ep-online]
// attribute by loadEndpoints(), so both buttons just iterate the DOM
// without re-fetching anything they don't already have.
const _refreshOfflineCount = ( ) => {
const lbl = el ( 'adm-epOfflineCount' ) ;
if ( ! lbl ) return ;
const n = document . querySelectorAll ( '[data-adm-ep-id] [data-adm-ep-online="0"]' ) . length ;
lbl . textContent = n > 0 ? ` ( ${ n } ) ` : '' ;
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
// Hide the button entirely when there's nothing offline — no point
// showing an action that has nothing to act on.
Cookbook UI: Ollama browser, advanced serve fold, API tokens form, diagnosis toolbar, polish
Surface a lot of accumulated cookbook + UI work as a single non-agent
commit so the agent rework lands cleanly.
Highlights:
- Ollama as a first-class backend in the Cookbook:
* Download input accepts ollama-style names (name:tag) → backend=ollama
* /api/cookbook/ollama/library (cached scrape of ollama.com + curated
fallback so classic models like qwen2.5 stay reachable)
* "Browse Ollama library" toggle below Download with size chips
* Engine=Ollama in hwfit toolbar merges the Ollama library into the
main scan list as per-tag rows with the same Fit/Param/Quant/VRAM
columns; click → fills Download input
- API Tokens form added to Integrations panel (matching wired
loadTokens()/initTokenForm() that had no HTML)
- Serve panel polish: Advanced fold tightening (-8px nudges on vLLM
checks, Extra args, Spec row), n_cpu_moe + Split Mode controls
pulled up 8px to align with the row's checkboxes, GGUF File dropdown
exposed for Ollama backend, GPU re-render on Edit serve restore,
_forceBackend flag so saved serveState wins over backend detection,
cookbook:servers-changed CustomEvent so panels don't need refresh
- Models page redesign: Add Models row (URL + hidden API key reveal +
Type select + Scan/Ollama/Key/Test/Add icon buttons), Probe All +
Clear-offline buttons in Added Models toolbar, offline-pill removed
(opacity already conveys state), Engine dropdown gains Ollama option
- _ping_endpoint probes /v1/models then base, accepts 4xx as
reachable (vLLM returns 404 on bare /v1, fully working endpoints
were showing offline)
- Diagnosis card: × dismiss + Copy bundle buttons restored on the
serve error feedback card
- Orphan tmux sweep re-enabled behind a 60s rate-limit + background
Thread (off the main event loop) so dead serves get discovered
- cookbook_routes auto-register watchdog: drops the endpoint if the
serve session exits non-zero within the first ~3min
- ollama-rocm sidecar awareness in download wrapper (`docker exec
ollama-rocm ollama pull` when host ollama isn't installed)
- Skill extractor sets initial_status="published" when
auto_approve_skills pref is on (audit demotes later)
- Skill list / model list / cookbook scan misc polish
2026-06-08 22:38:49 +09:00
const btn = el ( 'adm-epClearOfflineBtn' ) ;
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
if ( btn ) btn . style . display = n === 0 ? 'none' : '' ;
Cookbook UI: Ollama browser, advanced serve fold, API tokens form, diagnosis toolbar, polish
Surface a lot of accumulated cookbook + UI work as a single non-agent
commit so the agent rework lands cleanly.
Highlights:
- Ollama as a first-class backend in the Cookbook:
* Download input accepts ollama-style names (name:tag) → backend=ollama
* /api/cookbook/ollama/library (cached scrape of ollama.com + curated
fallback so classic models like qwen2.5 stay reachable)
* "Browse Ollama library" toggle below Download with size chips
* Engine=Ollama in hwfit toolbar merges the Ollama library into the
main scan list as per-tag rows with the same Fit/Param/Quant/VRAM
columns; click → fills Download input
- API Tokens form added to Integrations panel (matching wired
loadTokens()/initTokenForm() that had no HTML)
- Serve panel polish: Advanced fold tightening (-8px nudges on vLLM
checks, Extra args, Spec row), n_cpu_moe + Split Mode controls
pulled up 8px to align with the row's checkboxes, GGUF File dropdown
exposed for Ollama backend, GPU re-render on Edit serve restore,
_forceBackend flag so saved serveState wins over backend detection,
cookbook:servers-changed CustomEvent so panels don't need refresh
- Models page redesign: Add Models row (URL + hidden API key reveal +
Type select + Scan/Ollama/Key/Test/Add icon buttons), Probe All +
Clear-offline buttons in Added Models toolbar, offline-pill removed
(opacity already conveys state), Engine dropdown gains Ollama option
- _ping_endpoint probes /v1/models then base, accepts 4xx as
reachable (vLLM returns 404 on bare /v1, fully working endpoints
were showing offline)
- Diagnosis card: × dismiss + Copy bundle buttons restored on the
serve error feedback card
- Orphan tmux sweep re-enabled behind a 60s rate-limit + background
Thread (off the main event loop) so dead serves get discovered
- cookbook_routes auto-register watchdog: drops the endpoint if the
serve session exits non-zero within the first ~3min
- ollama-rocm sidecar awareness in download wrapper (`docker exec
ollama-rocm ollama pull` when host ollama isn't installed)
- Skill extractor sets initial_status="published" when
auto_approve_skills pref is on (audit demotes later)
- Skill list / model list / cookbook scan misc polish
2026-06-08 22:38:49 +09:00
} ;
// Wire after every loadEndpoints() run by patching the render hook —
// simplest path: MutationObserver on the two list containers.
const _obsRoots = [ 'adm-epList-local' , 'adm-epList-api' ]
. map ( id => el ( id ) ) . filter ( Boolean ) ;
if ( _obsRoots . length ) {
const mo = new MutationObserver ( _refreshOfflineCount ) ;
_obsRoots . forEach ( r => mo . observe ( r , { childList : true , subtree : true } ) ) ;
_refreshOfflineCount ( ) ;
}
2026-06-29 11:20:32 +00:00
const _fetchWithTimeout = async ( url , opts = { } , timeoutMs = 25000 ) => {
const ctrl = new AbortController ( ) ;
const timer = setTimeout ( ( ) => ctrl . abort ( ) , timeoutMs ) ;
try {
return await fetch ( url , { ... opts , signal : ctrl . signal } ) ;
} finally {
clearTimeout ( timer ) ;
}
} ;
const _collectAddedEndpointIds = async ( ) => {
const domIds = Array . from ( document . querySelectorAll ( '[data-adm-ep-id]' ) )
. map ( r => r . getAttribute ( 'data-adm-ep-id' ) )
. filter ( Boolean ) ;
if ( domIds . length ) return Array . from ( new Set ( domIds ) ) ;
try {
const res = await fetch ( '/api/model-endpoints' , { credentials : 'same-origin' } ) ;
const data = await res . json ( ) . catch ( ( ) => [ ] ) ;
return ( Array . isArray ( data ) ? data : [ ] ) . map ( ep => ep && ep . id ) . filter ( Boolean ) ;
} catch ( _ ) {
return [ ] ;
}
} ;
const _setProbeAllButtonLabel = async ( btn , text , whirlpoolRef ) => {
btn . innerHTML = '' ;
if ( whirlpoolRef && whirlpoolRef . element ) btn . appendChild ( whirlpoolRef . element ) ;
btn . appendChild ( document . createTextNode ( text ) ) ;
} ;
if ( ! window . _ _admEpProbeAllWired ) {
window . _ _admEpProbeAllWired = true ;
document . addEventListener ( 'click' , async ( ev ) => {
const probeAllBtn = ev . target . closest ( '#adm-epProbeAllBtn' ) ;
if ( ! probeAllBtn || probeAllBtn . disabled ) return ;
ev . preventDefault ( ) ;
Cookbook UI: Ollama browser, advanced serve fold, API tokens form, diagnosis toolbar, polish
Surface a lot of accumulated cookbook + UI work as a single non-agent
commit so the agent rework lands cleanly.
Highlights:
- Ollama as a first-class backend in the Cookbook:
* Download input accepts ollama-style names (name:tag) → backend=ollama
* /api/cookbook/ollama/library (cached scrape of ollama.com + curated
fallback so classic models like qwen2.5 stay reachable)
* "Browse Ollama library" toggle below Download with size chips
* Engine=Ollama in hwfit toolbar merges the Ollama library into the
main scan list as per-tag rows with the same Fit/Param/Quant/VRAM
columns; click → fills Download input
- API Tokens form added to Integrations panel (matching wired
loadTokens()/initTokenForm() that had no HTML)
- Serve panel polish: Advanced fold tightening (-8px nudges on vLLM
checks, Extra args, Spec row), n_cpu_moe + Split Mode controls
pulled up 8px to align with the row's checkboxes, GGUF File dropdown
exposed for Ollama backend, GPU re-render on Edit serve restore,
_forceBackend flag so saved serveState wins over backend detection,
cookbook:servers-changed CustomEvent so panels don't need refresh
- Models page redesign: Add Models row (URL + hidden API key reveal +
Type select + Scan/Ollama/Key/Test/Add icon buttons), Probe All +
Clear-offline buttons in Added Models toolbar, offline-pill removed
(opacity already conveys state), Engine dropdown gains Ollama option
- _ping_endpoint probes /v1/models then base, accepts 4xx as
reachable (vLLM returns 404 on bare /v1, fully working endpoints
were showing offline)
- Diagnosis card: × dismiss + Copy bundle buttons restored on the
serve error feedback card
- Orphan tmux sweep re-enabled behind a 60s rate-limit + background
Thread (off the main event loop) so dead serves get discovered
- cookbook_routes auto-register watchdog: drops the endpoint if the
serve session exits non-zero within the first ~3min
- ollama-rocm sidecar awareness in download wrapper (`docker exec
ollama-rocm ollama pull` when host ollama isn't installed)
- Skill extractor sets initial_status="published" when
auto_approve_skills pref is on (audit demotes later)
- Skill list / model list / cookbook scan misc polish
2026-06-08 22:38:49 +09:00
probeAllBtn . disabled = true ;
const origHTML = probeAllBtn . innerHTML ;
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
let _wp = null ;
try {
2026-06-29 11:20:32 +00:00
try {
const sp = window . spinnerModule || ( await import ( './spinner.js' ) ) . default ;
_wp = sp . createWhirlpool ( 11 ) ;
_wp . element . style . cssText = 'display:inline-flex;width:11px;height:11px;margin:0 4px 0 0;' ;
await _setProbeAllButtonLabel ( probeAllBtn , 'Probing' , _wp ) ;
} catch ( _ ) {
probeAllBtn . innerHTML = '<span style="opacity:0.7;">Probing...</span>' ;
}
await _fetchWithTimeout ( '/api/model-endpoints/probe-local' , { credentials : 'same-origin' } , 12000 ) . catch ( ( ) => null ) ;
const ids = await _collectAddedEndpointIds ( ) ;
if ( ! ids . length ) {
await loadEndpoints ( ) ;
if ( uiModule && uiModule . showToast ) uiModule . showToast ( 'No endpoints to probe' , 1800 ) ;
return ;
}
let done = 0 ;
let failed = 0 ;
Cookbook UI: Ollama browser, advanced serve fold, API tokens form, diagnosis toolbar, polish
Surface a lot of accumulated cookbook + UI work as a single non-agent
commit so the agent rework lands cleanly.
Highlights:
- Ollama as a first-class backend in the Cookbook:
* Download input accepts ollama-style names (name:tag) → backend=ollama
* /api/cookbook/ollama/library (cached scrape of ollama.com + curated
fallback so classic models like qwen2.5 stay reachable)
* "Browse Ollama library" toggle below Download with size chips
* Engine=Ollama in hwfit toolbar merges the Ollama library into the
main scan list as per-tag rows with the same Fit/Param/Quant/VRAM
columns; click → fills Download input
- API Tokens form added to Integrations panel (matching wired
loadTokens()/initTokenForm() that had no HTML)
- Serve panel polish: Advanced fold tightening (-8px nudges on vLLM
checks, Extra args, Spec row), n_cpu_moe + Split Mode controls
pulled up 8px to align with the row's checkboxes, GGUF File dropdown
exposed for Ollama backend, GPU re-render on Edit serve restore,
_forceBackend flag so saved serveState wins over backend detection,
cookbook:servers-changed CustomEvent so panels don't need refresh
- Models page redesign: Add Models row (URL + hidden API key reveal +
Type select + Scan/Ollama/Key/Test/Add icon buttons), Probe All +
Clear-offline buttons in Added Models toolbar, offline-pill removed
(opacity already conveys state), Engine dropdown gains Ollama option
- _ping_endpoint probes /v1/models then base, accepts 4xx as
reachable (vLLM returns 404 on bare /v1, fully working endpoints
were showing offline)
- Diagnosis card: × dismiss + Copy bundle buttons restored on the
serve error feedback card
- Orphan tmux sweep re-enabled behind a 60s rate-limit + background
Thread (off the main event loop) so dead serves get discovered
- cookbook_routes auto-register watchdog: drops the endpoint if the
serve session exits non-zero within the first ~3min
- ollama-rocm sidecar awareness in download wrapper (`docker exec
ollama-rocm ollama pull` when host ollama isn't installed)
- Skill extractor sets initial_status="published" when
auto_approve_skills pref is on (audit demotes later)
- Skill list / model list / cookbook scan misc polish
2026-06-08 22:38:49 +09:00
const lane = async ( id ) => {
2026-06-29 11:20:32 +00:00
try {
const res = await _fetchWithTimeout ( ` /api/model-endpoints/ ${ encodeURIComponent ( id ) } /models?refresh=true&refresh_timeout=20 ` , {
credentials : 'same-origin'
} , 25000 ) ;
if ( ! res || ! res . ok || res . headers . get ( 'X-Model-Refresh-Status' ) === 'failed' ) failed += 1 ;
else await res . json ( ) . catch ( ( ) => null ) ;
} catch ( err ) {
failed += 1 ;
console . warn ( 'Endpoint probe failed' , id , err ) ;
} finally {
done += 1 ;
try { await _setProbeAllButtonLabel ( probeAllBtn , ` Probing ${ done } / ${ ids . length } ` , _wp ) ; } catch ( _ ) { }
}
Cookbook UI: Ollama browser, advanced serve fold, API tokens form, diagnosis toolbar, polish
Surface a lot of accumulated cookbook + UI work as a single non-agent
commit so the agent rework lands cleanly.
Highlights:
- Ollama as a first-class backend in the Cookbook:
* Download input accepts ollama-style names (name:tag) → backend=ollama
* /api/cookbook/ollama/library (cached scrape of ollama.com + curated
fallback so classic models like qwen2.5 stay reachable)
* "Browse Ollama library" toggle below Download with size chips
* Engine=Ollama in hwfit toolbar merges the Ollama library into the
main scan list as per-tag rows with the same Fit/Param/Quant/VRAM
columns; click → fills Download input
- API Tokens form added to Integrations panel (matching wired
loadTokens()/initTokenForm() that had no HTML)
- Serve panel polish: Advanced fold tightening (-8px nudges on vLLM
checks, Extra args, Spec row), n_cpu_moe + Split Mode controls
pulled up 8px to align with the row's checkboxes, GGUF File dropdown
exposed for Ollama backend, GPU re-render on Edit serve restore,
_forceBackend flag so saved serveState wins over backend detection,
cookbook:servers-changed CustomEvent so panels don't need refresh
- Models page redesign: Add Models row (URL + hidden API key reveal +
Type select + Scan/Ollama/Key/Test/Add icon buttons), Probe All +
Clear-offline buttons in Added Models toolbar, offline-pill removed
(opacity already conveys state), Engine dropdown gains Ollama option
- _ping_endpoint probes /v1/models then base, accepts 4xx as
reachable (vLLM returns 404 on bare /v1, fully working endpoints
were showing offline)
- Diagnosis card: × dismiss + Copy bundle buttons restored on the
serve error feedback card
- Orphan tmux sweep re-enabled behind a 60s rate-limit + background
Thread (off the main event loop) so dead serves get discovered
- cookbook_routes auto-register watchdog: drops the endpoint if the
serve session exits non-zero within the first ~3min
- ollama-rocm sidecar awareness in download wrapper (`docker exec
ollama-rocm ollama pull` when host ollama isn't installed)
- Skill extractor sets initial_status="published" when
auto_approve_skills pref is on (audit demotes later)
- Skill list / model list / cookbook scan misc polish
2026-06-08 22:38:49 +09:00
} ;
const queue = [ ... ids ] ;
2026-06-29 11:20:32 +00:00
const workers = Array . from ( { length : Math . min ( 4 , queue . length ) } , ( ) => ( async ( ) => {
Cookbook UI: Ollama browser, advanced serve fold, API tokens form, diagnosis toolbar, polish
Surface a lot of accumulated cookbook + UI work as a single non-agent
commit so the agent rework lands cleanly.
Highlights:
- Ollama as a first-class backend in the Cookbook:
* Download input accepts ollama-style names (name:tag) → backend=ollama
* /api/cookbook/ollama/library (cached scrape of ollama.com + curated
fallback so classic models like qwen2.5 stay reachable)
* "Browse Ollama library" toggle below Download with size chips
* Engine=Ollama in hwfit toolbar merges the Ollama library into the
main scan list as per-tag rows with the same Fit/Param/Quant/VRAM
columns; click → fills Download input
- API Tokens form added to Integrations panel (matching wired
loadTokens()/initTokenForm() that had no HTML)
- Serve panel polish: Advanced fold tightening (-8px nudges on vLLM
checks, Extra args, Spec row), n_cpu_moe + Split Mode controls
pulled up 8px to align with the row's checkboxes, GGUF File dropdown
exposed for Ollama backend, GPU re-render on Edit serve restore,
_forceBackend flag so saved serveState wins over backend detection,
cookbook:servers-changed CustomEvent so panels don't need refresh
- Models page redesign: Add Models row (URL + hidden API key reveal +
Type select + Scan/Ollama/Key/Test/Add icon buttons), Probe All +
Clear-offline buttons in Added Models toolbar, offline-pill removed
(opacity already conveys state), Engine dropdown gains Ollama option
- _ping_endpoint probes /v1/models then base, accepts 4xx as
reachable (vLLM returns 404 on bare /v1, fully working endpoints
were showing offline)
- Diagnosis card: × dismiss + Copy bundle buttons restored on the
serve error feedback card
- Orphan tmux sweep re-enabled behind a 60s rate-limit + background
Thread (off the main event loop) so dead serves get discovered
- cookbook_routes auto-register watchdog: drops the endpoint if the
serve session exits non-zero within the first ~3min
- ollama-rocm sidecar awareness in download wrapper (`docker exec
ollama-rocm ollama pull` when host ollama isn't installed)
- Skill extractor sets initial_status="published" when
auto_approve_skills pref is on (audit demotes later)
- Skill list / model list / cookbook scan misc polish
2026-06-08 22:38:49 +09:00
while ( queue . length ) {
const id = queue . shift ( ) ;
if ( id ) await lane ( id ) ;
}
} ) ( ) ) ;
await Promise . all ( workers ) ;
await loadEndpoints ( ) ;
2026-06-29 11:20:32 +00:00
_refreshOfflineCount ( ) ;
if ( uiModule && uiModule . showToast ) {
const ok = Math . max ( 0 , ids . length - failed ) ;
uiModule . showToast ( failed ? ` Probed ${ ok } / ${ ids . length } endpoints; ${ failed } failed ` : ` Probed ${ ids . length } endpoints ` , failed ? 4200 : 1800 ) ;
}
Cookbook UI: Ollama browser, advanced serve fold, API tokens form, diagnosis toolbar, polish
Surface a lot of accumulated cookbook + UI work as a single non-agent
commit so the agent rework lands cleanly.
Highlights:
- Ollama as a first-class backend in the Cookbook:
* Download input accepts ollama-style names (name:tag) → backend=ollama
* /api/cookbook/ollama/library (cached scrape of ollama.com + curated
fallback so classic models like qwen2.5 stay reachable)
* "Browse Ollama library" toggle below Download with size chips
* Engine=Ollama in hwfit toolbar merges the Ollama library into the
main scan list as per-tag rows with the same Fit/Param/Quant/VRAM
columns; click → fills Download input
- API Tokens form added to Integrations panel (matching wired
loadTokens()/initTokenForm() that had no HTML)
- Serve panel polish: Advanced fold tightening (-8px nudges on vLLM
checks, Extra args, Spec row), n_cpu_moe + Split Mode controls
pulled up 8px to align with the row's checkboxes, GGUF File dropdown
exposed for Ollama backend, GPU re-render on Edit serve restore,
_forceBackend flag so saved serveState wins over backend detection,
cookbook:servers-changed CustomEvent so panels don't need refresh
- Models page redesign: Add Models row (URL + hidden API key reveal +
Type select + Scan/Ollama/Key/Test/Add icon buttons), Probe All +
Clear-offline buttons in Added Models toolbar, offline-pill removed
(opacity already conveys state), Engine dropdown gains Ollama option
- _ping_endpoint probes /v1/models then base, accepts 4xx as
reachable (vLLM returns 404 on bare /v1, fully working endpoints
were showing offline)
- Diagnosis card: × dismiss + Copy bundle buttons restored on the
serve error feedback card
- Orphan tmux sweep re-enabled behind a 60s rate-limit + background
Thread (off the main event loop) so dead serves get discovered
- cookbook_routes auto-register watchdog: drops the endpoint if the
serve session exits non-zero within the first ~3min
- ollama-rocm sidecar awareness in download wrapper (`docker exec
ollama-rocm ollama pull` when host ollama isn't installed)
- Skill extractor sets initial_status="published" when
auto_approve_skills pref is on (audit demotes later)
- Skill list / model list / cookbook scan misc polish
2026-06-08 22:38:49 +09:00
} finally {
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
if ( _wp ) { try { _wp . destroy ( ) ; } catch ( _ ) { } }
Cookbook UI: Ollama browser, advanced serve fold, API tokens form, diagnosis toolbar, polish
Surface a lot of accumulated cookbook + UI work as a single non-agent
commit so the agent rework lands cleanly.
Highlights:
- Ollama as a first-class backend in the Cookbook:
* Download input accepts ollama-style names (name:tag) → backend=ollama
* /api/cookbook/ollama/library (cached scrape of ollama.com + curated
fallback so classic models like qwen2.5 stay reachable)
* "Browse Ollama library" toggle below Download with size chips
* Engine=Ollama in hwfit toolbar merges the Ollama library into the
main scan list as per-tag rows with the same Fit/Param/Quant/VRAM
columns; click → fills Download input
- API Tokens form added to Integrations panel (matching wired
loadTokens()/initTokenForm() that had no HTML)
- Serve panel polish: Advanced fold tightening (-8px nudges on vLLM
checks, Extra args, Spec row), n_cpu_moe + Split Mode controls
pulled up 8px to align with the row's checkboxes, GGUF File dropdown
exposed for Ollama backend, GPU re-render on Edit serve restore,
_forceBackend flag so saved serveState wins over backend detection,
cookbook:servers-changed CustomEvent so panels don't need refresh
- Models page redesign: Add Models row (URL + hidden API key reveal +
Type select + Scan/Ollama/Key/Test/Add icon buttons), Probe All +
Clear-offline buttons in Added Models toolbar, offline-pill removed
(opacity already conveys state), Engine dropdown gains Ollama option
- _ping_endpoint probes /v1/models then base, accepts 4xx as
reachable (vLLM returns 404 on bare /v1, fully working endpoints
were showing offline)
- Diagnosis card: × dismiss + Copy bundle buttons restored on the
serve error feedback card
- Orphan tmux sweep re-enabled behind a 60s rate-limit + background
Thread (off the main event loop) so dead serves get discovered
- cookbook_routes auto-register watchdog: drops the endpoint if the
serve session exits non-zero within the first ~3min
- ollama-rocm sidecar awareness in download wrapper (`docker exec
ollama-rocm ollama pull` when host ollama isn't installed)
- Skill extractor sets initial_status="published" when
auto_approve_skills pref is on (audit demotes later)
- Skill list / model list / cookbook scan misc polish
2026-06-08 22:38:49 +09:00
probeAllBtn . innerHTML = origHTML ;
probeAllBtn . disabled = false ;
}
} ) ;
}
const clearOfflineBtn = el ( 'adm-epClearOfflineBtn' ) ;
if ( clearOfflineBtn ) {
clearOfflineBtn . addEventListener ( 'click' , async ( ) => {
const offlineBtns = Array . from ( document . querySelectorAll ( '[data-adm-del-ep][data-adm-ep-online="0"]' ) ) ;
const ids = offlineBtns . map ( b => b . getAttribute ( 'data-adm-del-ep' ) ) . filter ( Boolean ) ;
if ( ! ids . length ) {
if ( uiModule && uiModule . showToast ) {
uiModule . showToast ( 'No offline endpoints — nothing to clear' , 1800 ) ;
}
return ;
}
const confirmMsg = ids . length === 1
? 'Remove 1 offline endpoint?'
: ` Remove ${ ids . length } offline endpoints? ` ;
if ( uiModule && uiModule . styledConfirm ) {
const ok = await uiModule . styledConfirm ( confirmMsg , { confirmText : 'Remove' , danger : true } ) ;
if ( ! ok ) return ;
} else if ( ! confirm ( confirmMsg ) ) {
return ;
}
clearOfflineBtn . disabled = true ;
// Optimistic UI: pull rows immediately, then fire the DELETEs.
offlineBtns . forEach ( b => {
const row = b . closest ( '[data-adm-ep-id]' ) ;
if ( row ) row . remove ( ) ;
} ) ;
await Promise . all ( ids . map ( id =>
fetch ( '/api/model-endpoints/' + id , { method : 'DELETE' , credentials : 'same-origin' } ) . catch ( ( ) => { } )
) ) ;
try { await loadEndpoints ( ) ; } catch ( _ ) { }
_refreshOfflineCount ( ) ;
if ( uiModule && uiModule . showToast ) uiModule . showToast ( ` Removed ${ ids . length } offline endpoint ${ ids . length === 1 ? '' : 's' } ` , 1800 ) ;
} ) ;
}
// Clear-on-focus for the API key inputs. The fields are type=password so the
// value is masked; users can't see what's there to edit it in place, so the
// expected gesture is "click in, type new key". Wiping on focus removes the
// select-all-and-delete dance.
const _wireClearOnFocus = ( id ) => {
const inp = el ( id ) ;
if ( ! inp ) return ;
inp . addEventListener ( 'focus' , ( ) => {
if ( inp . value ) inp . value = '' ;
} ) ;
} ;
_wireClearOnFocus ( 'adm-epLocalApiKey' ) ;
_wireClearOnFocus ( 'adm-epApiKey' ) ;
// Drop the Ollama provider logo into the Ollama Quickstart button. Reuses
// the same SVG the provider picker uses, so brand parity stays free.
try {
const _ollamaLogoSlot = document . querySelector ( '#adm-epOllamaBtn .adm-ollama-logo' ) ;
if ( _ollamaLogoSlot ) {
const svg = providerLogo ( 'ollama' ) || '' ;
if ( svg ) _ollamaLogoSlot . innerHTML = svg ;
}
} catch ( _ ) { }
2026-05-31 23:58:26 +09:00
// Local "Add" button — sibling form for self-hosted base URLs.
const localAddBtn = el ( 'adm-epLocalAddBtn' ) ;
2026-06-01 10:00:15 +09:00
const localTestBtn = el ( 'adm-epLocalTestBtn' ) ;
if ( localTestBtn ) {
localTestBtn . addEventListener ( 'click' , async ( ) => {
2026-06-15 22:49:06 +09:00
const testOriginalHtml = localTestBtn . innerHTML || '>Test' ;
2026-06-01 10:00:15 +09:00
const msg = _endpointMsg ( 'local' ) ;
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
msg . textContent = '' ; msg . className = 'adm-ep-inline-msg' ;
2026-06-01 10:00:15 +09:00
const raw = ( el ( 'adm-epLocalUrl' ) . value || '' ) . trim ( ) ;
if ( ! raw ) { msg . textContent = 'Enter a base URL to test' ; msg . className = 'admin-error' ; return ; }
const url = _normalizeBaseUrl ( raw ) ;
2026-06-02 07:36:54 -04:00
const keyEl = el ( 'adm-epLocalApiKey' ) ;
const apiKey = keyEl ? keyEl . value . trim ( ) : '' ;
2026-06-01 10:00:15 +09:00
localTestBtn . disabled = true ;
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
localTestBtn . innerHTML = testOriginalHtml . replace ( />Test\s*$/ , '>Testing...' ) ;
2026-06-01 10:00:15 +09:00
try {
const fd = new FormData ( ) ;
fd . append ( 'base_url' , url ) ;
2026-06-02 07:36:54 -04:00
if ( apiKey ) fd . append ( 'api_key' , apiKey ) ;
2026-06-01 10:00:15 +09:00
const res = await fetch ( '/api/model-endpoints/test' , { method : 'POST' , body : fd , credentials : 'same-origin' } ) ;
const d = await res . json ( ) ;
_renderEndpointTestResult ( msg , res , d ) ;
} catch ( e ) {
msg . textContent = 'Test failed: ' + ( e && e . message ? e . message : 'request failed' ) ;
msg . className = 'admin-error' ;
}
localTestBtn . disabled = false ;
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
localTestBtn . innerHTML = testOriginalHtml ;
2026-06-01 10:00:15 +09:00
} ) ;
}
2026-05-31 23:58:26 +09:00
if ( localAddBtn ) {
localAddBtn . addEventListener ( 'click' , async ( ) => {
2026-06-15 22:49:06 +09:00
const addOriginalHtml = localAddBtn . innerHTML || '>Add' ;
2026-06-01 10:00:15 +09:00
const msg = _endpointMsg ( 'local' ) ;
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
msg . textContent = '' ; msg . className = 'adm-ep-inline-msg' ;
2026-05-31 23:58:26 +09:00
const raw = ( el ( 'adm-epLocalUrl' ) . value || '' ) . trim ( ) ;
if ( ! raw ) { msg . textContent = 'Enter a base URL (e.g. http://localhost:8002/v1)' ; msg . className = 'admin-error' ; return ; }
const url = _normalizeBaseUrl ( raw ) ;
2026-06-02 07:36:54 -04:00
const keyEl = el ( 'adm-epLocalApiKey' ) ;
const apiKey = keyEl ? keyEl . value . trim ( ) : '' ;
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
localAddBtn . disabled = true ;
localAddBtn . innerHTML = addOriginalHtml . replace ( />Add\s*$/ , '>Adding...' ) ;
2026-05-31 23:58:26 +09:00
try {
const fd = new FormData ( ) ;
fd . append ( 'base_url' , url ) ;
2026-06-02 07:36:54 -04:00
if ( apiKey ) fd . append ( 'api_key' , apiKey ) ;
2026-06-04 00:56:11 -03:00
fd . append ( 'endpoint_kind' , 'local' ) ;
fd . append ( 'model_refresh_mode' , 'auto' ) ;
2026-05-31 23:58:26 +09:00
const lt = el ( 'adm-epLocalType' ) ;
if ( lt ) fd . append ( 'model_type' , lt . value ) ;
2026-06-01 10:00:15 +09:00
fd . append ( 'skip_probe' , 'false' ) ;
2026-05-31 23:58:26 +09:00
const res = await fetch ( '/api/model-endpoints' , { method : 'POST' , body : fd , credentials : 'same-origin' } ) ;
const d = await res . json ( ) ;
if ( res . ok ) {
el ( 'adm-epLocalUrl' ) . value = '' ;
2026-06-02 07:36:54 -04:00
if ( keyEl ) keyEl . value = '' ;
2026-05-31 23:58:26 +09:00
if ( lt ) lt . value = 'llm' ;
if ( d . id ) _recentlyAddedEpId = String ( d . id ) ;
2026-06-01 10:00:15 +09:00
await loadEndpoints ( ) ;
await _selectAddedModelInChat ( d ) ;
2026-05-31 23:58:26 +09:00
const count = ( d . models || [ ] ) . length ;
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
const baseText = d . status === 'empty'
2026-06-01 10:00:15 +09:00
? 'Added — Ollama is running, no models pulled yet'
: d . online
2026-05-31 23:58:26 +09:00
? ` Added — found ${ count } model ${ count !== 1 ? 's' : '' } `
: 'Added (offline — will retry on next load)' ;
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
msg . innerHTML = ` ${ baseText } <a href="#" data-go-added-models style="margin-left:6px;text-decoration:underline;color:inherit;font-weight:600;">Added Models →</a> ` ;
2026-05-31 23:58:26 +09:00
msg . className = d . online ? 'admin-success' : 'admin-error' ;
} else { msg . textContent = d . detail || 'Failed' ; msg . className = 'admin-error' ; }
} catch ( e ) { msg . textContent = 'Request failed' ; msg . className = 'admin-error' ; }
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
localAddBtn . disabled = false ;
localAddBtn . innerHTML = addOriginalHtml ;
2026-05-31 23:58:26 +09:00
} ) ;
}
2026-06-01 10:00:15 +09:00
const ollamaBtn = el ( 'adm-epOllamaBtn' ) ;
if ( ollamaBtn ) {
ollamaBtn . addEventListener ( 'click' , async ( ) => {
const input = el ( 'adm-epLocalUrl' ) ;
if ( input ) {
input . value = await _defaultOllamaUrl ( ) ;
input . focus ( ) ;
}
const msg = _endpointMsg ( 'local' ) ;
if ( msg ) {
msg . innerHTML = '<span style="font-size:11px;opacity:0.55;">Ollama ready to test.</span>' ;
msg . className = '' ;
}
} ) ;
}
2026-05-31 23:58:26 +09:00
// Discover local models button
const discoverBtn = el ( 'adm-epDiscoverBtn' ) ;
if ( discoverBtn ) {
discoverBtn . addEventListener ( 'click' , async ( ) => {
2026-06-01 10:00:15 +09:00
const msg = _endpointMsg ( 'local' ) ;
2026-05-31 23:58:26 +09:00
discoverBtn . disabled = true ;
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
msg . className = 'adm-ep-inline-msg' ;
2026-05-31 23:58:26 +09:00
msg . innerHTML = '' ;
try {
const sp = window . spinnerModule || ( await import ( './spinner.js' ) ) . default ;
const wp = sp . createWhirlpool ( 20 ) ;
wp . element . style . cssText = 'display:inline-block;vertical-align:middle;margin:0 8px 0 0;' ;
const wrap = document . createElement ( 'div' ) ;
wrap . style . cssText = 'display:flex;align-items:center;padding:8px 0;' ;
wrap . appendChild ( wp . element ) ;
const txt = document . createElement ( 'span' ) ;
2026-06-23 23:39:56 +02:00
txt . textContent = 'Scanning ports 8000-8020, 8080, 1234, 11434, and 11435 for model servers...' ;
txt . style . cssText = 'font-size:12px;opacity:0.7;' ;
2026-05-31 23:58:26 +09:00
wrap . appendChild ( txt ) ;
msg . appendChild ( wrap ) ;
discoverBtn . _wp = wp ;
} catch ( e ) { msg . textContent = 'Scanning...' ; }
try {
const res = await fetch ( '/api/discover' ) ;
const data = await res . json ( ) ;
const items = data . items || [ ] ;
if ( ! items . length ) {
2026-06-01 21:27:04 -06:00
msg . textContent = 'No model servers found. Make sure vLLM, llama.cpp, SGLang, or Ollama is running. Docker users may need Ollama bound to a trusted reachable interface.' ;
2026-05-31 23:58:26 +09:00
msg . className = 'admin-error' ;
} else {
2026-06-01 15:22:06 +10:00
// Auto-add each discovered endpoint. Server dedupes on base_url
// and returns `existing: true` for already-registered ones.
2026-06-23 23:39:56 +02:00
// Map fingerprinted provider IDs to friendly display names.
const _PROVIDER _DISPLAY = {
llamacpp : 'llama.cpp' , lmstudio : 'LM Studio' , vllm : 'vLLM' ,
ollama : 'Ollama' ,
} ;
2026-05-31 23:58:26 +09:00
let added = 0 ;
2026-06-01 15:22:06 +10:00
let skipped = 0 ;
2026-05-31 23:58:26 +09:00
for ( const item of items ) {
const base = item . url . replace ( '/chat/completions' , '' ) . replace ( /\/$/ , '' ) ;
2026-06-23 23:39:56 +02:00
const providerDisplay = _PROVIDER _DISPLAY [ item . provider ] || null ;
2026-05-31 23:58:26 +09:00
const fd = new FormData ( ) ;
fd . append ( 'base_url' , base ) ;
2026-06-23 23:39:56 +02:00
if ( providerDisplay ) {
// Use "Provider (host:port)" so the endpoint is immediately
// identifiable in the list, e.g. "llama.cpp (localhost:8080)".
const hostPart = base . replace ( /^https?:\/\// , '' ) . split ( '/' ) [ 0 ] ;
fd . append ( 'name' , ` ${ providerDisplay } ( ${ hostPart } ) ` ) ;
}
2026-06-04 00:56:11 -03:00
fd . append ( 'endpoint_kind' , 'local' ) ;
fd . append ( 'model_refresh_mode' , 'auto' ) ;
2026-06-01 10:00:15 +09:00
fd . append ( 'skip_probe' , 'false' ) ;
2026-05-31 23:58:26 +09:00
const r = await fetch ( '/api/model-endpoints' , { method : 'POST' , body : fd } ) ;
if ( r . ok ) {
2026-06-01 15:22:06 +10:00
try {
const dd = await r . json ( ) ;
if ( dd && dd . existing ) { skipped ++ ; }
else { added ++ ; if ( dd && dd . id ) _recentlyAddedEpId = String ( dd . id ) ; }
} catch ( _ ) { added ++ ; }
2026-05-31 23:58:26 +09:00
}
}
const totalModels = items . reduce ( ( n , i ) => n + ( i . models ? i . models . length : 0 ) , 0 ) ;
2026-06-23 23:39:56 +02:00
const serverNames = items . map ( i =>
( _PROVIDER _DISPLAY [ i . provider ] || i . url . replace ( /^https?:\/\// , '' ) . split ( '/' ) [ 0 ] )
) ;
const parts = [
` Found ${ items . length } server ${ items . length !== 1 ? 's' : '' } ( ${ serverNames . join ( ', ' ) } ) with ${ totalModels } model ${ totalModels !== 1 ? 's' : '' } ` ,
] ;
2026-06-01 15:22:06 +10:00
if ( added ) parts . push ( ` added ${ added } new ` ) ;
if ( skipped ) parts . push ( ` ${ skipped } already added ` ) ;
msg . innerHTML = parts . join ( ' — ' ) ;
2026-05-31 23:58:26 +09:00
msg . className = 'admin-success' ;
loadEndpoints ( ) ;
}
} catch ( e ) {
msg . textContent = 'Scan failed: ' + e . message ;
msg . className = 'admin-error' ;
}
if ( discoverBtn . _wp ) { discoverBtn . _wp . destroy ( ) ; discoverBtn . _wp = null ; }
discoverBtn . disabled = false ;
discoverBtn . innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" style="vertical-align:-1px;margin-right:4px;"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>Scan for Servers' ;
} ) ;
}
2026-06-01 10:00:15 +09:00
document . querySelectorAll ( '.adm-quickstart-section' ) . forEach ( ( sec ) => {
const head = sec . querySelector ( '.adm-quickstart-toggle' ) ;
if ( ! head ) return ;
const key = 'odysseus.addModels.' + sec . id + '.open' ;
let open = false ;
try { open = localStorage . getItem ( key ) === '1' ; } catch { }
2026-05-31 23:58:26 +09:00
const apply = ( ) => {
sec . classList . toggle ( 'collapsed' , ! open ) ;
head . setAttribute ( 'aria-expanded' , open ? 'true' : 'false' ) ;
} ;
apply ( ) ;
const toggle = ( ) => {
open = ! open ;
try { localStorage . setItem ( key , open ? '1' : '0' ) ; } catch { }
apply ( ) ;
} ;
head . addEventListener ( 'click' , toggle ) ;
head . addEventListener ( 'keydown' , ( e ) => {
if ( e . key === 'Enter' || e . key === ' ' ) { e . preventDefault ( ) ; toggle ( ) ; }
} ) ;
} ) ;
}
/ * ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═
TOOLS TAB — MCP
═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ * /
const _GOOGLE _OAUTH _HELP = ` To get Google OAuth credentials:
1. Go to console . cloud . google . com
2. Click the project dropdown ( top left ) > New Project > name it > Create
3. APIs & Services > Library > enable the API you need ( Gmail , Calendar , Drive , etc . )
4. APIs & Services > OAuth consent screen > configure ( External , app name + email )
5. Under Audience , click Add Users > add your Google email as a test user
6. APIs & Services > Credentials > + Create Credentials > OAuth Client ID > Desktop App
7. Copy the Client ID and Client Secret into the fields above
8. After adding the server , click Authorize to sign in with Google
9. If accessing remotely : sign in , then copy the URL from the error page and paste it back ` ;
const MCP _PRESETS = [
{ name : "Gmail" , command : "npx" , args : [ "-y" , "@gongrzhe/server-gmail-autoauth-mcp" ] , env : { GOOGLE _CLIENT _ID : "" , GOOGLE _CLIENT _SECRET : "" } ,
2026-06-04 17:10:23 +00:00
oauthFile : { dir : "gmail" , filename : "gcp-oauth.keys.json" } ,
2026-05-31 23:58:26 +09:00
oauth : {
provider : "google" ,
2026-06-04 17:10:23 +00:00
keys _file : "gmail/gcp-oauth.keys.json" ,
token _file : "gmail/credentials.json" ,
2026-05-31 23:58:26 +09:00
scopes : [ "https://www.googleapis.com/auth/gmail.modify" , "https://www.googleapis.com/auth/gmail.settings.basic" ] ,
} ,
help : ` Setup:
1. Go to console . cloud . google . com > create or select a project
2. APIs & Services > Library > search "Gmail API" > Enable
3. APIs & Services > OAuth consent screen > set up ( External is fine )
4. Under Audience , add your Gmail address as a test user
5. APIs & Services > Credentials > + Create Credentials > OAuth Client ID
6. Application type : Desktop App > Create
7. Copy the Client ID and Client Secret into the fields above
8. Click Add Server , then click the Authorize button
9. Sign in with Google , copy the URL from the error page , paste it back ` },
{ name : "Email (IMAP/SMTP)" , command : "npx" , args : [ "-y" , "@codefuturist/email-mcp" , "stdio" ] , env : { MCP _EMAIL _ADDRESS : "" , MCP _EMAIL _PASSWORD : "" , MCP _EMAIL _IMAP _HOST : "" , MCP _EMAIL _SMTP _HOST : "" } ,
providerDropdown : {
label : "Provider" ,
targets : { MCP _EMAIL _IMAP _HOST : "imap" , MCP _EMAIL _SMTP _HOST : "smtp" } ,
options : [
{ name : "Migadu" , imap : "imap.migadu.com" , smtp : "smtp.migadu.com" } ,
{ name : "Fastmail" , imap : "imap.fastmail.com" , smtp : "smtp.fastmail.com" } ,
{ name : "Proton Bridge" , imap : "127.0.0.1" , smtp : "127.0.0.1" } ,
{ name : "Outlook/Hotmail" , imap : "outlook.office365.com" , smtp : "smtp.office365.com" } ,
{ name : "Yahoo" , imap : "imap.mail.yahoo.com" , smtp : "smtp.mail.yahoo.com" } ,
{ name : "iCloud" , imap : "imap.mail.me.com" , smtp : "smtp.mail.me.com" } ,
{ name : "Zoho" , imap : "imap.zoho.com" , smtp : "smtp.zoho.com" } ,
{ name : "Custom" , imap : "" , smtp : "" } ,
] ,
} ,
help : "Works with any IMAP/SMTP email provider.\n1. Pick your provider from the dropdown (or choose Custom)\n2. Enter your email address and password (or app password)\n3. Click Add Server" } ,
{ name : "CalDAV (Radicale/Nextcloud)" , command : "npx" , args : [ "-y" , "caldav-mcp" ] , env : { CALDAV _BASE _URL : "http://localhost:5232" , CALDAV _USERNAME : "" , CALDAV _PASSWORD : "" } ,
help : "Works with any CalDAV server (Radicale, Nextcloud, etc.).\n1. Enter your CalDAV server URL (e.g. http://localhost:5232)\n2. Enter your username and password\n3. Click Add Server" } ,
{ name : "Google Calendar" , command : "npx" , args : [ "-y" , "@cocal/google-calendar-mcp" ] , env : { GOOGLE _OAUTH _CREDENTIALS : "" } ,
help : ` Setup:
1. Go to console . cloud . google . com > create / select a project
2. APIs & Services > Library > enable Google Calendar API
3. APIs & Services > Credentials > + Create Credentials > OAuth Client ID
4. Application type : Desktop App > Create
5. Click "Download JSON" on the credential you just created
6. Set Google Oauth Credentials to the full path of the downloaded JSON file ` },
{ name : "Google Drive" , command : "npx" , args : [ "-y" , "@modelcontextprotocol/server-gdrive" ] , env : { } ,
help : "Google Drive uses browser-based OAuth on first run. No env vars needed — just click Add and authorize when prompted." } ,
{ name : "GitHub" , command : "npx" , args : [ "-y" , "@modelcontextprotocol/server-github" ] , env : { GITHUB _PERSONAL _ACCESS _TOKEN : "" } ,
help : "1. Go to github.com > Settings > Developer Settings > Personal Access Tokens > Fine-grained tokens\n2. Generate a new token with the repo permissions you need\n3. Paste it as Github Personal Access Token" } ,
{ name : "Slack" , command : "npx" , args : [ "-y" , "@modelcontextprotocol/server-slack" ] , env : { SLACK _BOT _TOKEN : "" , SLACK _TEAM _ID : "" } ,
help : "1. Go to api.slack.com/apps > Create New App > From Scratch\n2. Add Bot Token Scopes (channels:read, chat:write, etc.)\n3. Install to workspace, copy the Bot User OAuth Token (xoxb-...)\n4. Team ID is in your workspace URL or Slack admin settings" } ,
{ name : "Notion" , command : "npx" , args : [ "-y" , "@notionhq/notion-mcp-server" ] , env : { OPENAPI _MCP _HEADERS : "" } ,
help : "1. Go to notion.so/my-integrations\n2. Create a new integration\n3. Copy the Internal Integration Secret\n4. Share the Notion pages/databases you want accessible with the integration\n5. For Openapi Mcp Headers enter:\n {\"Authorization\": \"Bearer YOUR_SECRET\", \"Notion-Version\": \"2022-06-28\"}" } ,
{ name : "Linear" , command : "npx" , args : [ "-y" , "mcp-linear" ] , env : { LINEAR _API _KEY : "" } ,
help : "1. Go to linear.app > Settings > API\n2. Create a Personal API Key\n3. Paste it as Linear Api Key" } ,
{ name : "Brave Search" , command : "npx" , args : [ "-y" , "@modelcontextprotocol/server-brave-search" ] , env : { BRAVE _API _KEY : "" } ,
help : "1. Go to brave.com/search/api\n2. Sign up for a free plan (2000 queries/month)\n3. Copy your API key" } ,
{ name : "Browser (Playwright)" , command : "npx" , args : [ "-y" , "@playwright/mcp@latest" , "--headless" ] , env : { } ,
help : "Browser automation via Playwright. The AI can navigate pages, click, fill forms, and read content.\nRuns headless by default. Remove --headless from Args to see the browser window.\nFirst run installs Chromium automatically." } ,
{ name : "Filesystem" , command : "npx" , args : [ "-y" , "@modelcontextprotocol/server-filesystem" , "/home" ] , env : { } ,
help : "Edit the Args field to change which directory the server has access to." } ,
{ name : "Memory" , command : "npx" , args : [ "-y" , "@modelcontextprotocol/server-memory" ] , env : { } } ,
{ name : "Postgres" , command : "npx" , args : [ "-y" , "@modelcontextprotocol/server-postgres" , "postgresql://user:pass@localhost/db" ] , env : { } ,
help : "Replace the connection string in the Args field with your actual Postgres connection URL." } ,
{ name : "Todoist" , command : "npx" , args : [ "-y" , "todoist-mcp-server" ] , env : { TODOIST _API _TOKEN : "" } ,
help : "1. Go to todoist.com > Settings > Integrations > Developer\n2. Copy your API token" } ,
] ;
// ── Built-in tools management ──
const TOOL _META = {
bash : { name : 'Shell' , desc : 'Execute bash commands' , cat : 'Code' , ctx : '~200' } ,
python : { name : 'Python' , desc : 'Run Python scripts' , cat : 'Code' , ctx : '~200' } ,
read _file : { name : 'Read File' , desc : 'Read files from disk' , cat : 'Code' , ctx : '~150' } ,
write _file : { name : 'Write File' , desc : 'Write/create files' , cat : 'Code' , ctx : '~150' } ,
web _search : { name : 'Web Search' , desc : 'Search the web via SearXNG' , cat : 'Search' , ctx : '~300' } ,
search _chats : { name : 'Search Chats' , desc : 'Search conversation history' , cat : 'Search' , ctx : '~150' } ,
create _document : { name : 'Create Document' , desc : 'Create new documents' , cat : 'Documents' , ctx : '~200' } ,
update _document : { name : 'Update Document' , desc : 'Modify existing documents' , cat : 'Documents' , ctx : '~200' } ,
edit _document : { name : 'Edit Document' , desc : 'Find & replace in documents' , cat : 'Documents' , ctx : '~200' } ,
suggest _document : { name : 'Suggest Changes' , desc : 'Propose document edits' , cat : 'Documents' , ctx : '~200' } ,
manage _documents : { name : 'Manage Documents' , desc : 'List, delete, organize docs' , cat : 'Documents' , ctx : '~150' } ,
generate _image : { name : 'Generate Image' , desc : 'Create images via AI' , cat : 'Media' , ctx : '~150' } ,
manage _memory : { name : 'Memory' , desc : 'Save and recall memories' , cat : 'Knowledge' , ctx : '~200' } ,
manage _skills : { name : 'Skills' , desc : 'Learn and use procedures' , cat : 'Knowledge' , ctx : '~200' } ,
manage _rag : { name : 'RAG / Docs' , desc : 'Query indexed documents' , cat : 'Knowledge' , ctx : '~150' } ,
chat _with _model : { name : 'Chat with Model' , desc : 'Talk to another AI model' , cat : 'Multi-Agent' , ctx : '~200' } ,
pipeline : { name : 'Pipeline' , desc : 'Multi-step AI workflows' , cat : 'Multi-Agent' , ctx : '~200' } ,
ask _teacher : { name : 'Ask Teacher' , desc : 'Query a more capable model' , cat : 'Multi-Agent' , ctx : '~150' } ,
send _to _session : { name : 'Send to Session' , desc : 'Send message to another chat' , cat : 'Sessions' , ctx : '~100' } ,
create _session : { name : 'Create Session' , desc : 'Start a new chat session' , cat : 'Sessions' , ctx : '~100' } ,
list _sessions : { name : 'List Sessions' , desc : 'Browse existing sessions' , cat : 'Sessions' , ctx : '~100' } ,
manage _session : { name : 'Manage Session' , desc : 'Rename, archive, configure' , cat : 'Sessions' , ctx : '~100' } ,
list _models : { name : 'List Models' , desc : 'Show available models' , cat : 'System' , ctx : '~100' } ,
ui _control : { name : 'UI Control' , desc : 'Change theme, layout, settings' , cat : 'System' , ctx : '~150' } ,
manage _tasks : { name : 'Tasks' , desc : 'Schedule automated tasks' , cat : 'System' , ctx : '~150' } ,
api _call : { name : 'API Call' , desc : 'Make HTTP requests' , cat : 'System' , ctx : '~200' } ,
manage _endpoints : { name : 'Endpoints' , desc : 'Add/remove model endpoints' , cat : 'System' , ctx : '~100' } ,
manage _mcp : { name : 'MCP Servers' , desc : 'Manage MCP connections' , cat : 'System' , ctx : '~100' } ,
manage _webhooks : { name : 'Webhooks' , desc : 'Configure webhook events' , cat : 'System' , ctx : '~100' } ,
manage _tokens : { name : 'API Tokens' , desc : 'Manage API access tokens' , cat : 'System' , ctx : '~100' } ,
manage _settings : { name : 'Settings' , desc : 'Change app settings' , cat : 'System' , ctx : '~100' } ,
} ;
async function loadBuiltinTools ( ) {
const list = el ( 'adm-builtin-tools-list' ) ;
if ( ! list ) return ;
try {
const res = await fetch ( '/api/tools' , { credentials : 'same-origin' } ) ;
const data = await res . json ( ) ;
const tools = data . tools || [ ] ;
if ( ! tools . length ) { list . innerHTML = '<div class="admin-empty">No tools found</div>' ; return ; }
// Group by category
const groups = { } ;
for ( const t of tools ) {
const meta = TOOL _META [ t . id ] || { name : t . id , desc : '' , cat : 'Other' , ctx : '?' } ;
const cat = meta . cat ;
if ( ! groups [ cat ] ) groups [ cat ] = [ ] ;
groups [ cat ] . push ( { ... t , ... meta } ) ;
}
// Category order
const catOrder = [ 'Code' , 'Search' , 'Documents' , 'Media' , 'Knowledge' , 'Multi-Agent' , 'Sessions' , 'System' , 'Other' ] ;
let html = '' ;
for ( const cat of catOrder ) {
const items = groups [ cat ] ;
if ( ! items ) continue ;
const enabledCount = items . filter ( i => i . enabled ) . length ;
const totalCount = items . length ;
const catId = 'tool-cat-' + cat . replace ( /[^a-zA-Z]/g , '' ) ;
const allEnabled = enabledCount === totalCount ;
html += ` <div class="admin-tool-category">
< div class = "admin-tool-cat-header" data - tool - cat = "${catId}" style = "cursor:pointer;display:flex;align-items:center;justify-content:space-between;" >
< span > $ { esc ( cat ) } < / s p a n >
< span style = "display:flex;align-items:center;gap:6px;" class = "admin-tool-cat-right" >
< span class = "admin-tool-cat-count" style = "font-size:10px;opacity:0.5;" > $ { enabledCount } / $ { totalCount } < / s p a n >
< label class = "admin-switch" style = "flex-shrink:0;" >
< input type = "checkbox" data - tool - cat - toggle = "${catId}" $ { allEnabled ? 'checked' : '' } >
< span class = "admin-slider" > < / s p a n >
< / l a b e l >
< svg class = "admin-tool-cat-chevron" width = "12" height = "12" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke - width = "2.5" stroke - linecap = "round" stroke - linejoin = "round" style = "opacity:0.3;transition:transform 0.2s,opacity 0.2s;" > < polyline points = "6 9 12 15 18 9" / > < / s v g >
< / s p a n >
< / d i v >
< div class = "admin-tool-cat-body hidden" id = "${catId}" > ` ;
for ( const t of items ) {
html += `
< div class = "admin-tool-row" >
< div class = "admin-tool-info" >
< span class = "admin-tool-name" > $ { esc ( t . name ) } < / s p a n >
< span class = "admin-tool-desc" > $ { esc ( t . desc ) } < / s p a n >
< / d i v >
< span class = "admin-tool-ctx" title = "Approximate context tokens used" > $ { esc ( t . ctx ) } < / s p a n >
< label class = "admin-switch" style = "flex-shrink:0;" >
< input type = "checkbox" data - tool - id = "${esc(t.id)}" $ { t . enabled ? 'checked' : '' } >
< span class = "admin-slider" > < / s p a n >
< / l a b e l >
< / d i v > ` ;
}
html += '</div></div>' ;
}
list . innerHTML = html ;
// Prevent toggle clicks from expanding/collapsing
list . querySelectorAll ( '.admin-tool-cat-right' ) . forEach ( span => {
span . addEventListener ( 'click' , e => e . stopPropagation ( ) ) ;
} ) ;
// Wire category expand/collapse
list . querySelectorAll ( '[data-tool-cat]' ) . forEach ( header => {
header . addEventListener ( 'click' , ( ) => {
const body = el ( header . dataset . toolCat ) ;
if ( ! body ) return ;
body . classList . toggle ( 'hidden' ) ;
const chevron = header . querySelector ( '.admin-tool-cat-chevron' ) ;
const isOpen = ! body . classList . contains ( 'hidden' ) ;
if ( chevron ) {
chevron . style . transform = isOpen ? 'rotate(180deg)' : '' ;
chevron . style . opacity = isOpen ? '0.7' : '0.3' ;
}
} ) ;
} ) ;
// Helper: save disabled tools + update counters
async function _saveToolState ( ) {
const allChecks = list . querySelectorAll ( 'input[data-tool-id]' ) ;
const disabled = [ ] ;
allChecks . forEach ( c => { if ( ! c . checked ) disabled . push ( c . dataset . toolId ) ; } ) ;
await fetch ( '/api/tools' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { disabled } ) ,
credentials : 'same-origin' ,
} ) ;
}
function _updateCatCounter ( catEl ) {
if ( ! catEl ) return ;
const catChecks = catEl . querySelectorAll ( 'input[data-tool-id]' ) ;
const catEnabled = Array . from ( catChecks ) . filter ( c => c . checked ) . length ;
const counter = catEl . querySelector ( '.admin-tool-cat-count' ) ;
if ( counter ) counter . textContent = catEnabled + '/' + catChecks . length ;
const catToggle = catEl . querySelector ( 'input[data-tool-cat-toggle]' ) ;
if ( catToggle ) catToggle . checked = ( catEnabled === catChecks . length ) ;
}
// Wire individual tool toggles
list . querySelectorAll ( 'input[data-tool-id]' ) . forEach ( chk => {
chk . addEventListener ( 'change' , async ( ) => {
await _saveToolState ( ) ;
_updateCatCounter ( chk . closest ( '.admin-tool-category' ) ) ;
} ) ;
} ) ;
// Wire category-level toggle (enable/disable all in category)
list . querySelectorAll ( 'input[data-tool-cat-toggle]' ) . forEach ( chk => {
chk . addEventListener ( 'change' , async ( ) => {
const catEl = chk . closest ( '.admin-tool-category' ) ;
if ( ! catEl ) return ;
const checked = chk . checked ;
catEl . querySelectorAll ( 'input[data-tool-id]' ) . forEach ( c => { c . checked = checked ; } ) ;
await _saveToolState ( ) ;
_updateCatCounter ( catEl ) ;
} ) ;
} ) ;
} catch ( e ) {
console . error ( 'Failed to load tools:' , e ) ;
list . innerHTML = '<div class="admin-empty">Failed to load tools</div>' ;
}
}
async function loadMcpServers ( ) {
const list = el ( 'adm-mcpList' ) ;
if ( ! list ) return ; // MCP section not visible / not yet rendered
try {
const res = await fetch ( '/api/mcp/servers' , { credentials : 'same-origin' } ) ;
const servers = await res . json ( ) ;
if ( ! servers . length ) { list . innerHTML = '<div class="admin-empty">No MCP servers configured</div>' ; return ; }
list . innerHTML = servers . map ( s => {
const statusColor = s . needs _oauth ? '#e5a33a' : s . status === 'connected' ? 'var(--fg)' : s . status === 'error' ? 'var(--red)' : 'color-mix(in srgb, var(--fg) 50%, transparent)' ;
const toolInfo = s . status === 'connected' ? ` ${ s . enabled _tool _count } / ${ s . tool _count } tools enabled ` : '' ;
const statusText = s . needs _oauth ? 'Needs authorization' : s . status === 'connected' ? ` Connected ( ${ toolInfo } ) ` : s . status === 'error' ? ` Error: ${ s . error || 'unknown' } ` : 'Disconnected' ;
const hasTools = s . status === 'connected' && s . tool _count > 0 ;
return ` <div class="admin-user-row" data-adm-mcp-id=" ${ s . id } ">
< div style = "display:flex;align-items:center;justify-content:space-between;${hasTools ? 'cursor:pointer;' : ''}padding:4px 0;" data - adm - mcp - header = "${s.id}" >
< div class = "admin-user-info" style = "flex:1;flex-wrap:wrap;gap:0.3rem;" >
< span class = "admin-user-name" > $ { esc ( s . name ) } < / s p a n >
< span class = "admin-badge" style = "background:${statusColor}33;color:${statusColor}" > $ { statusText } < / s p a n >
$ { hasTools ? ` <span style="font-size:10px;opacity:0.4;">Click to manage tools</span> ` : '' }
< / d i v >
< div style = "display:flex;gap:4px;align-items:center;" >
$ { s . needs _oauth ? ` <a href="/api/mcp/oauth/authorize/ ${ s . id } " target="_blank" class="admin-btn-sm" style="background:var(--red);color:#fff;text-decoration:none;padding:3px 10px;border-radius:4px;font-size:11px;font-weight:600;">Authorize</a> ` : '' }
< button class = "admin-btn-sm" data - adm - mcp - reconnect = "${s.id}" > Reconnect < / b u t t o n >
< button class = "admin-btn-delete" style = "border-color:${s.is_enabled ? 'color-mix(in srgb, var(--red) 30%, transparent)' : 'color-mix(in srgb, var(--fg) 30%, transparent)'};color:${s.is_enabled ? 'var(--red)' : 'var(--fg)'};" data - adm - mcp - toggle = "${s.id}" data - adm - mcp - enable = "${!s.is_enabled}" > $ { s . is _enabled ? 'Disable' : 'Enable' } < / b u t t o n >
< button class = "admin-btn-delete" data - adm - mcp - delete = "${s.id}" > Delete < / b u t t o n >
$ { hasTools ? '<svg class="admin-user-chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="opacity:0.3;transition:transform 0.2s,opacity 0.2s;"><polyline points="6 9 12 15 18 9"/></svg>' : '' }
< / d i v >
< / d i v >
$ { hasTools ? ` <div class="mcp-tools-panel hidden" data-adm-mcp-tools-panel=" ${ s . id } "></div> ` : '' }
< / d i v > ` ;
} ) . join ( '' ) ;
list . querySelectorAll ( '[data-adm-mcp-reconnect]' ) . forEach ( btn => {
btn . addEventListener ( 'click' , async ( ) => {
const msg = el ( 'adm-mcpMsg' ) ; msg . textContent = 'Reconnecting...' ; msg . className = '' ;
try {
const res = await fetch ( ` /api/mcp/servers/ ${ btn . dataset . admMcpReconnect } /reconnect ` , { method : 'POST' , credentials : 'same-origin' } ) ;
const data = await res . json ( ) ;
msg . textContent = data . connected ? ` Reconnected ( ${ data . tool _count } tools) ` : ` Failed: ${ data . error || 'unknown' } ` ;
msg . className = data . connected ? 'admin-success' : 'admin-error' ;
loadMcpServers ( ) ;
} catch ( e ) { msg . textContent = 'Failed: ' + e . message ; msg . className = 'admin-error' ; }
} ) ;
} ) ;
list . querySelectorAll ( '[data-adm-mcp-toggle]' ) . forEach ( btn => {
btn . addEventListener ( 'click' , async ( ) => {
const fd = new FormData ( ) ; fd . append ( 'is_enabled' , btn . dataset . admMcpEnable ) ;
await fetch ( ` /api/mcp/servers/ ${ btn . dataset . admMcpToggle } ` , { method : 'PATCH' , body : fd , credentials : 'same-origin' } ) ;
loadMcpServers ( ) ;
} ) ;
} ) ;
list . querySelectorAll ( '[data-adm-mcp-delete]' ) . forEach ( btn => {
btn . addEventListener ( 'click' , async ( ) => {
if ( ! await uiModule . styledConfirm ( 'Delete this MCP server?' , { confirmText : 'Delete' , danger : true } ) ) return ;
await fetch ( ` /api/mcp/servers/ ${ btn . dataset . admMcpDelete } ` , { method : 'DELETE' , credentials : 'same-origin' } ) ;
loadMcpServers ( ) ;
} ) ;
} ) ;
// Tools expand/collapse (click anywhere on card)
list . querySelectorAll ( '[data-adm-mcp-id]' ) . forEach ( row => {
const header = row . querySelector ( '[data-adm-mcp-header]' ) ;
if ( ! header ) return ;
let _toolsLoaded = false ;
row . style . cursor = 'pointer' ;
row . addEventListener ( 'click' , async ( e ) => {
if ( e . target . closest ( '.admin-btn-sm, .admin-btn-delete, a, .mcp-tools-list, .mcp-tools-header' ) ) return ;
const sid = header . dataset . admMcpHeader ;
const panel = row . querySelector ( ` [data-adm-mcp-tools-panel=" ${ sid } "] ` ) ;
if ( ! panel ) return ;
panel . classList . toggle ( 'hidden' ) ;
const chevron = row . querySelector ( '.admin-user-chevron' ) ;
const isOpen = ! panel . classList . contains ( 'hidden' ) ;
if ( chevron ) {
chevron . style . transform = isOpen ? 'rotate(180deg)' : '' ;
chevron . style . opacity = isOpen ? '0.7' : '0.3' ;
}
if ( ! _toolsLoaded && isOpen ) {
_toolsLoaded = true ;
panel . innerHTML = '<span style="opacity:0.5;font-size:11px;">Loading tools...</span>' ;
try {
const res = await fetch ( ` /api/mcp/servers/ ${ sid } /tools ` , { credentials : 'same-origin' } ) ;
const tools = await res . json ( ) ;
if ( ! tools . length ) { panel . innerHTML = '<span style="opacity:0.5;font-size:11px;">No tools</span>' ; return ; }
const disabled = new Set ( tools . filter ( t => t . is _disabled ) . map ( t => t . name ) ) ;
panel . innerHTML = ` <div class="mcp-tools-header">
< span > Tools < / s p a n >
< span style = "display:flex;gap:8px;align-items:center;" >
< span class = "mcp-tools-count" > $ { tools . length - disabled . size } / $ { tools . length } enabled < / s p a n >
< a href = "#" data - mcp - select - all = "${sid}" > All < / a >
< a href = "#" data - mcp - select - none = "${sid}" > None < / a >
< / s p a n >
< / d i v > < d i v c l a s s = " m c p - t o o l s - l i s t " > ` + t o o l s . m a p ( t = >
` <label title=" ${ esc ( t . description ) } ">
< input type = "checkbox" data - mcp - tool - name = "${esc(t.name)}" $ { ! t . is _disabled ? 'checked' : '' } >
< span > < strong > $ { esc ( t . name ) } < / s t r o n g > < s p a n s t y l e = " o p a c i t y : 0 . 5 ; " > — $ { e s c ( ( t . d e s c r i p t i o n | | ' ' ) . s l i c e ( 0 , 8 0 ) ) } < / s p a n > < / s p a n >
< / l a b e l > `
) . join ( '' ) + '</div>' ;
panel . querySelector ( ` [data-mcp-select-all=" ${ sid } "] ` ) ? . addEventListener ( 'click' , ( e ) => {
e . preventDefault ( ) ;
panel . querySelectorAll ( 'input[type=checkbox]' ) . forEach ( cb => cb . checked = true ) ;
_saveMcpToolState ( sid , panel ) ;
} ) ;
panel . querySelector ( ` [data-mcp-select-none=" ${ sid } "] ` ) ? . addEventListener ( 'click' , ( e ) => {
e . preventDefault ( ) ;
panel . querySelectorAll ( 'input[type=checkbox]' ) . forEach ( cb => cb . checked = false ) ;
_saveMcpToolState ( sid , panel ) ;
} ) ;
panel . querySelectorAll ( 'input[type=checkbox]' ) . forEach ( cb => {
cb . addEventListener ( 'change' , ( ) => _saveMcpToolState ( sid , panel ) ) ;
} ) ;
} catch ( e ) { panel . innerHTML = '<span class="admin-error" style="font-size:11px;">Failed to load tools</span>' ; }
}
} ) ;
} ) ;
} catch ( e ) { if ( list ) list . innerHTML = '<div class="admin-error">Failed to load MCP servers</div>' ; }
}
async function _saveMcpToolState ( serverId , panel ) {
const disabled = [ ] ;
panel . querySelectorAll ( 'input[type=checkbox]' ) . forEach ( cb => {
if ( ! cb . checked ) disabled . push ( cb . dataset . mcpToolName ) ;
} ) ;
const total = panel . querySelectorAll ( 'input[type=checkbox]' ) . length ;
try {
await fetch ( ` /api/mcp/servers/ ${ serverId } /tools ` , {
method : 'PATCH' ,
headers : { 'Content-Type' : 'application/json' } ,
credentials : 'same-origin' ,
body : JSON . stringify ( { disabled } ) ,
} ) ;
// Update the count label in the panel
const countLabel = panel . querySelector ( '.mcp-tools-count' ) ;
if ( countLabel ) countLabel . textContent = ` ${ total - disabled . length } / ${ total } enabled ` ;
// Update badge in the server row
const row = panel . closest ( '[data-adm-mcp-id]' ) ;
if ( row ) {
const badge = row . querySelector ( '.admin-badge' ) ;
if ( badge ) badge . textContent = ` Connected ( ${ total - disabled . length } / ${ total } tools enabled) ` ;
}
} catch ( e ) { /* silent */ }
}
function initMcpForm ( ) {
const cmdEl = el ( 'adm-mcpCommand' ) ;
if ( ! cmdEl ) return ; // MCP form not present in this build — nothing to wire
const transportSel = el ( 'adm-mcpTransport' ) ;
const sseRow = el ( 'adm-mcpSseRow' ) ;
const envRow = el ( 'adm-mcpEnvRow' ) ;
const envFieldsWrap = el ( 'adm-mcpEnvFields' ) ;
const helpBox = el ( 'adm-mcpHelp' ) ;
const cmdRow = cmdEl . parentElement ;
let _activeHelp = null ;
let _envKeys = [ ] ; // track which env keys have dedicated fields
let _activeOauthFile = null ; // preset oauthFile config (for Google servers)
let _activeOauth = null ; // preset OAuth flow config (provider, scopes, etc.)
function _clearEnvFields ( ) {
envFieldsWrap . innerHTML = '' ;
_envKeys = [ ] ;
envRow . style . display = 'none' ;
el ( 'adm-mcpEnv' ) . value = '' ;
_activeOauth = null ;
}
function _buildEnvFields ( envObj , help , preset ) {
_clearEnvFields ( ) ;
const keys = Object . keys ( envObj ) ;
if ( ! keys . length ) return ;
_envKeys = keys ;
// Provider dropdown (e.g. for Email IMAP/SMTP)
if ( preset ? . providerDropdown ) {
const pd = preset . providerDropdown ;
const row = document . createElement ( 'div' ) ;
row . className = 'admin-model-form-row' ;
row . style . cssText = 'gap:6px;align-items:center;' ;
const label = document . createElement ( 'span' ) ;
label . style . cssText = 'font-size:11px;opacity:0.55;min-width:0;white-space:nowrap;' ;
label . textContent = pd . label || 'Provider' ;
const select = document . createElement ( 'select' ) ;
select . style . cssText = 'flex:1;padding:6px 8px;border-radius:6px;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-primary);font-size:12px;' ;
pd . options . forEach ( ( opt , i ) => {
const o = document . createElement ( 'option' ) ;
o . value = i ;
o . textContent = opt . name ;
select . appendChild ( o ) ;
} ) ;
select . addEventListener ( 'change' , ( ) => {
const opt = pd . options [ parseInt ( select . value ) ] ;
for ( const [ envKey , field ] of Object . entries ( pd . targets ) ) {
const inp = envFieldsWrap . querySelector ( ` .mcp-env-input[data-env-key=" ${ envKey } "] ` ) ;
if ( inp ) inp . value = opt [ field ] || '' ;
}
} ) ;
row . appendChild ( label ) ;
row . appendChild ( select ) ;
envFieldsWrap . appendChild ( row ) ;
// Auto-fill with first provider after inputs are created
setTimeout ( ( ) => {
const first = pd . options [ 0 ] ;
for ( const [ envKey , field ] of Object . entries ( pd . targets ) ) {
const inp = envFieldsWrap . querySelector ( ` .mcp-env-input[data-env-key=" ${ envKey } "] ` ) ;
if ( inp && ! inp . value ) inp . value = first [ field ] || '' ;
}
} , 0 ) ;
}
for ( const key of keys ) {
const row = document . createElement ( 'div' ) ;
row . className = 'admin-model-form-row' ;
row . style . cssText = 'gap:6px;align-items:center;' ;
const label = document . createElement ( 'span' ) ;
label . style . cssText = 'font-size:11px;opacity:0.55;min-width:0;white-space:nowrap;' ;
label . textContent = key . replace ( /_/g , ' ' ) . replace ( /\b\w/g , c => c . toUpperCase ( ) ) ;
const input = document . createElement ( 'input' ) ;
input . type = key . toLowerCase ( ) . includes ( 'secret' ) || key . toLowerCase ( ) . includes ( 'token' ) || key . toLowerCase ( ) . includes ( 'key' ) || key . toLowerCase ( ) . includes ( 'password' ) ? 'password' : 'text' ;
input . placeholder = key ;
input . dataset . envKey = key ;
input . className = 'mcp-env-input' ;
input . style . cssText = 'flex:1;' ;
if ( envObj [ key ] ) input . value = envObj [ key ] ;
row . appendChild ( label ) ;
row . appendChild ( input ) ;
envFieldsWrap . appendChild ( row ) ;
}
// Help toggle link
if ( help ) {
_activeHelp = help ;
const helpLink = document . createElement ( 'a' ) ;
helpLink . textContent = 'How do I get these?' ;
helpLink . href = '#' ;
helpLink . style . cssText = 'font-size:10.5px;opacity:0.5;margin-top:2px;display:inline-block;' ;
helpLink . addEventListener ( 'click' , ( e ) => {
e . preventDefault ( ) ;
helpBox . style . display = helpBox . style . display === 'none' ? '' : 'none' ;
} ) ;
envFieldsWrap . appendChild ( helpLink ) ;
helpBox . textContent = help ;
helpBox . style . display = 'none' ;
} else {
_activeHelp = null ;
helpBox . style . display = 'none' ;
}
}
// Collect env from either dedicated fields or raw JSON fallback
function _collectEnv ( ) {
if ( _envKeys . length ) {
const obj = { } ;
envFieldsWrap . querySelectorAll ( '.mcp-env-input' ) . forEach ( inp => {
if ( inp . value . trim ( ) ) obj [ inp . dataset . envKey ] = inp . value . trim ( ) ;
} ) ;
return JSON . stringify ( obj ) ;
}
return el ( 'adm-mcpEnv' ) . value . trim ( ) || '{}' ;
}
transportSel . addEventListener ( 'change' , ( ) => {
const isSse = transportSel . value === 'sse' ;
sseRow . style . display = isSse ? '' : 'none' ;
cmdRow . style . display = isSse ? 'none' : '' ;
if ( isSse ) { _clearEnvFields ( ) ; helpBox . style . display = 'none' ; }
} ) ;
// Preset catalog
const presetSel = el ( 'adm-mcpPreset' ) ;
if ( presetSel ) {
MCP _PRESETS . forEach ( ( p , i ) => {
const opt = document . createElement ( 'option' ) ;
opt . value = i ;
opt . textContent = p . name + ( Object . keys ( p . env ) . length ? ' (requires keys)' : '' ) ;
presetSel . appendChild ( opt ) ;
} ) ;
presetSel . addEventListener ( 'change' , ( ) => {
if ( presetSel . value === '' ) return ;
const p = MCP _PRESETS [ parseInt ( presetSel . value ) ] ;
el ( 'adm-mcpName' ) . value = p . name . toLowerCase ( ) . replace ( /\s+/g , '-' ) ;
transportSel . value = 'stdio' ;
el ( 'adm-mcpCommand' ) . value = p . command ;
el ( 'adm-mcpArgs' ) . value = JSON . stringify ( p . args ) ;
sseRow . style . display = 'none' ;
cmdRow . style . display = '' ;
_buildEnvFields ( p . env , p . help || null , p ) ;
_activeOauthFile = p . oauthFile || null ;
_activeOauth = p . oauth || null ;
presetSel . value = '' ;
// Focus first env field if keys are needed
const firstInput = envFieldsWrap . querySelector ( '.mcp-env-input' ) ;
if ( firstInput ) firstInput . focus ( ) ;
else el ( 'adm-mcpAddBtn' ) . focus ( ) ;
} ) ;
}
el ( 'adm-mcpAddBtn' ) . addEventListener ( 'click' , async ( ) => {
const name = el ( 'adm-mcpName' ) . value . trim ( ) ;
const transport = transportSel . value ;
const command = el ( 'adm-mcpCommand' ) . value . trim ( ) ;
const args = el ( 'adm-mcpArgs' ) . value . trim ( ) || '[]' ;
const env = _collectEnv ( ) ;
const url = el ( 'adm-mcpUrl' ) . value . trim ( ) ;
const msg = el ( 'adm-mcpMsg' ) ;
if ( ! name ) { msg . textContent = 'Name is required' ; msg . className = 'admin-error' ; return ; }
if ( transport === 'stdio' && ! command ) { msg . textContent = 'Command is required for stdio' ; msg . className = 'admin-error' ; return ; }
if ( transport === 'sse' && ! url ) { msg . textContent = 'URL is required for SSE' ; msg . className = 'admin-error' ; return ; }
try { JSON . parse ( env ) ; } catch { msg . textContent = 'Env must be valid JSON' ; msg . className = 'admin-error' ; return ; }
const fd = new FormData ( ) ;
fd . append ( 'name' , name ) ; fd . append ( 'transport' , transport ) ; fd . append ( 'command' , command ) ; fd . append ( 'args' , args ) ; fd . append ( 'env' , env ) ; fd . append ( 'url' , url ) ;
// If preset has oauthFile config, send credentials for file generation
if ( _activeOauthFile ) {
const envObj = JSON . parse ( env ) ;
fd . append ( 'oauth_file' , JSON . stringify ( {
dir : _activeOauthFile . dir ,
filename : _activeOauthFile . filename ,
client _id : envObj . GOOGLE _CLIENT _ID || '' ,
client _secret : envObj . GOOGLE _CLIENT _SECRET || '' ,
} ) ) ;
}
// If preset has OAuth flow config, send it so the server can handle authorization
if ( _activeOauth ) {
fd . append ( 'oauth_config' , JSON . stringify ( _activeOauth ) ) ;
}
msg . textContent = 'Adding...' ; msg . className = '' ;
try {
const res = await fetch ( '/api/mcp/servers' , { method : 'POST' , body : fd , credentials : 'same-origin' } ) ;
const data = await res . json ( ) ;
if ( data . needs _oauth ) {
msg . innerHTML = ` Added ${ esc ( name ) } — <a href="/api/mcp/oauth/authorize/ ${ data . id } " target="_blank" style="color:var(--red);font-weight:600;">Authorize with Google</a> to connect ` ;
msg . className = 'admin-success' ;
} else if ( data . connected ) {
msg . textContent = ` Added ${ name } ( ${ data . tool _count } tools discovered) ` ; msg . className = 'admin-success' ;
} else { msg . textContent = ` Added but connection failed: ${ data . error || 'unknown' } ` ; msg . className = 'admin-error' ; }
el ( 'adm-mcpName' ) . value = '' ; el ( 'adm-mcpCommand' ) . value = '' ; el ( 'adm-mcpArgs' ) . value = '' ; el ( 'adm-mcpUrl' ) . value = '' ;
_clearEnvFields ( ) ; helpBox . style . display = 'none' ; _activeHelp = null ; _activeOauthFile = null ; _activeOauth = null ;
loadMcpServers ( ) ;
} catch ( e ) { msg . textContent = 'Failed: ' + e . message ; msg . className = 'admin-error' ; }
} ) ;
}
/ * ─ ─ E m b e d d i n g m o d e l ─ ─
No settings UI : the embedding model ( RAG , semantic memory , tool selection )
is fixed infrastructure that ships with the app , and swapping it would
invalidate every existing vector . Configure via the FASTEMBED _MODEL /
EMBEDDING _URL env vars if you really need to override it . * /
/* ── RAG ── */
async function loadRag ( ) {
try {
const res = await fetch ( '/api/personal' ) ;
const data = await res . json ( ) ;
const dirList = el ( 'adm-ragDirList' ) ;
const dirs = data . directories || [ ] ;
if ( dirs . length === 0 ) { dirList . innerHTML = '<div class="admin-empty">No directories indexed</div>' ; }
else {
dirList . innerHTML = dirs . map ( d => ` <div class="admin-rag-item"><span class="admin-rag-item-name" title=" ${ esc ( d ) } "> ${ esc ( d ) } </span><button class="admin-btn-delete" data-adm-rag-dir=" ${ esc ( d ) } ">Remove</button></div> ` ) . join ( '' ) ;
dirList . querySelectorAll ( '[data-adm-rag-dir]' ) . forEach ( btn => {
btn . addEventListener ( 'click' , async ( ) => {
if ( ! await uiModule . styledConfirm ( ` Remove directory " ${ btn . dataset . admRagDir } " from RAG? ` , { confirmText : 'Remove' , danger : true } ) ) return ;
btn . disabled = true ; btn . textContent = '...' ;
try {
const res = await fetch ( '/api/personal/remove_directory?directory=' + encodeURIComponent ( btn . dataset . admRagDir ) , { method : 'DELETE' } ) ;
if ( res . ok ) { ragMsg ( 'Directory removed' ) ; loadRag ( ) ; }
else { const e = await res . json ( ) ; ragMsg ( e . detail || 'Failed' , true ) ; }
} catch ( e ) { ragMsg ( 'Error: ' + e . message , true ) ; }
} ) ;
} ) ;
}
const fileList = el ( 'adm-ragFileList' ) ;
const files = data . files || [ ] ;
if ( files . length === 0 ) { fileList . innerHTML = '<div class="admin-empty">No files indexed</div>' ; }
else {
fileList . innerHTML = files . map ( f => {
const size = f . size ? ( f . size > 1024 ? ( f . size / 1024 ) . toFixed ( 1 ) + ' KB' : f . size + ' B' ) : '' ;
return ` <div class="admin-rag-item"><span class="admin-rag-item-name" title=" ${ esc ( f . path || f . name ) } "> ${ esc ( f . name ) } </span><span class="admin-rag-item-meta"> ${ size } </span><button class="admin-btn-delete" data-adm-rag-file=" ${ esc ( f . path || f . name ) } ">Delete</button></div> ` ;
} ) . join ( '' ) ;
fileList . querySelectorAll ( '[data-adm-rag-file]' ) . forEach ( btn => {
btn . addEventListener ( 'click' , async ( ) => {
if ( ! await uiModule . styledConfirm ( ` Delete " ${ btn . dataset . admRagFile } " from RAG? ` , { confirmText : 'Delete' , danger : true } ) ) return ;
btn . disabled = true ; btn . textContent = '...' ;
try {
const res = await fetch ( '/api/personal/file?filepath=' + encodeURIComponent ( btn . dataset . admRagFile ) , { method : 'DELETE' } ) ;
if ( res . ok ) { ragMsg ( 'File removed' ) ; loadRag ( ) ; }
else { const e = await res . json ( ) ; ragMsg ( e . detail || 'Failed' , true ) ; }
} catch ( e ) { ragMsg ( 'Error: ' + e . message , true ) ; }
} ) ;
} ) ;
}
} catch ( e ) {
el ( 'adm-ragDirList' ) . innerHTML = '<div class="admin-error">Failed to load</div>' ;
el ( 'adm-ragFileList' ) . innerHTML = '' ;
}
}
let _ragMsgTimer = null ;
function ragMsg ( text , isError , persist ) {
const s = el ( 'adm-ragStatus' ) ;
s . textContent = text ; s . style . color = isError ? 'var(--red)' : 'var(--fg)' ;
if ( _ragMsgTimer ) { clearTimeout ( _ragMsgTimer ) ; _ragMsgTimer = null ; }
if ( text && ! persist ) _ragMsgTimer = setTimeout ( ( ) => { s . textContent = '' ; } , 5000 ) ;
}
async function ragUpload ( files ) {
if ( ! files || files . length === 0 ) return ;
ragMsg ( 'Uploading ' + files . length + ' file(s)...' , false , true ) ;
const fd = new FormData ( ) ;
for ( const f of files ) fd . append ( 'files' , f ) ;
try {
const res = await fetch ( '/api/personal/upload' , { method : 'POST' , body : fd } ) ;
const data = await res . json ( ) ;
if ( data . success ) { ragMsg ( ` Uploaded ${ data . uploaded . length } file(s), ${ data . indexed _count } chunks indexed ` ) ; loadRag ( ) ; }
else ragMsg ( data . detail || 'Upload failed' , true ) ;
} catch ( e ) { ragMsg ( 'Upload error: ' + e . message , true ) ; }
}
function initRag ( ) {
const dropZone = el ( 'adm-ragDropZone' ) ;
const fileInput = el ( 'adm-ragFileInput' ) ;
dropZone . addEventListener ( 'click' , ( ) => fileInput . click ( ) ) ;
fileInput . addEventListener ( 'change' , ( ) => ragUpload ( fileInput . files ) ) ;
dropZone . addEventListener ( 'dragover' , e => { e . preventDefault ( ) ; dropZone . classList . add ( 'dragover' ) ; } ) ;
dropZone . addEventListener ( 'dragleave' , ( ) => dropZone . classList . remove ( 'dragover' ) ) ;
dropZone . addEventListener ( 'drop' , e => { e . preventDefault ( ) ; dropZone . classList . remove ( 'dragover' ) ; ragUpload ( e . dataTransfer . files ) ; } ) ;
el ( 'adm-ragAddDirBtn' ) . addEventListener ( 'click' , async ( ) => {
const dir = el ( 'adm-ragDirInput' ) . value . trim ( ) ;
if ( ! dir ) return ;
const btn = el ( 'adm-ragAddDirBtn' ) ;
btn . disabled = true ; btn . textContent = 'Indexing...' ;
try {
const res = await fetch ( '/api/personal/add_directory' , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON . stringify ( { directory : dir } ) } ) ;
const data = await res . json ( ) ;
if ( data . success ) { ragMsg ( ` Indexed ${ data . indexed _count } chunks from directory ` ) ; el ( 'adm-ragDirInput' ) . value = '' ; loadRag ( ) ; }
else ragMsg ( data . detail || data . message || 'Failed' , true ) ;
} catch ( e ) { ragMsg ( 'Error: ' + e . message , true ) ; }
btn . disabled = false ; btn . textContent = 'Add Directory' ;
} ) ;
el ( 'adm-ragReloadBtn' ) . addEventListener ( 'click' , async ( ) => {
const btn = el ( 'adm-ragReloadBtn' ) ;
btn . disabled = true ; btn . textContent = 'Reloading...' ;
try {
const res = await fetch ( '/api/personal/reload' , { method : 'POST' } ) ;
const data = await res . json ( ) ;
ragMsg ( ` Index reloaded: ${ data . count } documents ` ) ;
loadRag ( ) ;
} catch ( e ) { ragMsg ( 'Reload failed: ' + e . message , true ) ; }
btn . disabled = false ; btn . textContent = 'Reload Index' ;
} ) ;
}
/ * ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═
SYSTEM TAB — Tokens
═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ * /
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
// Catalog mirrors the one in settings.js integration form. Keep keys in
// sync with the backend scope allowlist.
const _TOKEN _SCOPES = [
{ key : 'todos:read' , label : 'Todos read' , detail : 'Read notes and checklists' } ,
{ key : 'todos:write' , label : 'Todos write' , detail : 'Create, update, delete, and toggle todo items' } ,
{ key : 'documents:read' , label : 'Documents read' , detail : 'Read documents when a document API is enabled' } ,
{ key : 'documents:write' , label : 'Documents write' , detail : 'Create and update draft documents' } ,
{ key : 'email:read' , label : 'Email read' , detail : 'Read email when an email API is enabled' } ,
{ key : 'email:draft' , label : 'Email draft' , detail : 'Create email reply drafts without sending' } ,
{ key : 'email:send' , label : 'Email send' , detail : 'Send email directly' } ,
{ key : 'calendar:read' , label : 'Calendar read' , detail : 'Read calendar events when enabled' } ,
{ key : 'calendar:write' , label : 'Calendar write' , detail : 'Create and update calendar events' } ,
{ key : 'memory:read' , label : 'Memory read' , detail : 'Read memory when enabled' } ,
{ key : 'memory:write' , label : 'Memory write' , detail : 'Write memory when enabled' } ,
{ key : 'cookbook:read' , label : 'Cookbook read' , detail : 'List cookbook tasks + tail their tmux output' } ,
{ key : 'cookbook:launch' , label : 'Cookbook launch' , detail : 'Launch and stop cookbook serve tasks' } ,
] ;
function _renderTokenScopeRows ( t ) {
const have = new Set ( t . scopes || [ ] ) ;
return _TOKEN _SCOPES . map ( s => {
const action = ( s . key . split ( ':' ) [ 1 ] || '' ) . toLowerCase ( ) ;
const pill = action === 'read'
? 'background:rgba(150,150,150,0.18);color:var(--fg-muted,#888);'
: 'background:color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);color:var(--accent, var(--red));' ;
const tool = s . label . replace ( /\s+(read|write|draft|send|launch)$/i , '' ) ;
return `
< label style = "display:flex;align-items:center;gap:8px;min-height:28px;padding:1px 0;" >
< span class = "settings-label" style = "width:90px;flex-shrink:0;padding:0;font-size:12px;" > $ { esc ( tool ) } < / s p a n >
< span style = "font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;padding:1px 7px;border-radius:999px;flex-shrink:0;min-width:44px;text-align:center;box-sizing:border-box;${pill}" > $ { esc ( action ) } < / s p a n >
< span style = "font-size:11px;line-height:1.35;opacity:0.62;flex:1;min-width:0;" > $ { esc ( s . detail ) } < / s p a n >
< label class = "admin-switch" style = "margin-left:auto;flex-shrink:0;" > < input type = "checkbox" class = "adm-tok-scope" data - token - id = "${esc(t.id)}" data - scope = "${esc(s.key)}" $ { have . has ( s . key ) ? 'checked' : '' } > < span class = "admin-slider" > < / s p a n > < / l a b e l >
< / l a b e l > ` ;
} ) . join ( '' ) ;
}
2026-05-31 23:58:26 +09:00
async function loadTokens ( ) {
const list = el ( 'adm-tokenList' ) ;
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
if ( ! list ) return ;
2026-05-31 23:58:26 +09:00
try {
const res = await fetch ( '/api/tokens' , { credentials : 'same-origin' } ) ;
const tokens = await res . json ( ) ;
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
if ( ! tokens . length ) { list . innerHTML = '<div class="admin-empty" style="color:var(--accent, var(--red));opacity:0.7;font-size:10px;">No API tokens</div>' ; return ; }
2026-05-31 23:58:26 +09:00
list . innerHTML = tokens . map ( t => `
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
< div class = "admin-user-row" data - adm - tok - row = "${esc(t.id)}" style = "display:block;" >
< div style = "display:flex;align-items:center;gap:8px;flex-wrap:wrap;" >
< div class = "admin-user-info" style = "flex:1;min-width:0;flex-wrap:wrap;gap:0.3rem;" >
< input type = "text" class = "adm-tok-rename" data - token - id = "${esc(t.id)}" value = "${esc(t.name || '')}" placeholder = "Token name" style = "font-size:13px;font-weight:600;padding:3px 6px;background:transparent;border:1px solid transparent;border-radius:4px;min-width:160px;" title = "Click to rename" >
< span class = "admin-badge" > $ { esc ( t . token _prefix ) } ... < / s p a n >
$ { t . owner ? ` <span style="font-size:0.75rem;opacity:0.5;">Owner: ${ esc ( t . owner ) } </span> ` : '' }
$ { t . last _used _at ? ` <span style="font-size:0.75rem;opacity:0.5;">Last used: ${ new Date ( t . last _used _at ) . toLocaleDateString ( ) } </span> ` : '<span style="font-size:0.75rem;opacity:0.4;">Never used</span>' }
< / d i v >
< button class = "admin-btn-sm" data - adm - tok - toggle = "${esc(t.id)}" style = "opacity:0.75;" > Permissions < / b u t t o n >
< button class = "admin-btn-delete" data - adm - del - token = "${esc(t.id)}" > Revoke < / b u t t o n >
< / d i v >
< div data - adm - tok - perm = "${esc(t.id)}" style = "display:none;margin-top:8px;padding:8px 4px 0;border-top:1px solid var(--border);" >
$ { _renderTokenScopeRows ( t ) }
< div class = "adm-tok-scope-msg" data - token - id = "${esc(t.id)}" style = "font-size:11px;min-height:14px;margin-top:4px;" > < / d i v >
2026-05-31 23:58:26 +09:00
< / d i v >
< / d i v > ` ) . j o i n ( ' ' ) ;
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
// Revoke
2026-05-31 23:58:26 +09:00
list . querySelectorAll ( '[data-adm-del-token]' ) . forEach ( btn => {
btn . addEventListener ( 'click' , async ( ) => {
if ( ! await uiModule . styledConfirm ( 'Revoke this API token? External integrations using it will stop working.' , { confirmText : 'Revoke' , danger : true } ) ) return ;
await fetch ( ` /api/tokens/ ${ btn . dataset . admDelToken } ` , { method : 'DELETE' , credentials : 'same-origin' } ) ;
loadTokens ( ) ;
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
// Codex / Claude integration cards on the Integrations panel are
// backed by these tokens — let them re-render so the deleted token
// disappears there too.
try { window . dispatchEvent ( new CustomEvent ( 'odysseus-integrations-changed' ) ) ; } catch ( _ ) { }
} ) ;
} ) ;
// Toggle permissions panel
list . querySelectorAll ( '[data-adm-tok-toggle]' ) . forEach ( btn => {
btn . addEventListener ( 'click' , ( ) => {
const panel = list . querySelector ( ` [data-adm-tok-perm=" ${ btn . dataset . admTokToggle } "] ` ) ;
if ( ! panel ) return ;
panel . style . display = panel . style . display === 'none' ? '' : 'none' ;
} ) ;
} ) ;
// Rename
list . querySelectorAll ( '.adm-tok-rename' ) . forEach ( input => {
const original = input . value ;
const commit = async ( ) => {
const name = ( input . value || '' ) . trim ( ) ;
if ( ! name || name === original ) return ;
try {
const r = await fetch ( ` /api/tokens/ ${ input . dataset . tokenId } ` , {
method : 'PATCH' , credentials : 'same-origin' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { name } ) ,
} ) ;
if ( ! r . ok ) throw new Error ( 'Save failed' ) ;
loadTokens ( ) ;
} catch ( _ ) { input . value = original ; }
} ;
input . addEventListener ( 'blur' , commit ) ;
input . addEventListener ( 'keydown' , e => { if ( e . key === 'Enter' ) { e . preventDefault ( ) ; input . blur ( ) ; } } ) ;
} ) ;
// Scope toggle change → PATCH the whole scopes array for this token.
list . querySelectorAll ( '.adm-tok-scope' ) . forEach ( cb => {
cb . addEventListener ( 'change' , async ( ) => {
const tokenId = cb . dataset . tokenId ;
const panel = list . querySelector ( ` [data-adm-tok-perm=" ${ tokenId } "] ` ) ;
const msg = list . querySelector ( ` .adm-tok-scope-msg[data-token-id=" ${ tokenId } "] ` ) ;
const scopes = Array . from ( panel . querySelectorAll ( '.adm-tok-scope:checked' ) ) . map ( input => input . dataset . scope ) ;
try {
const r = await fetch ( ` /api/tokens/ ${ tokenId } ` , {
method : 'PATCH' , credentials : 'same-origin' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { scopes } ) ,
} ) ;
const d = await r . json ( ) . catch ( ( ) => ( { } ) ) ;
if ( ! r . ok ) throw new Error ( d . detail || 'Failed' ) ;
if ( msg ) { msg . textContent = 'Saved' ; msg . style . color = 'var(--green, #50fa7b)' ; setTimeout ( ( ) => { msg . textContent = '' ; } , 1200 ) ; }
} catch ( err ) {
cb . checked = ! cb . checked ;
if ( msg ) { msg . textContent = ( err && err . message ) || 'Failed' ; msg . style . color = 'var(--red)' ; }
}
2026-05-31 23:58:26 +09:00
} ) ;
} ) ;
} catch ( e ) { list . innerHTML = '<div class="admin-error">Failed to load tokens</div>' ; }
}
function initTokenForm ( ) {
Cookbook UI: Ollama browser, advanced serve fold, API tokens form, diagnosis toolbar, polish
Surface a lot of accumulated cookbook + UI work as a single non-agent
commit so the agent rework lands cleanly.
Highlights:
- Ollama as a first-class backend in the Cookbook:
* Download input accepts ollama-style names (name:tag) → backend=ollama
* /api/cookbook/ollama/library (cached scrape of ollama.com + curated
fallback so classic models like qwen2.5 stay reachable)
* "Browse Ollama library" toggle below Download with size chips
* Engine=Ollama in hwfit toolbar merges the Ollama library into the
main scan list as per-tag rows with the same Fit/Param/Quant/VRAM
columns; click → fills Download input
- API Tokens form added to Integrations panel (matching wired
loadTokens()/initTokenForm() that had no HTML)
- Serve panel polish: Advanced fold tightening (-8px nudges on vLLM
checks, Extra args, Spec row), n_cpu_moe + Split Mode controls
pulled up 8px to align with the row's checkboxes, GGUF File dropdown
exposed for Ollama backend, GPU re-render on Edit serve restore,
_forceBackend flag so saved serveState wins over backend detection,
cookbook:servers-changed CustomEvent so panels don't need refresh
- Models page redesign: Add Models row (URL + hidden API key reveal +
Type select + Scan/Ollama/Key/Test/Add icon buttons), Probe All +
Clear-offline buttons in Added Models toolbar, offline-pill removed
(opacity already conveys state), Engine dropdown gains Ollama option
- _ping_endpoint probes /v1/models then base, accepts 4xx as
reachable (vLLM returns 404 on bare /v1, fully working endpoints
were showing offline)
- Diagnosis card: × dismiss + Copy bundle buttons restored on the
serve error feedback card
- Orphan tmux sweep re-enabled behind a 60s rate-limit + background
Thread (off the main event loop) so dead serves get discovered
- cookbook_routes auto-register watchdog: drops the endpoint if the
serve session exits non-zero within the first ~3min
- ollama-rocm sidecar awareness in download wrapper (`docker exec
ollama-rocm ollama pull` when host ollama isn't installed)
- Skill extractor sets initial_status="published" when
auto_approve_skills pref is on (audit demotes later)
- Skill list / model list / cookbook scan misc polish
2026-06-08 22:38:49 +09:00
const addBtn = el ( 'adm-tokenAddBtn' ) ;
if ( ! addBtn || addBtn . dataset . bound ) return ;
addBtn . dataset . bound = '1' ;
addBtn . addEventListener ( 'click' , async ( ) => {
2026-05-31 23:58:26 +09:00
const msg = el ( 'adm-tokenMsg' ) ;
const reveal = el ( 'adm-tokenReveal' ) ;
msg . textContent = '' ; msg . className = '' ; reveal . style . display = 'none' ;
const name = el ( 'adm-tokenName' ) . value . trim ( ) ;
if ( ! name ) { msg . textContent = 'Token name is required' ; msg . className = 'admin-error' ; return ; }
const fd = new FormData ( ) ; fd . append ( 'name' , name ) ;
Cookbook UI: Ollama browser, advanced serve fold, API tokens form, diagnosis toolbar, polish
Surface a lot of accumulated cookbook + UI work as a single non-agent
commit so the agent rework lands cleanly.
Highlights:
- Ollama as a first-class backend in the Cookbook:
* Download input accepts ollama-style names (name:tag) → backend=ollama
* /api/cookbook/ollama/library (cached scrape of ollama.com + curated
fallback so classic models like qwen2.5 stay reachable)
* "Browse Ollama library" toggle below Download with size chips
* Engine=Ollama in hwfit toolbar merges the Ollama library into the
main scan list as per-tag rows with the same Fit/Param/Quant/VRAM
columns; click → fills Download input
- API Tokens form added to Integrations panel (matching wired
loadTokens()/initTokenForm() that had no HTML)
- Serve panel polish: Advanced fold tightening (-8px nudges on vLLM
checks, Extra args, Spec row), n_cpu_moe + Split Mode controls
pulled up 8px to align with the row's checkboxes, GGUF File dropdown
exposed for Ollama backend, GPU re-render on Edit serve restore,
_forceBackend flag so saved serveState wins over backend detection,
cookbook:servers-changed CustomEvent so panels don't need refresh
- Models page redesign: Add Models row (URL + hidden API key reveal +
Type select + Scan/Ollama/Key/Test/Add icon buttons), Probe All +
Clear-offline buttons in Added Models toolbar, offline-pill removed
(opacity already conveys state), Engine dropdown gains Ollama option
- _ping_endpoint probes /v1/models then base, accepts 4xx as
reachable (vLLM returns 404 on bare /v1, fully working endpoints
were showing offline)
- Diagnosis card: × dismiss + Copy bundle buttons restored on the
serve error feedback card
- Orphan tmux sweep re-enabled behind a 60s rate-limit + background
Thread (off the main event loop) so dead serves get discovered
- cookbook_routes auto-register watchdog: drops the endpoint if the
serve session exits non-zero within the first ~3min
- ollama-rocm sidecar awareness in download wrapper (`docker exec
ollama-rocm ollama pull` when host ollama isn't installed)
- Skill extractor sets initial_status="published" when
auto_approve_skills pref is on (audit demotes later)
- Skill list / model list / cookbook scan misc polish
2026-06-08 22:38:49 +09:00
const scopes = ( el ( 'adm-tokenScopes' ) ? . value || '' ) . trim ( ) ;
if ( scopes ) fd . append ( 'scopes' , scopes ) ;
2026-05-31 23:58:26 +09:00
try {
const res = await fetch ( '/api/tokens' , { method : 'POST' , body : fd , credentials : 'same-origin' } ) ;
const data = await res . json ( ) ;
Cookbook UI: Ollama browser, advanced serve fold, API tokens form, diagnosis toolbar, polish
Surface a lot of accumulated cookbook + UI work as a single non-agent
commit so the agent rework lands cleanly.
Highlights:
- Ollama as a first-class backend in the Cookbook:
* Download input accepts ollama-style names (name:tag) → backend=ollama
* /api/cookbook/ollama/library (cached scrape of ollama.com + curated
fallback so classic models like qwen2.5 stay reachable)
* "Browse Ollama library" toggle below Download with size chips
* Engine=Ollama in hwfit toolbar merges the Ollama library into the
main scan list as per-tag rows with the same Fit/Param/Quant/VRAM
columns; click → fills Download input
- API Tokens form added to Integrations panel (matching wired
loadTokens()/initTokenForm() that had no HTML)
- Serve panel polish: Advanced fold tightening (-8px nudges on vLLM
checks, Extra args, Spec row), n_cpu_moe + Split Mode controls
pulled up 8px to align with the row's checkboxes, GGUF File dropdown
exposed for Ollama backend, GPU re-render on Edit serve restore,
_forceBackend flag so saved serveState wins over backend detection,
cookbook:servers-changed CustomEvent so panels don't need refresh
- Models page redesign: Add Models row (URL + hidden API key reveal +
Type select + Scan/Ollama/Key/Test/Add icon buttons), Probe All +
Clear-offline buttons in Added Models toolbar, offline-pill removed
(opacity already conveys state), Engine dropdown gains Ollama option
- _ping_endpoint probes /v1/models then base, accepts 4xx as
reachable (vLLM returns 404 on bare /v1, fully working endpoints
were showing offline)
- Diagnosis card: × dismiss + Copy bundle buttons restored on the
serve error feedback card
- Orphan tmux sweep re-enabled behind a 60s rate-limit + background
Thread (off the main event loop) so dead serves get discovered
- cookbook_routes auto-register watchdog: drops the endpoint if the
serve session exits non-zero within the first ~3min
- ollama-rocm sidecar awareness in download wrapper (`docker exec
ollama-rocm ollama pull` when host ollama isn't installed)
- Skill extractor sets initial_status="published" when
auto_approve_skills pref is on (audit demotes later)
- Skill list / model list / cookbook scan misc polish
2026-06-08 22:38:49 +09:00
if ( res . ok ) {
el ( 'adm-tokenValue' ) . textContent = data . token ;
reveal . style . display = '' ;
el ( 'adm-tokenName' ) . value = '' ;
if ( el ( 'adm-tokenScopes' ) ) el ( 'adm-tokenScopes' ) . value = '' ;
loadTokens ( ) ;
}
2026-05-31 23:58:26 +09:00
else { msg . textContent = data . detail || 'Failed' ; msg . className = 'admin-error' ; }
} catch ( e ) { msg . textContent = 'Request failed' ; msg . className = 'admin-error' ; }
} ) ;
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
const TOKEN _COPY _ICON = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>' ;
const TOKEN _CHECK _ICON = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>' ;
2026-05-31 23:58:26 +09:00
el ( 'adm-tokenCopyBtn' ) . addEventListener ( 'click' , ( ) => {
const val = el ( 'adm-tokenValue' ) . textContent ;
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
const btn = el ( 'adm-tokenCopyBtn' ) ;
2026-05-31 23:58:26 +09:00
navigator . clipboard . writeText ( val ) . then ( ( ) => {
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
btn . innerHTML = TOKEN _CHECK _ICON ;
btn . style . color = 'var(--accent, var(--red))' ;
btn . style . opacity = '1' ;
setTimeout ( ( ) => {
btn . innerHTML = TOKEN _COPY _ICON ;
btn . style . color = '' ;
btn . style . opacity = '0.7' ;
} , 1600 ) ;
2026-05-31 23:58:26 +09:00
} ) ;
} ) ;
}
/* ── Webhooks ── */
async function loadWebhooks ( ) {
const list = el ( 'adm-whList' ) ;
try {
const res = await fetch ( '/api/webhooks' , { credentials : 'same-origin' } ) ;
const hooks = await res . json ( ) ;
if ( ! hooks . length ) { list . innerHTML = '<div class="admin-empty">No webhooks configured</div>' ; return ; }
list . innerHTML = hooks . map ( w => {
const events = ( w . events || [ ] ) . map ( e => ` <span class="admin-badge"> ${ esc ( e ) } </span> ` ) . join ( ' ' ) ;
const statusBadge = w . last _status _code
? ` <span class="admin-badge" style="background: ${ w . last _status _code < 400 ? 'color-mix(in srgb, var(--fg) 20%, transparent)' : 'color-mix(in srgb, var(--red) 20%, transparent)' } ;color: ${ w . last _status _code < 400 ? 'var(--fg)' : 'var(--red)' } ;"> ${ w . last _status _code } </span> `
: '' ;
const lastTriggered = w . last _triggered _at ? new Date ( w . last _triggered _at ) . toLocaleString ( ) : 'Never' ;
const errorText = w . last _error ? ` <div style="font-size:0.75rem;color:var(--red);margin-top:0.2rem;">Error: ${ esc ( w . last _error . substring ( 0 , 80 ) ) } </div> ` : '' ;
return `
< div class = "admin-ep-item" style = "flex-wrap:wrap;" >
< div class = "admin-ep-info" style = "flex:1;min-width:200px;" >
< div class = "admin-ep-name" > $ { esc ( w . name ) } $ { w . is _active ? '' : '<span class="admin-badge admin-badge-off">disabled</span>' } $ { w . has _secret ? '<span class="admin-badge">signed</span>' : '' } < / d i v >
< div class = "admin-ep-detail" > $ { esc ( w . url ) } < / d i v >
< div style = "margin-top:0.3rem;" > $ { events } < / d i v >
< div class = "admin-ep-detail" > Last : $ { lastTriggered } $ { statusBadge } < / d i v >
$ { errorText }
< / d i v >
< div class = "admin-ep-actions" >
< button class = "admin-btn-sm" data - adm - wh - test = "${w.id}" > Test < / b u t t o n >
< button class = "admin-btn-sm" data - adm - wh - toggle = "${w.id}" > $ { w . is _active ? 'Disable' : 'Enable' } < / b u t t o n >
< button class = "admin-btn-delete" data - adm - wh - delete = "${w.id}" > Delete < / b u t t o n >
< / d i v >
< / d i v > ` ;
} ) . join ( '' ) ;
list . querySelectorAll ( '[data-adm-wh-test]' ) . forEach ( btn => {
btn . addEventListener ( 'click' , async ( ) => {
const msg = el ( 'adm-whMsg' ) ; msg . textContent = 'Sending test...' ; msg . className = '' ;
try {
const res = await fetch ( ` /api/webhooks/ ${ btn . dataset . admWhTest } /test ` , { method : 'POST' , credentials : 'same-origin' } ) ;
msg . textContent = res . ok ? 'Test sent!' : 'Test failed' ; msg . className = res . ok ? 'admin-success' : 'admin-error' ;
setTimeout ( ( ) => loadWebhooks ( ) , 1000 ) ;
} catch ( e ) { msg . textContent = 'Failed: ' + e . message ; msg . className = 'admin-error' ; }
} ) ;
} ) ;
list . querySelectorAll ( '[data-adm-wh-toggle]' ) . forEach ( btn => {
btn . addEventListener ( 'click' , async ( ) => { await fetch ( ` /api/webhooks/ ${ btn . dataset . admWhToggle } ` , { method : 'PATCH' , credentials : 'same-origin' } ) ; loadWebhooks ( ) ; } ) ;
} ) ;
list . querySelectorAll ( '[data-adm-wh-delete]' ) . forEach ( btn => {
btn . addEventListener ( 'click' , async ( ) => {
if ( ! await uiModule . styledConfirm ( 'Delete this webhook?' , { confirmText : 'Delete' , danger : true } ) ) return ;
await fetch ( ` /api/webhooks/ ${ btn . dataset . admWhDelete } ` , { method : 'DELETE' , credentials : 'same-origin' } ) ; loadWebhooks ( ) ;
} ) ;
} ) ;
} catch ( e ) { list . innerHTML = '<div class="admin-error">Failed to load webhooks</div>' ; }
}
function initWebhookForm ( ) {
el ( 'adm-whAddBtn' ) . addEventListener ( 'click' , async ( ) => {
const msg = el ( 'adm-whMsg' ) ;
msg . textContent = '' ; msg . className = '' ;
const name = el ( 'adm-whName' ) . value . trim ( ) ;
const url = el ( 'adm-whUrl' ) . value . trim ( ) ;
const secret = el ( 'adm-whSecret' ) . value . trim ( ) ;
const events = Array . from ( modalEl . querySelectorAll ( '.adm-wh-event:checked' ) ) . map ( e => e . value ) . join ( ',' ) ;
if ( ! name ) { msg . textContent = 'Name is required' ; msg . className = 'admin-error' ; return ; }
if ( ! url ) { msg . textContent = 'URL is required' ; msg . className = 'admin-error' ; return ; }
if ( ! events ) { msg . textContent = 'Select at least one event' ; msg . className = 'admin-error' ; return ; }
const fd = new FormData ( ) ;
fd . append ( 'name' , name ) ; fd . append ( 'url' , url ) ; fd . append ( 'secret' , secret ) ; fd . append ( 'events' , events ) ;
try {
const res = await fetch ( '/api/webhooks' , { method : 'POST' , body : fd , credentials : 'same-origin' } ) ;
if ( res . ok ) { msg . textContent = 'Webhook added' ; msg . className = 'admin-success' ; el ( 'adm-whName' ) . value = '' ; el ( 'adm-whUrl' ) . value = '' ; el ( 'adm-whSecret' ) . value = '' ; loadWebhooks ( ) ; }
else { const d = await res . json ( ) ; msg . textContent = d . detail || 'Failed' ; msg . className = 'admin-error' ; }
} catch ( e ) { msg . textContent = 'Failed: ' + e . message ; msg . className = 'admin-error' ; }
} ) ;
}
/* ── Features ── */
const featureLabels = {
web _search : 'Web Search' , deep _research : 'Deep Research' ,
memory : 'Memory' , document _editor : 'Document Editor' , rag : 'RAG Knowledge Base' , sensitive _filter : 'Sensitive Info Filter' ,
gallery : 'Gallery'
} ;
async function loadFeatures ( ) {
const container = el ( 'adm-featureToggles' ) ;
try {
const res = await fetch ( '/api/auth/features' , { credentials : 'same-origin' } ) ;
const features = await res . json ( ) ;
container . innerHTML = Object . entries ( featureLabels ) . map ( ( [ key , label ] ) => `
< div class = "admin-toggle-row" style = "padding:0.4rem 0;border-bottom:1px solid var(--border);" >
< div class = "admin-toggle-label" > $ { label } < / d i v >
< label class = "admin-switch" > < input type = "checkbox" data - adm - feature = "${key}" $ { features [ key ] ? 'checked' : '' } > < span class = "admin-slider" > < / s p a n > < / l a b e l >
< / d i v > ` ) . j o i n ( ' ' ) ;
container . querySelectorAll ( 'input[data-adm-feature]' ) . forEach ( toggle => {
toggle . addEventListener ( 'change' , async ( ) => {
const body = { } ; body [ toggle . dataset . admFeature ] = toggle . checked ;
await fetch ( '/api/auth/features' , { method : 'POST' , credentials : 'same-origin' , headers : { 'Content-Type' : 'application/json' } , body : JSON . stringify ( body ) } ) ;
} ) ;
} ) ;
} catch ( e ) { container . innerHTML = '<div class="admin-error">Failed to load features</div>' ; }
}
/* ── CalDAV Config ── */
function initCalDAV ( ) {
const urlIn = el ( 'caldav-url' ) ;
const userIn = el ( 'caldav-user' ) ;
const passIn = el ( 'caldav-pass' ) ;
const saveBtn = el ( 'caldav-save-btn' ) ;
const testBtn = el ( 'caldav-test-btn' ) ;
const status = el ( 'caldav-status' ) ;
if ( ! urlIn || ! saveBtn ) return ;
// Load current config
fetch ( ` ${ API _BASE } /api/calendar/config ` , { credentials : 'same-origin' } )
. then ( r => r . json ( ) ) . then ( d => {
urlIn . value = d . caldav _url || '' ;
userIn . value = d . caldav _username || '' ;
passIn . value = d . caldav _password || '' ;
} ) . catch ( ( ) => { } ) ;
saveBtn . addEventListener ( 'click' , async ( ) => {
status . textContent = 'Saving...' ;
try {
const res = await fetch ( ` ${ API _BASE } /api/calendar/config ` , {
method : 'POST' , credentials : 'same-origin' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { caldav _url : urlIn . value , caldav _username : userIn . value , caldav _password : passIn . value } ) ,
} ) ;
const d = await res . json ( ) ;
status . textContent = d . ok ? 'Saved' : 'Error' ;
status . style . color = d . ok ? 'var(--green)' : 'var(--red)' ;
} catch ( e ) { status . textContent = 'Error' ; status . style . color = 'var(--red)' ; }
setTimeout ( ( ) => { status . textContent = '' ; status . style . color = '' ; } , 3000 ) ;
} ) ;
testBtn . addEventListener ( 'click' , async ( ) => {
status . textContent = 'Testing...' ;
try {
// Save first
await fetch ( ` ${ API _BASE } /api/calendar/config ` , {
method : 'POST' , credentials : 'same-origin' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { caldav _url : urlIn . value , caldav _username : userIn . value , caldav _password : passIn . value } ) ,
} ) ;
const res = await fetch ( ` ${ API _BASE } /api/calendar/test ` , { method : 'POST' , credentials : 'same-origin' } ) ;
const d = await res . json ( ) ;
status . textContent = d . ok ? ` Connected ( ${ d . calendars } calendars) ` : ` Failed: ${ d . error } ` ;
status . style . color = d . ok ? 'var(--green)' : 'var(--red)' ;
} catch ( e ) { status . textContent = 'Error' ; status . style . color = 'var(--red)' ; }
setTimeout ( ( ) => { status . textContent = '' ; status . style . color = '' ; } , 5000 ) ;
} ) ;
}
/* ── Data Backup (export/import) ── */
function initBackup ( ) {
el ( 'adm-exportDataBtn' ) . addEventListener ( 'click' , async ( ) => {
const btn = el ( 'adm-exportDataBtn' ) ;
const msg = el ( 'adm-backupMsg' ) ;
btn . disabled = true ; btn . textContent = 'Exporting...' ; msg . textContent = '' ;
try {
const res = await fetch ( '/api/export' , { credentials : 'same-origin' } ) ;
if ( ! res . ok ) throw new Error ( 'Export failed' ) ;
const blob = await res . blob ( ) ;
const disposition = res . headers . get ( 'Content-Disposition' ) || '' ;
const match = disposition . match ( /filename=(.+)/ ) ;
const filename = match ? match [ 1 ] : 'odysseus_backup.json' ;
const a = document . createElement ( 'a' ) ;
a . href = URL . createObjectURL ( blob ) ;
a . download = filename ;
a . click ( ) ;
URL . revokeObjectURL ( a . href ) ;
msg . textContent = 'Export downloaded.' ; msg . className = 'admin-success' ;
} catch ( e ) { msg . textContent = 'Export failed: ' + e . message ; msg . className = 'admin-error' ; }
btn . disabled = false ; btn . textContent = 'Export Data' ;
} ) ;
const fileInput = el ( 'adm-importFile' ) ;
el ( 'adm-importDataBtn' ) . addEventListener ( 'click' , ( ) => { fileInput . value = '' ; fileInput . click ( ) ; } ) ;
fileInput . addEventListener ( 'change' , async ( ) => {
const file = fileInput . files [ 0 ] ;
if ( ! file ) return ;
const msg = el ( 'adm-backupMsg' ) ;
const btn = el ( 'adm-importDataBtn' ) ;
btn . disabled = true ; btn . textContent = 'Importing...' ; msg . textContent = '' ;
try {
2026-06-06 23:46:32 +03:00
const text = ( await file . text ( ) ) . replace ( /^\uFEFF/ , '' ) . trim ( ) ;
let data ;
try {
data = JSON . parse ( text ) ;
} catch ( e ) {
throw new Error ( 'Invalid backup file: ' + e . message ) ;
}
2026-05-31 23:58:26 +09:00
const res = await fetch ( '/api/import' , {
method : 'POST' , credentials : 'same-origin' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( data ) ,
} ) ;
2026-06-06 23:46:32 +03:00
const result = await res . json ( ) . catch ( ( ) => null ) ;
if ( ! result ) {
throw new Error ( ` Import failed: server returned ${ res . status } ` ) ;
}
2026-05-31 23:58:26 +09:00
if ( res . ok && result . ok ) {
msg . textContent = result . message || 'Import successful.' ; msg . className = 'admin-success' ;
} else {
msg . textContent = result . message || result . detail || 'Import failed' ; msg . className = 'admin-error' ;
}
} catch ( e ) { msg . textContent = 'Import failed: ' + e . message ; msg . className = 'admin-error' ; }
btn . disabled = false ; btn . textContent = 'Import Data' ;
} ) ;
}
/* ── Danger Zone ── */
function initDangerZone ( ) {
// Per-category Danger Zone wipes. Each button declares its target
// via data-wipe-kind; one delegated handler handles double-confirm,
// POSTs to /api/admin/wipe/{kind}, and writes the result.
const _LABELS = {
chats : 'chats' , memory : 'memory entries' , skills : 'skills' ,
notes : 'notes' , tasks : 'tasks' , documents : 'documents' ,
gallery : 'gallery images' , calendar : 'calendar items' ,
} ;
const _wipeMsg = el ( 'adm-wipeMsg' ) ;
modalEl . querySelectorAll ( '[data-wipe-kind]' ) . forEach ( btn => {
btn . addEventListener ( 'click' , async ( ) => {
const kind = btn . dataset . wipeKind ;
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
const isAll = kind === '__all__' ;
const label = isAll ? 'data across every category' : ( _LABELS [ kind ] || kind ) ;
if ( ! await uiModule . styledConfirm ( ` Delete ALL ${ label } ? This cannot be undone. ` , { confirmText : 'Delete' , danger : true } ) ) return ;
if ( ! await uiModule . styledConfirm ( ` Really delete every one of your ${ label } ? ` , { confirmText : isAll ? 'Yes, delete everything' : 'Yes, delete everything' , danger : true } ) ) return ;
btn . disabled = true ;
const prevHtml = btn . innerHTML ;
btn . innerHTML = isAll ? 'Deleting all…' : 'Deleting…' ;
2026-05-31 23:58:26 +09:00
if ( _wipeMsg ) { _wipeMsg . textContent = '' ; _wipeMsg . className = '' ; }
try {
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
if ( isAll ) {
// Iterate every known category. Failures in one shouldn't stop
// the rest — record per-category counts and surface a summary.
const kinds = Object . keys ( _LABELS ) ;
const results = [ ] ;
for ( const k of kinds ) {
try {
const r = await fetch ( ` /api/admin/wipe/ ${ k } ` , { method : 'DELETE' , credentials : 'same-origin' } ) ;
const d = await r . json ( ) . catch ( ( ) => ( { } ) ) ;
results . push ( { k , ok : r . ok , count : d . count ? ? 0 , error : r . ok ? null : ( d . detail || 'failed' ) } ) ;
} catch ( e ) {
results . push ( { k , ok : false , count : 0 , error : e . message } ) ;
}
}
const okCount = results . filter ( r => r . ok ) . length ;
const total = results . reduce ( ( n , r ) => n + ( r . ok ? r . count : 0 ) , 0 ) ;
const fails = results . filter ( r => ! r . ok ) . map ( r => r . k ) ;
if ( _wipeMsg ) {
if ( ! fails . length ) {
_wipeMsg . textContent = ` Deleted ${ total } items across all ${ okCount } categories. ` ;
_wipeMsg . className = 'admin-success' ;
} else {
_wipeMsg . textContent = ` Deleted ${ total } items; failed: ${ fails . join ( ', ' ) } . ` ;
_wipeMsg . className = 'admin-error' ;
}
}
2026-05-31 23:58:26 +09:00
} else {
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
const res = await fetch ( ` /api/admin/wipe/ ${ kind } ` , { method : 'DELETE' , credentials : 'same-origin' } ) ;
const data = await res . json ( ) . catch ( ( ) => ( { } ) ) ;
if ( res . ok ) {
if ( _wipeMsg ) { _wipeMsg . textContent = ` Deleted ${ data . count ? ? 0 } ${ label } . ` ; _wipeMsg . className = 'admin-success' ; }
} else {
if ( _wipeMsg ) { _wipeMsg . textContent = data . detail || 'Failed' ; _wipeMsg . className = 'admin-error' ; }
}
2026-05-31 23:58:26 +09:00
}
} catch ( e ) {
if ( _wipeMsg ) { _wipeMsg . textContent = 'Request failed: ' + e . message ; _wipeMsg . className = 'admin-error' ; }
}
Settings overhaul + UI polish pass
Two months of iteration on the Settings panel, integration forms, and
small visual nudges across the app. Highlights:
Settings restructure
- Add Models: split into separate Local + API cards (no more in-card
tabs); each fuses Type/Provider with the URL input.
- Added Models: new dedicated sidebar tab, with Probe + Clear-offline
pulled into its header; Local/API sub-section icons accent-tinted.
- Search: Web Search and a new Deep Research card (Model + tuning),
with a cross-link to AI Defaults. Provider hints use real clickable
anchors; Web Search Test button shows a whirlpool spinner.
- AI Defaults: Image Generation card returns; Research Model card
carries only Endpoint+Model with a cross-link to Search; Vision /
Default / Utility fallbacks unified under one numbered-row design
matching Search's chain.
- API Permissions (was 'API Tokens'): per-row rename, inline
Permissions toggle that expands the scope-edit panel, in-field
copy icons (icon→check on success). Empty state accent-tinted.
- Integrations: + Add Integration drops a type-picker menu directly
under the button (drop-up on tight viewports); each integration
form (API, CalDAV, CardDAV, Email, Codex/Claude, Vault, MCP) uses
the same accent-outlined Save/Test/Cancel buttons right-aligned.
- Danger Zone: Wipe→Delete with trash icons; new 'Delete everything'
row at the bottom that loops every category.
AI Synthesis (Reminders)
- Persona dropdown sourced from PROMPT_TEMPLATES + custom preset.
- src/reminder_personas.py mirrors the five built-ins for the
server-side synthesis path.
- dispatch_reminder() reads reminder_llm_persona and uses the
persona's system prompt; empty/unknown falls back to warm-neutral.
Esc handling
- Kebab menus and the provider picker intercept Esc in capture phase
so dismissing a popup no longer closes the whole Settings modal.
Accent tinting
- Scoped CSS rule across data-settings-panel=ai/services/added-models/
search/integrations/reminders for card h2 icons + the Added Models
sub-section icons.
Codex/Claude integration form
- No more auto-creation on form open — explicit Create token button.
- New tokens start with every scope granted; existing tokens move out
of the integration form into the API Permissions card.
- Setup reveal: copy buttons inline inside the token + setup code
blocks; shorter subtitle wording.
Misc visual polish
- Save/Test/Cancel uniformly accent-outlined and right-aligned on
every integration form.
- Provider logos render inline next to the search fallback selects
and the Deep Research Search dropdown.
- Trash icons in fallback rows bumped to 20x20 so they fill the 32px
button.
- Image generation default flipped to off.
2026-06-10 15:15:13 +09:00
btn . disabled = false ; btn . innerHTML = prevHtml ;
2026-05-31 23:58:26 +09:00
} ) ;
} ) ;
}
2026-06-15 11:32:51 +03:00
/ * ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═
TERMINAL LOGS VIEWER
═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ * /
let logsPollInterval = null ;
let isLogsPolling = false ;
let cachedLogs = [ ] ;
let logsAbortController = null ;
function renderLogs ( isAutoPoll = false ) {
const consoleContainer = el ( 'log-console-container' ) ;
const levelSelect = el ( 'log-level-select' ) ;
const searchInput = el ( 'log-search-input' ) ;
if ( ! consoleContainer ) return ;
const levelFilter = levelSelect ? levelSelect . value : 'ALL' ;
const searchQuery = searchInput ? searchInput . value . trim ( ) . toLowerCase ( ) : '' ;
let logs = cachedLogs ;
// Filter by level locally
if ( levelFilter !== 'ALL' ) {
logs = logs . filter ( line => line . includes ( ` - ${ levelFilter } - ` ) ) ;
}
// Filter by search query locally
if ( searchQuery ) {
logs = logs . filter ( line => line . toLowerCase ( ) . includes ( searchQuery ) ) ;
}
if ( logs . length === 0 ) {
consoleContainer . innerHTML = '<div class="settings-system-logs-placeholder">No logs found matching current filters.</div>' ;
return ;
}
// Preserve scroll position if user is reading previous logs
const atBottom = consoleContainer . scrollHeight - consoleContainer . scrollTop - consoleContainer . clientHeight < 40 ;
consoleContainer . innerHTML = logs . map ( line => {
let levelClass = 'log-line-default' ;
if ( line . includes ( ' - INFO - ' ) ) {
levelClass = 'log-line-info' ;
} else if ( line . includes ( ' - WARNING - ' ) ) {
levelClass = 'log-line-warning' ;
} else if ( line . includes ( ' - ERROR - ' ) || line . includes ( ' - CRITICAL - ' ) ) {
levelClass = 'log-line-error' ;
} else if ( line . includes ( ' - DEBUG - ' ) ) {
levelClass = 'log-line-debug' ;
}
// XSS safe escape
const escaped = line
. replace ( /&/g , '&' )
. replace ( /</g , '<' )
. replace ( />/g , '>' )
. replace ( /"/g , '"' )
. replace ( /'/g , ''' ) ;
return ` <div class="log-line ${ levelClass } "> ${ escaped } </div> ` ;
} ) . join ( '' ) ;
if ( ! isAutoPoll || atBottom ) {
consoleContainer . scrollTop = consoleContainer . scrollHeight ;
}
}
async function loadLogs ( isAutoPoll = false ) {
const consoleContainer = el ( 'log-console-container' ) ;
const limitSelect = el ( 'log-limit-select' ) ;
if ( ! consoleContainer ) return ;
const limit = limitSelect ? limitSelect . value : 200 ;
if ( logsAbortController ) {
logsAbortController . abort ( ) ;
}
logsAbortController = new AbortController ( ) ;
const { signal } = logsAbortController ;
try {
const res = await fetch ( ` /api/diagnostics/logs?limit= ${ limit } ` , {
credentials : 'same-origin' ,
signal
} ) ;
if ( ! res . ok ) {
if ( ! isAutoPoll ) {
consoleContainer . innerHTML = '' ;
const errDiv = document . createElement ( 'div' ) ;
errDiv . style . color = 'var(--red)' ;
errDiv . style . fontWeight = '600' ;
errDiv . textContent = ` Failed to load logs: HTTP ${ res . status } ` ;
consoleContainer . appendChild ( errDiv ) ;
}
return ;
}
const data = await res . json ( ) ;
if ( data . status !== 'success' || ! data . logs ) {
if ( ! isAutoPoll ) {
consoleContainer . innerHTML = '' ;
const errDiv = document . createElement ( 'div' ) ;
errDiv . style . color = 'var(--red)' ;
errDiv . style . fontWeight = '600' ;
errDiv . textContent = 'Failed to parse logs data' ;
consoleContainer . appendChild ( errDiv ) ;
}
return ;
}
cachedLogs = data . logs ;
renderLogs ( isAutoPoll ) ;
} catch ( err ) {
if ( err . name === 'AbortError' ) {
return ; // Silently ignore deliberate abort
}
if ( ! isAutoPoll ) {
consoleContainer . innerHTML = '' ;
const errDiv = document . createElement ( 'div' ) ;
errDiv . style . color = 'var(--red)' ;
errDiv . style . fontWeight = '600' ;
errDiv . textContent = ` Error retrieving logs: ${ err . message } ` ;
consoleContainer . appendChild ( errDiv ) ;
}
} finally {
if ( logsAbortController ? . signal === signal ) {
logsAbortController = null ;
}
}
}
function startLogsPolling ( ) {
if ( isLogsPolling ) return ;
isLogsPolling = true ;
const toggle = el ( 'log-auto-refresh-toggle' ) ;
if ( toggle ) toggle . checked = true ;
logsPollInterval = setInterval ( ( ) => {
const modal = el ( 'settings-modal' ) ;
const systemPanel = el ( 'settings-modal' ) ? . querySelector ( '[data-settings-panel="system"]' ) ;
// Safe self-cleanup if modal or panel is hidden/closed
if ( ! modal || modal . classList . contains ( 'hidden' ) || ! systemPanel || systemPanel . classList . contains ( 'hidden' ) ) {
stopLogsPolling ( ) ;
return ;
}
loadLogs ( true ) ;
} , 3000 ) ;
}
function stopLogsPolling ( ) {
if ( ! isLogsPolling ) return ;
isLogsPolling = false ;
if ( logsPollInterval ) {
clearInterval ( logsPollInterval ) ;
logsPollInterval = null ;
}
const toggle = el ( 'log-auto-refresh-toggle' ) ;
if ( toggle ) toggle . checked = false ;
}
function initLogsView ( ) {
const refreshBtn = el ( 'log-refresh-btn' ) ;
const levelSelect = el ( 'log-level-select' ) ;
const limitSelect = el ( 'log-limit-select' ) ;
const searchInput = el ( 'log-search-input' ) ;
const autoRefreshToggle = el ( 'log-auto-refresh-toggle' ) ;
if ( refreshBtn ) refreshBtn . addEventListener ( 'click' , ( ) => loadLogs ( false ) ) ;
if ( levelSelect ) levelSelect . addEventListener ( 'change' , ( ) => renderLogs ( false ) ) ;
if ( limitSelect ) limitSelect . addEventListener ( 'change' , ( ) => loadLogs ( false ) ) ;
if ( searchInput ) searchInput . addEventListener ( 'input' , ( ) => renderLogs ( false ) ) ;
if ( autoRefreshToggle ) {
autoRefreshToggle . addEventListener ( 'change' , ( e ) => {
if ( e . target . checked ) {
startLogsPolling ( ) ;
} else {
stopLogsPolling ( ) ;
}
} ) ;
}
// Initial fetch on view loading
loadLogs ( false ) ;
}
2026-05-31 23:58:26 +09:00
/ * ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═
INIT & REFRESH
═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ * /
function initAll ( ) {
modalEl = el ( 'settings-modal' ) ;
2026-06-15 11:32:51 +03:00
const inits = [
2026-06-23 23:06:45 +02:00
initSignupToggle , initShareDefaultsToggle , initAddUser , initEndpointForm , initMcpForm ,
2026-06-15 11:32:51 +03:00
initCalDAV , initBackup , initDangerZone , initTokenForm , initLogsView ,
( ) => settingsModule . initIntegrations ( )
] ;
2026-05-31 23:58:26 +09:00
for ( const fn of inits ) {
try { fn ( ) ; } catch ( e ) { console . error ( 'Admin init error in' , fn . name || 'anonymous' , e ) ; }
}
initialized = true ;
refreshAll ( ) ;
}
function refreshAll ( ) {
loadUsers ( ) ;
loadEndpoints ( ) ;
loadBuiltinTools ( ) ;
loadMcpServers ( ) ;
Cookbook UI: Ollama browser, advanced serve fold, API tokens form, diagnosis toolbar, polish
Surface a lot of accumulated cookbook + UI work as a single non-agent
commit so the agent rework lands cleanly.
Highlights:
- Ollama as a first-class backend in the Cookbook:
* Download input accepts ollama-style names (name:tag) → backend=ollama
* /api/cookbook/ollama/library (cached scrape of ollama.com + curated
fallback so classic models like qwen2.5 stay reachable)
* "Browse Ollama library" toggle below Download with size chips
* Engine=Ollama in hwfit toolbar merges the Ollama library into the
main scan list as per-tag rows with the same Fit/Param/Quant/VRAM
columns; click → fills Download input
- API Tokens form added to Integrations panel (matching wired
loadTokens()/initTokenForm() that had no HTML)
- Serve panel polish: Advanced fold tightening (-8px nudges on vLLM
checks, Extra args, Spec row), n_cpu_moe + Split Mode controls
pulled up 8px to align with the row's checkboxes, GGUF File dropdown
exposed for Ollama backend, GPU re-render on Edit serve restore,
_forceBackend flag so saved serveState wins over backend detection,
cookbook:servers-changed CustomEvent so panels don't need refresh
- Models page redesign: Add Models row (URL + hidden API key reveal +
Type select + Scan/Ollama/Key/Test/Add icon buttons), Probe All +
Clear-offline buttons in Added Models toolbar, offline-pill removed
(opacity already conveys state), Engine dropdown gains Ollama option
- _ping_endpoint probes /v1/models then base, accepts 4xx as
reachable (vLLM returns 404 on bare /v1, fully working endpoints
were showing offline)
- Diagnosis card: × dismiss + Copy bundle buttons restored on the
serve error feedback card
- Orphan tmux sweep re-enabled behind a 60s rate-limit + background
Thread (off the main event loop) so dead serves get discovered
- cookbook_routes auto-register watchdog: drops the endpoint if the
serve session exits non-zero within the first ~3min
- ollama-rocm sidecar awareness in download wrapper (`docker exec
ollama-rocm ollama pull` when host ollama isn't installed)
- Skill extractor sets initial_status="published" when
auto_approve_skills pref is on (audit demotes later)
- Skill list / model list / cookbook scan misc polish
2026-06-08 22:38:49 +09:00
loadTokens ( ) ;
2026-06-15 11:32:51 +03:00
loadLogs ( false ) ;
2026-05-31 23:58:26 +09:00
}
/ * ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═
PUBLIC API
═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ * /
export function _initData ( ) {
if ( ! initialized ) initAll ( ) ;
else refreshAll ( ) ;
}
export function open ( tab ) {
_initData ( ) ;
settingsModule . open ( tab || 'services' ) ;
}
export function close ( ) {
2026-06-15 11:32:51 +03:00
stopLogsPolling ( ) ;
2026-05-31 23:58:26 +09:00
settingsModule . close ( ) ;
}
const adminModule = { open , close , _initData , get _initialized ( ) { return initialized ; } } ;
export default adminModule ;