Skip to content

Commit 914ea05

Browse files
committed
feat(oracle): header version badge + persistent UI preferences (hub parity)
/api/health now carries the C3 version (_c3_version, same source as the Discovery API) and the dashboard header shows a v<version> badge next to the logo, mirroring the hub topbar. The active tab persists to config on every switch (new ui_last_tab key) and is restored on load via restoreLastTab() after init — using the hub's save-on-change config pattern rather than localStorage so preferences follow the server, not the browser. Note: the header theme toggle already POSTed its preference, but was silently 401-broken until the v2.47.0 session cookie — it now actually sticks. Bundle re-verified with esbuild; live smoke: version 2.49.0 in health, cookie-authed ui_last_tab save + readback green.
1 parent 77dde11 commit 914ea05

10 files changed

Lines changed: 46 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
### Added
10+
11+
- **Oracle version badge** (hub-topbar parity): `/api/health` now carries the
12+
C3 `version`, and the dashboard header shows `v<version>` next to the logo.
13+
- **Persistent Oracle UI preferences** (hub parity): the active tab is saved
14+
to config on every switch (`ui_last_tab`) and restored on load; the header
15+
theme toggle's persistence — silently broken by a 401 before the v2.47.0
16+
session cookie — now actually sticks.
17+
918
### Removed
1019

1120
- **Oracle `/legacy` route and `oracle.html`.** The frozen pre-bundle

oracle-guide/api-reference.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Returns Oracle status and Ollama cloud availability.
2222
{
2323
"status": "ok",
2424
"service": "c3-oracle",
25+
"version": "2.49.0",
2526
"model": "gemma4:31b-cloud",
2627
"ollama_available": true,
2728
"model_verified": true,

oracle-guide/configuration.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ Oracle stores its configuration at `~/.c3/oracle/config.json`.
2727
| `digest_notify_file` | `""` | JSONL sink appended after each scheduled digest (`""` = disabled) |
2828
| `digest_retention_days` | `14` | Prune stored digests older than this |
2929
| `auto_open_browser` | `true` | Open browser on startup |
30-
| `theme` | `dark` | UI theme (`dark` or `light`) |
30+
| `theme` | `dark` | UI theme (`dark` or `light`) — persisted automatically when toggled in the header |
31+
| `ui_last_tab` | `chat` | Last active dashboard tab — persisted automatically on tab switch, restored on load |
3132
| `max_facts_per_analysis` | `100` | Max facts sent to LLM per analysis |
3233
| `insight_confidence_threshold` | `0.5` | Minimum confidence to store an insight |
3334
| `log_level` | `INFO` | Logging level (DEBUG, INFO, WARNING, ERROR) |

oracle/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"digest_retention_days": 14, # prune stored digests
3333
"auto_open_browser": True,
3434
"theme": "dark",
35+
"ui_last_tab": "chat", # persisted UI preference (hub parity)
3536
"max_facts_per_analysis": 100,
3637
"insight_confidence_threshold": 0.5,
3738
"log_level": "INFO",

oracle/oracle_server.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ def api_health():
262262
return jsonify({
263263
"status": "ok",
264264
"service": "c3-oracle",
265+
"version": _c3_version(),
265266
"model": _cfg.get("model", "gemma4:31b-cloud"),
266267
"ollama_available": ollama_ok,
267268
"model_verified": _model_verified,

oracle/oracle_ui.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
}
4545
.logo { font-size: 17px; font-weight: 700; letter-spacing: .5px; display: flex; align-items: center; gap: 10px; }
4646
.logo span { color: var(--accent); }
47+
.version-badge { font-size: 10px; font-weight: 500; font-family: monospace; color: var(--text2); background: var(--bg3); border: 1px solid var(--border); border-radius: 4px; padding: 1px 6px; }
4748
.header-right { display: flex; align-items: center; gap: 8px; }
4849
.badge { background: var(--bg3); border: 1px solid var(--border); border-radius: 9999px; padding: 3px 12px; font-size: 11px; color: var(--text2); }
4950
.badge .dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; margin-right: 5px; }
@@ -797,6 +798,7 @@
797798
<header>
798799
<div class="logo">
799800
<span>Oracle</span> Memory Agent
801+
<span class="version-badge" id="oracleVersion" style="display:none"></span>
800802
<div class="activity-spinner" id="activitySpinner"></div>
801803
<div class="activity-label" id="activityLabel"></div>
802804
</div>

oracle/ui/app.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
// ═══════════════════════════════════════════════════════════
55
(async function init() {
66
await Promise.all([refreshHeader(), loadProjects(), loadInsights(), loadSuggestions(), loadSettings(), loadDiscoveryKey(), chatLoadConversations(), chatLoadCommands()]);
7+
// Restore persisted UI preferences (loadSettings cached the config).
8+
restoreLastTab(window.oracleConfig || {});
79
// Auto-refresh every 60s
810
setInterval(() => { refreshHeader(); loadProjects(); }, 60000);
911
})();

oracle/ui/header.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ async function refreshHeader() {
2424
api('/api/review/status').catch(() => ({})),
2525
]);
2626

27+
// Version badge (hub topbar parity)
28+
if (health.version) {
29+
const vEl = document.getElementById('oracleVersion');
30+
if (vEl) { vEl.textContent = 'v' + health.version; vEl.style.display = ''; }
31+
}
32+
2733
// Ollama
2834
ollamaOk = !!health.ollama_available;
2935
model = health.model || '?';

oracle/ui/theme_tabs.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,18 @@ function toggleTheme() {
2525
// ═══════════════════════════════════════════════════════════
2626
// ── Tab switching ──
2727
// ═══════════════════════════════════════════════════════════
28+
let _restoringTab = false;
29+
30+
function restoreLastTab(cfg) {
31+
// Re-open the tab from the persisted UI preference (hub parity).
32+
const last = (cfg && cfg.ui_last_tab) || 'chat';
33+
if (last === 'chat') return; // chat is the default active tab
34+
const tab = document.querySelector(`.tab[data-tab="${last}"]`);
35+
if (!tab) return;
36+
_restoringTab = true;
37+
try { tab.click(); } finally { _restoringTab = false; }
38+
}
39+
2840
document.querySelectorAll('.tab').forEach(tab => {
2941
tab.addEventListener('click', () => {
3042
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
@@ -46,6 +58,10 @@ document.querySelectorAll('.tab').forEach(tab => {
4658
if (tab.dataset.tab === 'activity' && !window._actLoaded) {
4759
loadActivity(); window._actLoaded = true;
4860
}
61+
// Persist the UI preference (hub parity) — skip during restore.
62+
if (!_restoringTab) {
63+
api('/api/config', { method: 'POST', body: { ui_last_tab: tab.dataset.tab } }).catch(() => {});
64+
}
4965
});
5066
});
5167

tests/test_oracle_ui_bundle.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,12 @@ def test_root_serves_bundle_with_cookie(self):
7474
def test_legacy_route_removed(self):
7575
self.assertEqual(self.client.get("/legacy").status_code, 404)
7676

77-
def test_health_unaffected(self):
78-
self.assertEqual(self.client.get("/api/health").status_code, 200)
77+
def test_health_carries_version(self):
78+
r = self.client.get("/api/health")
79+
self.assertEqual(r.status_code, 200)
80+
# Header version badge (hub parity) reads this field.
81+
version = r.get_json().get("version", "")
82+
self.assertRegex(version, r"^\d+\.\d+")
7983

8084

8185
if __name__ == "__main__":

0 commit comments

Comments
 (0)