# Multi-Device Federation Phase 3: Remote Auth + Terminal + Unreachable States
> **Execution:** Use the subagent-driven-development workflow to implement this plan.
**Goal:** Make remote sessions fully interactive — detect auth requirements, present login flow, show unreachable placeholders, and connect terminals to remote instances.
**Architecture:** Phase 2 left sources in three status states (`authenticated | auth_required | unreachable`) but only rendered sessions for `authenticated` sources. Phase 3 adds rendering for the other two states (auth-required tiles with login buttons, offline placeholder tiles with last-seen timestamps), implements the popup login flow, makes `terminal.js` connect WebSockets to remote origins, and updates `openSession`/`closeSession` to route API calls to the correct instance.
**Tech Stack:** Vanilla JS (no framework), CSS custom properties, Node.js `--test` runner with `assert`, `window.open()` for login popups, cross-origin `fetch` with `credentials: "include"`.
---
## Conventions (read this before you start)
**File paths are relative to the repo root** (`muxplex/`).
**How to run tests:**
```bash
cd muxplex && node --test muxplex/frontend/tests/test_app.mjs
cd muxplex && node --test muxplex/frontend/tests/test_terminal.mjs
```
**Test pattern:** Every test file uses `.mjs` extension, imports via `createRequire`, and mocks `globalThis.document`, `globalThis.fetch`, `globalThis.window`, etc. before requiring the module. See the top of `muxplex/frontend/tests/test_app.mjs` for the full mock setup.
**CommonJS export shim:** `app.js` ends with a `module.exports = { ... }` block wrapped in `if (typeof module !== 'undefined' && module.exports)`. Any new function you want to test **must** be added to that exports object at the bottom of `app.js` (around line 1660).
**CSS design tokens:** The file `muxplex/frontend/style.css` defines tokens on `:root`. Key ones: `--bg: #0D1117`, `--accent: #00D9F5`, `--text-dim: #4A5060`, `--text-muted: #8E95A3`, `--border: #2A3040`, `--err: #f85149`, `--warn: #d29922`, `--ok: #3fb950`.
**Phase 2 assumptions:** The plan assumes Phase 2 left these in place:
- `_sources` array with `{ url, name, type, status, lastSeenAt, backoff }` per source
- `pollSessions()` does `Promise.all` across sources, sets `source.status` to `authenticated | auth_required | unreachable`
- Each session object has `deviceName`, `sourceUrl`, `sessionKey` fields
- Device badge HTML in `buildTileHTML` (something like `Laptop`)
- `_sources` is accessible from tests via a `_setSources` or `_getSources` test helper
If any of these are named slightly differently in the actual Phase 2 code, adjust accordingly — the logic is the same.
---
### Task 1: CSS for auth-required and offline tile states
**Files:**
- Modify: `muxplex/frontend/style.css`
Phase 3 tiles need two new visual states: a greyed-out "offline" tile and an "auth required" tile with a login button. Add these styles to the end of `style.css`, before the `@media (prefers-reduced-motion)` block.
**Step 1: Add the CSS**
Append these rules to `muxplex/frontend/style.css`, just before the `/* Reduced Motion */` media query (currently around line 798):
```css
/* ============================================================
Federation: source status tiles (auth-required + offline)
============================================================ */
.source-tile {
height: var(--tile-height);
background: var(--bg-tile);
border: 1px solid var(--border);
border-radius: 4px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
text-align: center;
padding: 24px;
}
/* Offline / unreachable device tile */
.source-tile--offline {
opacity: 0.45;
border-style: dashed;
}
.source-tile--offline .source-tile__name {
color: var(--text-dim);
}
.source-tile--offline .source-tile__badge {
background: var(--err);
color: #fff;
font-size: 10px;
font-weight: 700;
padding: 2px 8px;
border-radius: 10px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.source-tile--offline .source-tile__last-seen {
font-size: 11px;
color: var(--text-dim);
}
/* Auth-required device tile */
.source-tile--auth {
border-color: var(--warn);
border-style: dashed;
}
.source-tile__name {
font-size: 15px;
font-weight: 600;
color: var(--text);
}
.source-tile__login-btn {
background: var(--accent);
color: var(--bg);
border: none;
border-radius: 4px;
padding: 8px 20px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: opacity var(--t-fast);
}
.source-tile__login-btn:hover {
opacity: 0.85;
}
.source-tile__login-btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.source-tile__hint {
font-size: 11px;
color: var(--text-muted);
}
```
**Step 2: Verify the CSS file is valid**
Open the file and visually confirm the new classes are syntactically correct (no unclosed braces):
```bash
cd muxplex && grep -c 'source-tile' muxplex/frontend/style.css
```
Expected: a count of ~20+ (the lines we just added).
**Step 3: Commit**
```bash
cd muxplex && git add muxplex/frontend/style.css && git commit -m "feat(federation): add CSS for auth-required and offline source tiles"
```
---
### Task 2: `buildAuthTileHTML` — render an auth-required tile
**Files:**
- Modify: `muxplex/frontend/app.js` (add function + export)
- Modify: `muxplex/frontend/tests/test_app.mjs` (add tests)
This function builds the HTML for a single auth-required source tile. It shows the device name and a "Log in" button.
**Step 1: Write the failing tests**
Add these tests to the end of `muxplex/frontend/tests/test_app.mjs` (before the final blank lines):
```javascript
// --- buildAuthTileHTML ---
test('buildAuthTileHTML is exported as a function', () => {
assert.strictEqual(typeof app.buildAuthTileHTML, 'function');
});
test('buildAuthTileHTML returns article with source-tile--auth class', () => {
const html = app.buildAuthTileHTML({ name: 'Workstation', url: 'http://work:8088' });
assert.ok(html.includes('source-tile--auth'), 'must have source-tile--auth class');
assert.ok(html.startsWith(' element');
});
test('buildAuthTileHTML includes device name', () => {
const html = app.buildAuthTileHTML({ name: 'Dev Server', url: 'http://dev:8088' });
assert.ok(html.includes('Dev Server'), 'must include the device name');
});
test('buildAuthTileHTML includes login button with data-url attribute', () => {
const html = app.buildAuthTileHTML({ name: 'Dev', url: 'http://dev:8088' });
assert.ok(html.includes('source-tile__login-btn'), 'must have login button');
assert.ok(html.includes('data-url="http://dev:8088"'), 'button must have data-url');
});
test('buildAuthTileHTML escapes HTML in device name', () => {
const html = app.buildAuthTileHTML({ name: '', url: 'http://x' });
assert.ok(!html.includes('