# Settings Panel — Phase 2: Remaining Tabs + New Session UI > **Execution:** Use the subagent-driven-development workflow to implement this plan. **Phase 2 of 2** — complete [Phase 1](./2026-03-29-settings-phase1.md) before starting this phase. **Design doc:** [`docs/plans/2026-03-29-settings-design.md`](./2026-03-29-settings-design.md) **Goal:** Complete all four settings tabs and build the new session creation UI — `+` button, inline name input, mobile FAB, and end-to-end creation flow. **Architecture:** Sessions/New Session tabs use the `GET/PATCH /api/settings` and `POST /api/sessions` endpoints from Phase 1. The `+` button uses inline name input (header, sidebar, FAB) that calls the create endpoint. Display/Notifications tabs use localStorage. All controls apply immediately — no save button. **Tech Stack:** Vanilla JS, CSS custom properties, FastAPI backend (already deployed from Phase 1). **Prereqs from Phase 1:** - `muxplex/settings.py` — `load_settings()`, `save_settings()`, `patch_settings()` - `GET/PATCH /api/settings`, `POST /api/sessions` endpoints in `main.py` - Settings dialog HTML skeleton with tab navigation in `index.html` - `openSettings()`, `closeSettings()`, `switchSettingsTab()`, `loadDisplaySettings()`, `saveDisplaySettings()` in `app.js` - `.settings-*` CSS classes in `style.css` --- ### Task 1: Sessions tab — server settings UI **Files:** - Modify: `muxplex/frontend/index.html` - Modify: `muxplex/frontend/app.js` **Step 1: Add Sessions tab HTML** In `muxplex/frontend/index.html`, replace the empty `#settings-panel-sessions` div: ```html ``` **Step 2: Add server settings load/save functions to app.js** In `muxplex/frontend/app.js`, add after the `onDisplaySettingChange` function: ```javascript // ─── Server settings ───────────────────────────────────────────────────────── let _serverSettings = null; /** * Load server settings via GET /api/settings. * Caches the result in _serverSettings for local reads. * @returns {Promise} */ async function loadServerSettings() { try { var res = await api('GET', '/api/settings'); _serverSettings = await res.json(); } catch (err) { console.warn('[loadServerSettings]', err); _serverSettings = _serverSettings || {}; } return _serverSettings; } /** * Patch a single server setting field. * @param {string} key * @param {*} value * @returns {Promise} */ async function patchServerSetting(key, value) { var patch = {}; patch[key] = value; try { var res = await api('PATCH', '/api/settings', patch); _serverSettings = await res.json(); showToast('Setting saved'); } catch (err) { console.warn('[patchServerSetting]', err); showToast('Failed to save setting'); } } ``` **Step 3: Populate Sessions tab when settings opens** Update the `openSettings` function. After the display settings load, add server settings loading: ```javascript // Load server settings and populate Sessions tab loadServerSettings().then(function(ss) { // Default session dropdown: populate with current session list var defaultSel = $('setting-default-session'); if (defaultSel) { var current = ss.default_session || ''; defaultSel.innerHTML = '' + _currentSessions.map(function(s) { var name = escapeHtml(s.name); var selected = s.name === current ? ' selected' : ''; return ''; }).join(''); } // Sort order var sortSel = $('setting-sort-order'); if (sortSel) sortSel.value = ss.sort_order || 'manual'; // Hidden sessions checkboxes var hiddenDiv = $('setting-hidden-sessions'); if (hiddenDiv) { var hiddenList = ss.hidden_sessions || []; hiddenDiv.innerHTML = _currentSessions.map(function(s) { var name = escapeHtml(s.name); var checked = hiddenList.indexOf(s.name) !== -1 ? ' checked' : ''; return ''; }).join(''); } // Checkboxes var wslEl = $('setting-window-size-largest'); if (wslEl) wslEl.checked = !!ss.window_size_largest; var aoEl = $('setting-auto-open'); if (aoEl) aoEl.checked = ss.auto_open_created !== false; }); ``` **Step 4: Bind change handlers for Sessions tab in bindStaticEventListeners** Add to `bindStaticEventListeners`: ```javascript // Sessions tab change handlers on($('setting-default-session'), 'change', function() { patchServerSetting('default_session', this.value || null); }); on($('setting-sort-order'), 'change', function() { patchServerSetting('sort_order', this.value); }); on($('setting-window-size-largest'), 'change', function() { patchServerSetting('window_size_largest', this.checked); }); on($('setting-auto-open'), 'change', function() { patchServerSetting('auto_open_created', this.checked); }); // Hidden sessions: delegated handler on container (checkboxes are dynamic) var hiddenContainer = $('setting-hidden-sessions'); if (hiddenContainer) { hiddenContainer.addEventListener('change', function() { var checkboxes = hiddenContainer.querySelectorAll('input[type="checkbox"]'); var hidden = []; checkboxes.forEach(function(cb) { if (cb.checked) hidden.push(cb.dataset.session); }); patchServerSetting('hidden_sessions', hidden); }); } ``` **Step 5: Add CSS for column-layout fields and checkbox list** Append to `muxplex/frontend/style.css`: ```css .settings-field--column { flex-direction: column; align-items: flex-start; gap: 8px; } .settings-checkbox-list { display: flex; flex-direction: column; gap: 6px; padding-left: 4px; } .settings-checkbox-item { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--text); cursor: pointer; } .settings-checkbox { accent-color: var(--accent); cursor: pointer; } ``` **Step 6: Add exports** Add to `module.exports`: ```javascript loadServerSettings, patchServerSetting, ``` **Step 7: Run tests** ```bash cd muxplex && python -m pytest muxplex/tests/ -v ``` Expected: All tests pass. **Step 8: Commit** ```bash git add -A && git commit -m "feat: add Sessions settings tab with server-side persistence" ``` --- ### Task 2: Notifications tab **Files:** - Modify: `muxplex/frontend/index.html` - Modify: `muxplex/frontend/app.js` - Modify: `muxplex/frontend/style.css` **Step 1: Add Notifications tab HTML** In `muxplex/frontend/index.html`, replace the empty `#settings-panel-notifications` div: ```html ``` **Step 2: Add CSS for notification status and action button** Append to `muxplex/frontend/style.css`: ```css .settings-notification-status { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; } .settings-status-text { font-size: 12px; color: var(--text-muted); } .settings-action-btn { background: var(--bg); border: 1px solid var(--border); border-radius: 4px; color: var(--text); font-size: 12px; font-family: var(--font-ui); padding: 6px 12px; cursor: pointer; transition: border-color var(--t-fast); } .settings-action-btn:hover { border-color: var(--accent); } .settings-action-btn:disabled { opacity: 0.5; cursor: default; } ``` **Step 3: Add notification handlers to app.js** Update `openSettings` to populate notification controls (add inside the function): ```javascript // Notifications tab var ds2 = loadDisplaySettings(); var bellSoundEl = $('setting-bell-sound'); if (bellSoundEl) bellSoundEl.checked = !!ds2.bellSound; var notifText = $('notification-status-text'); var notifBtn = $('notification-request-btn'); if (notifText && notifBtn) { var perm = typeof Notification !== 'undefined' ? Notification.permission : 'unavailable'; if (perm === 'granted') { notifText.textContent = 'Granted'; notifBtn.textContent = 'Granted'; notifBtn.disabled = true; } else if (perm === 'denied') { notifText.textContent = 'Denied (change in browser settings)'; notifBtn.textContent = 'Denied'; notifBtn.disabled = true; } else if (perm === 'unavailable') { notifText.textContent = 'Not supported'; notifBtn.disabled = true; } else { notifText.textContent = 'Not requested'; notifBtn.textContent = 'Request permission'; notifBtn.disabled = false; } } ``` **Step 4: Bind notification change handlers in bindStaticEventListeners** Add to `bindStaticEventListeners`: ```javascript // Notifications tab on($('setting-bell-sound'), 'change', function() { var ds = loadDisplaySettings(); ds.bellSound = this.checked; saveDisplaySettings(ds); }); on($('notification-request-btn'), 'click', function() { if (typeof Notification === 'undefined') return; Notification.requestPermission().then(function(perm) { _notificationPermission = perm; var ds = loadDisplaySettings(); ds.notificationPermission = perm; saveDisplaySettings(ds); // Update UI var notifText = $('notification-status-text'); var notifBtn = $('notification-request-btn'); if (perm === 'granted') { if (notifText) notifText.textContent = 'Granted'; if (notifBtn) { notifBtn.textContent = 'Granted'; notifBtn.disabled = true; } } else if (perm === 'denied') { if (notifText) notifText.textContent = 'Denied (change in browser settings)'; if (notifBtn) { notifBtn.textContent = 'Denied'; notifBtn.disabled = true; } } }); }); ``` **Step 5: Run tests** ```bash cd muxplex && python -m pytest muxplex/tests/ -v ``` Expected: All tests pass. **Step 6: Commit** ```bash git add -A && git commit -m "feat: add Notifications settings tab" ``` --- ### Task 3: New Session tab — template textarea **Files:** - Modify: `muxplex/frontend/index.html` - Modify: `muxplex/frontend/app.js` - Modify: `muxplex/frontend/style.css` **Step 1: Add New Session tab HTML** In `muxplex/frontend/index.html`, replace the empty `#settings-panel-new-session` div: ```html ``` **Step 2: Add CSS for textarea and helper text** Append to `muxplex/frontend/style.css`: ```css .settings-textarea { width: 100%; background: var(--bg); border: 1px solid var(--border); border-radius: 4px; color: var(--text); font-size: 13px; font-family: var(--font-mono); padding: 10px; resize: vertical; min-height: 60px; } .settings-textarea:focus { outline: 1px solid var(--accent); border-color: var(--accent); } .settings-helper { font-size: 12px; color: var(--text-muted); font-style: italic; } ``` **Step 3: Populate template on settings open** Inside `openSettings`, in the `loadServerSettings().then(...)` callback, add: ```javascript // New Session tab var templateEl = $('setting-template'); if (templateEl) templateEl.value = ss.new_session_template || 'tmux new-session -d -s {name}'; ``` **Step 4: Bind template change handlers in bindStaticEventListeners** Add to `bindStaticEventListeners`: ```javascript // New Session tab: save template on blur (not every keystroke) var templateEl = $('setting-template'); if (templateEl) { var _templateDebounce = null; templateEl.addEventListener('input', function() { clearTimeout(_templateDebounce); _templateDebounce = setTimeout(function() { patchServerSetting('new_session_template', templateEl.value); }, 500); }); } on($('setting-template-reset'), 'click', function() { var templateEl = $('setting-template'); if (templateEl) templateEl.value = 'tmux new-session -d -s {name}'; patchServerSetting('new_session_template', 'tmux new-session -d -s {name}'); }); ``` **Step 5: Run tests** ```bash cd muxplex && python -m pytest muxplex/tests/ -v ``` Expected: All tests pass. **Step 6: Commit** ```bash git add -A && git commit -m "feat: add New Session settings tab with template textarea" ``` --- ### Task 4: Header `+` button with inline name input **Files:** - Modify: `muxplex/frontend/app.js` - Modify: `muxplex/frontend/style.css` **Step 1: Add new session creation function** In `muxplex/frontend/app.js`, add after the server settings functions: ```javascript // ─── New session creation ──────────────────────────────────────────────────── /** * Show an inline name input replacing the given button element. * On Enter: call createNewSession(). On Escape: cancel and restore button. * @param {HTMLElement} btn - the button to replace with input */ function showNewSessionInput(btn) { if (!btn) return; var container = btn.parentElement; if (!container) return; // Create inline input var input = document.createElement('input'); input.type = 'text'; input.className = 'new-session-input'; input.placeholder = 'Session name\u2026'; input.setAttribute('autocomplete', 'off'); input.setAttribute('spellcheck', 'false'); // Replace button with input btn.style.display = 'none'; container.insertBefore(input, btn); input.focus(); function cleanup() { input.remove(); btn.style.display = ''; } input.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); var name = input.value.trim(); if (name) { cleanup(); createNewSession(name); } } else if (e.key === 'Escape') { e.preventDefault(); cleanup(); } }); // Cancel on blur (click away) input.addEventListener('blur', function() { // Delay to allow click handlers to fire first setTimeout(cleanup, 150); }); } /** * Create a new session by POSTing to /api/sessions. * Shows toast, triggers poll refresh, auto-opens if setting enabled. * @param {string} name * @returns {Promise} */ async function createNewSession(name) { try { var res = await api('POST', '/api/sessions', { name: name }); var data = await res.json(); showToast("Session '" + data.name + "' created"); // Trigger immediate poll refresh to pick up the new session await pollSessions(); // Auto-open if setting enabled var ss = _serverSettings || await loadServerSettings(); if (ss.auto_open_created !== false) { // Small delay to let tmux create the session setTimeout(function() { openSession(data.name); }, 500); } } catch (err) { showToast(err.message || 'Failed to create session'); } } ``` **Step 2: Add CSS for inline input** Append to `muxplex/frontend/style.css`: ```css /* ============================================================ New session inline input ============================================================ */ .new-session-input { background: var(--bg); border: 1px solid var(--accent); border-radius: 4px; color: var(--text); font-size: 13px; font-family: var(--font-ui); padding: 4px 10px; width: 180px; outline: none; } .new-session-input::placeholder { color: var(--text-dim); } ``` **Step 3: Bind the header `+` button** Add to `bindStaticEventListeners`: ```javascript // New session: header + button on($('new-session-btn'), 'click', function() { showNewSessionInput($('new-session-btn')); }); ``` **Step 4: Add exports** Add to `module.exports`: ```javascript showNewSessionInput, createNewSession, ``` **Step 5: Run tests** ```bash cd muxplex && python -m pytest muxplex/tests/ -v ``` Expected: All tests pass. **Step 6: Commit** ```bash git add -A && git commit -m "feat: add header + button with inline name input for session creation" ``` --- ### Task 5: Sidebar `+ New` sticky footer **Files:** - Modify: `muxplex/frontend/index.html` - Modify: `muxplex/frontend/app.js` - Modify: `muxplex/frontend/style.css` **Step 1: Add sidebar footer HTML** In `muxplex/frontend/index.html`, add a footer inside the `#session-sidebar` div, after `#sidebar-list`: ```html
``` **Step 2: Add sidebar footer CSS** Append to `muxplex/frontend/style.css`: ```css .sidebar-footer { padding: 8px; border-top: 1px solid var(--border-subtle); flex-shrink: 0; } .sidebar-new-btn { width: 100%; background: none; border: 1px dashed var(--border); border-radius: 4px; color: var(--text-dim); font-size: 12px; font-family: var(--font-ui); padding: 8px; cursor: pointer; transition: color var(--t-fast), border-color var(--t-fast); } .sidebar-new-btn:hover { color: var(--text); border-color: var(--text-muted); } ``` **Step 3: Bind sidebar button** Add to `bindStaticEventListeners`: ```javascript // New session: sidebar footer button on($('sidebar-new-session-btn'), 'click', function() { showNewSessionInput($('sidebar-new-session-btn')); }); ``` **Step 4: Run tests** ```bash cd muxplex && python -m pytest muxplex/tests/ -v ``` Expected: All tests pass. **Step 5: Commit** ```bash git add -A && git commit -m "feat: add sidebar + New sticky footer with inline input" ``` --- ### Task 6: Mobile FAB **Files:** - Modify: `muxplex/frontend/index.html` - Modify: `muxplex/frontend/app.js` - Modify: `muxplex/frontend/style.css` **Step 1: Add FAB HTML** In `muxplex/frontend/index.html`, add before the toast element: ```html ``` **Step 2: Add FAB CSS** Append to `muxplex/frontend/style.css`: ```css /* ============================================================ Mobile FAB (new session) ============================================================ */ .new-session-fab { display: none; /* hidden by default, shown on mobile */ position: fixed; bottom: 16px; right: 16px; width: 56px; height: 56px; border-radius: 50%; background: var(--accent); color: var(--bg); border: none; font-size: 28px; font-weight: 300; line-height: 1; cursor: pointer; z-index: 100; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); transition: transform var(--t-fast), box-shadow var(--t-fast); } .new-session-fab:active { transform: scale(0.95); } @media (max-width: 959px) { .new-session-fab { display: flex; align-items: center; justify-content: center; } /* Hide the header + button on mobile since FAB replaces it */ #new-session-btn { display: none; } } ``` **Step 3: Bind FAB click** Add to `bindStaticEventListeners`: ```javascript // New session: mobile FAB on($('new-session-fab'), 'click', function() { showNewSessionInput($('new-session-fab')); }); ``` **Step 4: Hide FAB when in fullscreen view** In `openSession`, add after switching views: ```javascript var fab = $('new-session-fab'); if (fab) fab.classList.add('hidden'); ``` In `closeSession`, restore the FAB: ```javascript var fab = $('new-session-fab'); if (fab) fab.classList.remove('hidden'); ``` **Step 5: Run tests** ```bash cd muxplex && python -m pytest muxplex/tests/ -v ``` Expected: All tests pass. **Step 6: Commit** ```bash git add -A && git commit -m "feat: add mobile FAB for new session creation" ``` --- ### Task 7: Apply settings effects — font size, grid columns, hover delay, hidden sessions, sort order **Files:** - Modify: `muxplex/frontend/app.js` - Modify: `muxplex/frontend/terminal.js` **Step 1: Font size affects terminal.js** In `muxplex/frontend/terminal.js`, update the `createTerminal` function to read font size from localStorage: Replace the hardcoded fontSize in `createTerminal()`: ```javascript // Read font size from display settings (localStorage) var storedFontSize = 14; try { var raw = localStorage.getItem('muxplex.display'); if (raw) { var parsed = JSON.parse(raw); if (parsed.fontSize) storedFontSize = parsed.fontSize; } } catch (_) {} const mobile = window.innerWidth < 600; _term = new window.Terminal({ cursorBlink: true, fontSize: mobile ? Math.min(storedFontSize, 12) : storedFontSize, fontFamily: "'SF Mono', 'Fira Code', Consolas, monospace", // ... rest unchanged }); ``` **Step 2: Hidden sessions filter in renderGrid** In `muxplex/frontend/app.js`, update `renderGrid` to filter hidden sessions. At the top of the function, before computing `ordered`: ```javascript // Filter hidden sessions (server settings) var hidden = (_serverSettings && _serverSettings.hidden_sessions) || []; var visible = sessions; if (hidden.length > 0) { visible = sessions.filter(function(s) { return hidden.indexOf(s.name) === -1; }); } ``` Then use `visible` instead of `sessions` for the rest of the function. Update the empty-state check and the `ordered` computation: ```javascript if (!visible || visible.length === 0) { if (grid) grid.innerHTML = ''; if (emptyState) emptyState.classList.remove('hidden'); return; } if (emptyState) emptyState.classList.add('hidden'); const mobile = isMobile(); const ordered = mobile ? sortByPriority(visible) : visible; ``` **Step 3: Sort order support in renderGrid** Update `renderGrid` to apply sort order from server settings. After the hidden filter, before the mobile check: ```javascript // Apply sort order from server settings var sortOrder = (_serverSettings && _serverSettings.sort_order) || 'manual'; if (sortOrder === 'alphabetical') { visible = visible.slice().sort(function(a, b) { return (a.name || '').localeCompare(b.name || ''); }); } // 'recent' and 'manual' use the server-provided order (which is already the default) ``` **Step 4: Hidden sessions filter in renderSidebar** In `renderSidebar`, apply the same hidden filter: ```javascript var hidden = (_serverSettings && _serverSettings.hidden_sessions) || []; var visible = sessions; if (hidden.length > 0) { visible = sessions.filter(function(s) { return hidden.indexOf(s.name) === -1; }); } ``` Use `visible` instead of `sessions` for the sidebar rendering. **Step 5: Load server settings on page init** In the `DOMContentLoaded` handler, after `startPolling()`, add a call to load server settings: ```javascript restoreState() .then(() => { startPolling(); startHeartbeat(); requestNotificationPermission(); loadServerSettings(); // <-- add this line bindStaticEventListeners(); }) ``` **Step 6: Run tests** ```bash cd muxplex && python -m pytest muxplex/tests/ -v ``` Expected: All tests pass. **Step 7: Commit** ```bash git add -A && git commit -m "feat: apply settings effects — font size, grid columns, hidden sessions, sort order" ``` --- ### Task 8: Frontend tests for settings **Files:** - Modify: `muxplex/tests/test_frontend_html.py` - Modify: `muxplex/tests/test_frontend_css.py` **Step 1: Add HTML tests for settings elements** Add to `muxplex/tests/test_frontend_html.py`: ```python def test_html_settings_dialog() -> None: """id=settings-dialog, settings-backdrop, settings-btn.""" soup = _SOUP for id_ in ("settings-dialog", "settings-backdrop", "settings-btn"): assert soup.find(id=id_), f"Missing element with id='{id_}'" def test_html_settings_tabs() -> None: """Settings dialog contains four tab buttons: display, sessions, notifications, new-session.""" soup = _SOUP tabs = soup.select(".settings-tab") tab_names = [t.get("data-tab") for t in tabs] assert "display" in tab_names assert "sessions" in tab_names assert "notifications" in tab_names assert "new-session" in tab_names def test_html_display_tab_controls() -> None: """Display tab has font-size, hover-delay, and grid-columns selects.""" soup = _SOUP for id_ in ("setting-font-size", "setting-hover-delay", "setting-grid-columns"): assert soup.find(id=id_), f"Missing element with id='{id_}'" def test_html_new_session_btn() -> None: """id=new-session-btn, new-session-fab exist.""" soup = _SOUP assert soup.find(id="new-session-btn"), "Missing element with id='new-session-btn'" assert soup.find(id="new-session-fab"), "Missing element with id='new-session-fab'" def test_html_sidebar_new_session_btn() -> None: """id=sidebar-new-session-btn exists inside the sidebar.""" soup = _SOUP assert soup.find(id="sidebar-new-session-btn"), "Missing element with id='sidebar-new-session-btn'" def test_html_sessions_tab_controls() -> None: """Sessions tab has default-session, sort-order selects and checkboxes.""" soup = _SOUP for id_ in ("setting-default-session", "setting-sort-order", "setting-window-size-largest", "setting-auto-open"): assert soup.find(id=id_), f"Missing element with id='{id_}'" def test_html_notifications_tab_controls() -> None: """Notifications tab has bell-sound checkbox and permission button.""" soup = _SOUP assert soup.find(id="setting-bell-sound"), "Missing id='setting-bell-sound'" assert soup.find(id="notification-request-btn"), "Missing id='notification-request-btn'" def test_html_new_session_tab_controls() -> None: """New Session tab has template textarea and reset button.""" soup = _SOUP assert soup.find(id="setting-template"), "Missing id='setting-template'" assert soup.find(id="setting-template-reset"), "Missing id='setting-template-reset'" ``` **Step 2: Add CSS tests for settings styles** Add to `muxplex/tests/test_frontend_css.py`: ```python def test_css_settings_dialog(): css = read_css() assert ".settings-dialog" in css assert ".settings-tabs" in css assert ".settings-tab--active" in css assert ".settings-content" in css assert ".settings-field" in css assert ".settings-select" in css def test_css_header_btn(): css = read_css() assert ".header-btn" in css assert ".header-actions" in css def test_css_new_session_fab(): css = read_css() assert ".new-session-fab" in css def test_css_new_session_input(): css = read_css() assert ".new-session-input" in css def test_css_settings_textarea(): css = read_css() assert ".settings-textarea" in css def test_css_sidebar_footer(): css = read_css() assert ".sidebar-footer" in css assert ".sidebar-new-btn" in css ``` **Step 3: Run all tests** ```bash cd muxplex && python -m pytest muxplex/tests/ -v ``` Expected: All tests pass (both old and new). **Step 4: Commit** ```bash git add -A && git commit -m "test: add frontend tests for settings dialog and new session UI" ``` --- ## Phase 2 Complete After completing all 8 tasks, the full settings feature is done: 1. **Sessions tab** — default session, sort order, hidden sessions, window-size largest, auto-open 2. **Notifications tab** — bell sound toggle, desktop notification permission button 3. **New Session tab** — command template textarea with reset button 4. **Header `+` button** — inline name input, creates session via API 5. **Sidebar `+ New`** — sticky footer with same inline input 6. **Mobile FAB** — 56px floating action button, <960px only 7. **Settings effects applied** — font size, grid columns, hover delay, hidden sessions, sort order 8. **Frontend tests** — HTML structure and CSS coverage for all new elements ### Open questions (from design doc — decide during implementation): 1. Hidden sessions: completely removed from grid/sidebar (current implementation) 2. Session name validation: accepts anything the user types (tmux will reject invalid names) 3. Settings pre-auth: not accessible from login page (v1)