Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions console/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,54 @@
<nav class="tabs" id="tabs">
<button class="tab is-active" data-target="log">Activity</button>
<button class="tab" data-target="mcpio">MCP · oab-mcp</button>
<button class="tab" data-target="config">Config</button>
</nav>
<div id="log" class="log pane"></div>
<div id="mcpio" class="log pane mcpio" hidden></div>
<div id="config" class="pane config" hidden>
<form id="config-form" class="config-form" autocomplete="off">
<label
>Cluster
<input
id="cfg-cluster"
name="cluster"
type="text"
placeholder="oab"
spellcheck="false"
/>
</label>
<label
>AWS profile
<input
id="cfg-profile"
name="profile"
type="text"
placeholder="e.g. brettchien"
spellcheck="false"
/>
</label>
<label
>AWS region
<input
id="cfg-region"
name="region"
type="text"
placeholder="e.g. ap-east-2"
spellcheck="false"
/>
</label>
<div class="config-actions">
<button type="submit" id="cfg-save">Save &amp; reload core</button>
<span class="config-status" id="cfg-status"></span>
</div>
<p class="config-hint">
Pins the oab-mcp sidecar to this profile / region (a hermetic env, so
no ambient AWS credentials leak in) and reloads the core. Leaving
profile or region blank means the sidecar has no AWS identity — an
explicit auth error, not a silent drift.
</p>
</form>
</div>
</section>
<section id="roster" class="roster-wrap"></section>
</main>
Expand Down
94 changes: 94 additions & 0 deletions console/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Config tab: read the current oab-mcp target from the backend, let the user pin
// cluster / profile / region, and on save persist + reload the core onto it.
// Only meaningful inside the Tauri shell; the browser build disables the form.
//
// The backend target is provider-tagged (`McpTarget`, config.rs). Today the only
// variant is ECS, so the form maps 1:1 to `{ provider: "ecs", cluster, profile,
// region }`; a future provider would add its own fields/section.

export interface EcsTarget {
provider: "ecs";
cluster: string;
profile?: string | null;
region?: string | null;
}
export type McpTarget = EcsTarget;

type Invoke = <T>(cmd: string, args?: Record<string, unknown>) => Promise<T>;

function tauriInvoke(): Invoke | null {
const t = (globalThis as { __TAURI__?: { core?: { invoke?: Invoke } } }).__TAURI__;
return t?.core?.invoke ?? null;
}

// Tauri command rejections arrive as plain strings, not Error objects.
function errText(e: unknown): string {
return e instanceof Error ? e.message : String(e);
}

export interface ConfigHooks {
/** Fired with the target loaded from disk at startup. */
onLoaded?: (t: McpTarget) => void;
/** Fired after a successful save + core reload. */
onSaved?: (t: McpTarget) => void;
}

export function initConfigTab(hooks: ConfigHooks = {}): void {
const form = document.getElementById("config-form") as HTMLFormElement | null;
const profile = document.getElementById("cfg-profile") as HTMLInputElement | null;
const region = document.getElementById("cfg-region") as HTMLInputElement | null;
const cluster = document.getElementById("cfg-cluster") as HTMLInputElement | null;
const save = document.getElementById("cfg-save") as HTMLButtonElement | null;
const status = document.getElementById("cfg-status");
if (!form || !profile || !region || !cluster) return;

const setStatus = (msg: string, cls = ""): void => {
if (status) {
status.textContent = msg;
status.className = cls ? `config-status ${cls}` : "config-status";
}
};

const invoke = tauriInvoke();
if (!invoke) {
// Browser build — no core to configure.
setStatus("browser build — config unavailable");
for (const el of form.querySelectorAll<HTMLInputElement | HTMLButtonElement>(
"input, button",
)) {
el.disabled = true;
}
return;
}

// Populate the form from the persisted (or env-seeded) target.
invoke<McpTarget>("mcp_target_get")
.then((t) => {
cluster.value = t.cluster ?? "";
profile.value = t.profile ?? "";
region.value = t.region ?? "";
hooks.onLoaded?.(t);
})
.catch((e) => setStatus(`load failed: ${errText(e)}`, "err"));

form.addEventListener("submit", async (ev) => {
ev.preventDefault();
const target: McpTarget = {
provider: "ecs",
cluster: cluster.value.trim() || "oab",
profile: profile.value.trim() || null,
region: region.value.trim() || null,
};
if (save) save.disabled = true;
setStatus("saving & reloading core…");
try {
await invoke("mcp_target_set", { target });
setStatus("saved — core reloaded", "ok");
hooks.onSaved?.(target);
} catch (e) {
setStatus(`save failed: ${errText(e)}`, "err");
} finally {
if (save) save.disabled = false;
}
});
}
4 changes: 4 additions & 0 deletions console/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defaultSource } from "./source";
import { initConfigTab } from "./config";
import {
renderRoster,
renderIdentity,
Expand Down Expand Up @@ -750,6 +751,9 @@ async function boot(): Promise<void> {
note("info", `app: polling cluster "${activeCluster}" every ${POLL_MS / 1000}s`);
setupUpdater();
await startCore();
// Config tab: pin the oab-mcp target (cluster/profile/region → hermetic env);
// on save the backend reloads the core, so refresh the roster after.
initConfigTab({ onSaved: () => void tick() });
void refreshConfig();
void refreshIdentity();
void refreshRemote();
Expand Down
60 changes: 60 additions & 0 deletions console/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -810,3 +810,63 @@ button.act:disabled {
border-color: var(--s-unhealthy);
color: var(--s-unhealthy);
}

/* Config tab — pin the oab-mcp target (cluster / profile / region). */
.pane.config {
padding: 16px;
overflow: auto;
}
.config-form {
display: flex;
flex-direction: column;
gap: 12px;
max-width: 420px;
}
.config-form label {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 12px;
color: var(--muted);
}
.config-form input {
padding: 6px 8px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg);
color: var(--text);
font: inherit;
}
.config-actions {
display: flex;
align-items: center;
gap: 12px;
}
.config-actions button {
padding: 6px 12px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--panel);
color: var(--text);
cursor: pointer;
}
.config-actions button:disabled {
opacity: 0.5;
cursor: default;
}
.config-status {
font-size: 12px;
color: var(--muted);
}
.config-status.ok {
color: var(--ok);
}
.config-status.err {
color: var(--s-unhealthy);
}
.config-hint {
margin: 0;
font-size: 12px;
line-height: 1.5;
color: var(--muted);
}
Loading