chore: import upstream snapshot with attribution
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:40:33 +08:00
commit e071084ebe
982 changed files with 160368 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
// Minimal JS for reactive form submissions
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('form[data-reactive]')?.forEach(function(form) {
form.addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(form);
const params = {};
for (const [key, value] of formData.entries()) {
params[key] = value;
}
const action = form.getAttribute('action');
const method = form.getAttribute('method') || 'POST';
try {
const resp = await fetch(action, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params)
});
const data = await resp.json();
// Find or create a response container
let respDiv = form.querySelector('.js-response');
if (!respDiv) {
respDiv = document.createElement('div');
respDiv.className = 'js-response';
form.appendChild(respDiv);
}
respDiv.innerHTML = '<pre>' + JSON.stringify(data, null, 2) + '</pre>';
} catch (err) {
alert('Error: ' + err);
}
});
});
});
+243
View File
@@ -0,0 +1,243 @@
body {
background: #fff;
color: #111;
font-family: 'Inter', 'Segoe UI', 'Arial', 'Helvetica Neue', Arial, sans-serif;
font-size: 15px;
margin: 0;
padding: 0;
line-height: 1.7;
}
header, nav, footer {
background: #fff;
color: #111;
padding: 1.2em 2em 1.2em 2em;
margin-bottom: 2em;
}
nav {
margin: 20px;
border-radius: 20px;
}
main {
max-width: 1400px;
margin: 0 auto;
padding: 2em 1em 3em 1em;
background: #fff;
margin-left: 100px; /* leave space for sidebar */
margin-right: 100px;
}
h1, h2, h3, h4, h5, h6 {
color: #111;
font-weight: 600;
margin-top: 2em;
margin-bottom: 0.5em;
letter-spacing: -0.01em;
}
h1 {
font-size: 2.2em;
margin-top: 0;
}
h2 {
font-size: 1.4em;
}
hr {
border: none;
border-top: 1px solid #222;
margin: 2em 0;
}
a {
color: #111;
text-decoration: none;
transition: background 0.2s;
}
a:hover {
font-weight: bold;
}
ul, ol {
margin: 1em 0 1em 2em;
padding: 0;
}
li {
margin-bottom: 0.5em;
}
pre, code {
background: #f7f7f7;
color: #111;
font-family: inherit;
font-size: 0.98em;
border-radius: 5px;
padding: 0.2em 0.4em;
}
pre {
padding: 1em;
overflow-x: auto;
border-radius: 0;
margin: 1.5em 0;
}
form {
background: #fff;
border: 1px solid #222;
padding: 1.5em 1.5em 1em 1.5em;
margin: 2em 0;
border-radius: 10px;
box-shadow: none;
}
input, select, textarea {
background: #fff;
color: #111;
border: 1px solid #222;
border-radius: 7px;
font-size: 1em;
padding: 0.5em 0.7em;
margin-bottom: 1em;
width: 100%;
box-sizing: border-box;
outline: none;
transition: border 0.2s;
}
input:focus, select:focus, textarea:focus {
border: 1.5px solid #111;
}
button, input[type="submit"], .button {
background: #fff;
color: #111;
border: 1.5px solid #111;
border-radius: 7px;
font-size: 1em;
padding: 0.5em 1.2em;
margin: 0.5em 0.2em 0.5em 0;
cursor: pointer;
font-family: inherit;
transition: background 0.2s, color 0.2s;
}
button:hover, input[type="submit"]:hover, .button:hover {
background: #111;
color: #fff;
}
.table, table {
width: 100%;
border-collapse: collapse;
background: #fff;
margin: 2em 0;
}
table th, table td {
border: none;
padding: 0.7em 1em;
text-align: left;
}
table th {
background: #f7f7f7;
color: #111;
font-weight: 600;
}
table tr:nth-child(even) {
background: #f7f7f7;
}
.no-bullets {
list-style: none;
margin: 0;
padding: 0;
}
.no-bullets li {
padding: 0.45em 0;
border-bottom: 1px solid #e0e0e0;
}
.no-bullets li:last-child {
border-bottom: none;
}
.copy-btn {
background: #fff;
color: #111;
border: 1px solid #222;
border-radius: 7px;
font-size: 0.95em;
padding: 0.2em 0.7em;
margin-left: 0.5em;
cursor: pointer;
transition: background 0.2s, color 0.2s;
}
.copy-btn:hover {
background: #111;
color: #fff;
}
.alert, .error, .success {
background: #fff;
color: #111;
border: 1px solid #222;
padding: 1em 1.5em;
margin: 2em 0;
border-radius: 10px;
}
::-webkit-scrollbar {
width: 8px;
background: #fff;
}
::-webkit-scrollbar-thumb {
background: #222;
}
@media (max-width: 800px) {
main {
max-width: 98vw;
padding: 1em 0.2em 2em 0.2em;
margin-left: 0;
}
}
/* Inline/unstyled form for delete button */
.form-inline, .form-plain {
display: inline;
background: none;
border: none;
padding: 0;
margin: 0;
box-shadow: none;
}
.form-inline input, .form-inline button, .form-plain input, .form-plain button {
margin: 0;
padding: 0.3em 1em;
border-radius: 7px;
font-size: 1em;
}
.delete-btn, .form-inline .delete-btn, .form-plain .delete-btn {
background: #fff;
color: #c00;
border: 1.5px solid #c00;
border-radius: 7px;
font-size: 1em;
padding: 0.3em 1em;
margin: 0 0.2em;
cursor: pointer;
font-family: inherit;
transition: background 0.2s, color 0.2s;
}
.delete-btn:hover {
background: #c00;
color: #fff;
}
#title {
text-decoration: none;
}
.log-link:hover {
font-weight: normal;
text-decoration: underline;
}
+32
View File
@@ -0,0 +1,32 @@
{{define "content"}}
<h2 class="text-2xl font-bold mb-4">API</h2>
<p style="background:#f8f8e8; border:1px solid #e0e0b0; padding:1em; margin-bottom:2em; font-size:1.05em; border-radius:6px;">
<b>Authentication Required:</b> Include an <b>Authorization: Bearer &lt;token&gt;</b> header with all <code>/api/...</code> requests.
Generate tokens on the <a href="/auth/tokens">Tokens page</a>.
</p>
{{range .Services}}
<h3 id="{{.Anchor}}" style="margin-top:2.5em; margin-bottom:0.8em; font-size:1.15em; font-weight:bold; border-bottom:2px solid #ddd; padding-bottom:0.4em;">{{.Name}}</h3>
{{if .Endpoints}}
{{range .Endpoints}}
<div style="margin-bottom:1.8em; padding:1.2em 1.4em; background:#fafbfc; border-radius:7px; border:1px solid #e5e5e5;">
<div style="display:flex; align-items:baseline; gap:1em; margin-bottom:0.6em;">
<span style="font-size:1.08em; font-weight:600;"><a href="{{.Path}}" class="micro-link">{{.Name}}</a></span>
<code style="font-size:0.92em; color:#666;">{{.Path}}</code>
</div>
<div style="display:flex; gap:2.5em; flex-wrap:wrap;">
<div style="flex:1; min-width:220px;">
<div style="font-weight:600; font-size:0.92em; color:#555; text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.3em;">Request</div>
{{.Params}}
</div>
<div style="flex:1; min-width:220px;">
<div style="font-weight:600; font-size:0.92em; color:#555; text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.3em;">Response</div>
{{.Response}}
</div>
</div>
</div>
{{end}}
{{else}}
<p style="color:#888;">No endpoints</p>
{{end}}
{{end}}
{{end}}
+15
View File
@@ -0,0 +1,15 @@
{{define "content"}}
<h2 class="text-2xl font-bold mb-4">Login</h2>
<form method="POST" action="/auth/login" style="max-width:340px; margin:2em 0;">
<div style="margin-bottom:1.2em;">
<input name="id" placeholder="Username" required style="width:100%; padding:0.7em;">
</div>
<div style="margin-bottom:1.2em;">
<input name="password" type="password" placeholder="Password" required style="width:100%; padding:0.7em;">
</div>
<button type="submit" style="width:100%; padding:0.7em;">Login</button>
</form>
{{if .Error}}
<div style="color:#c00; margin-top:1em;">{{.Error}}</div>
{{end}}
{{end}}
+87
View File
@@ -0,0 +1,87 @@
{{define "content"}}
<h2 class="text-2xl font-bold mb-4">Tokens</h2>
<table style="margin-bottom:2em;">
<thead>
<tr><th>ID</th><th>Type</th><th>Scopes</th><th>Metadata</th><th>Token</th><th>Delete</th></tr>
</thead>
<tbody>
{{range .Tokens}}
<tr>
<td>{{.ID}}</td>
<td>{{.Type}}</td>
<td>{{range .Scopes}}<code>{{.}}</code> {{end}}</td>
<td>
{{range $k, $v := .Metadata}}
{{if and (ne $k "password_hash") (ne $k "token")}}
<b>{{$k}}</b>: {{$v}}
{{end}}
{{end}}
</td>
<td style="max-width:320px; word-break:break-all;">
{{if .Token}}
<span class="obfuscated-token" data-token="{{.Token}}">
{{if .TokenSuffix}}
{{.TokenPrefix}}...{{.TokenSuffix}}
{{else}}
{{.Token}}
{{end}}
</span>
<button onclick="copyToken(this)" data-token="{{.Token}}" style="margin-left:0.5em;">Copy</button>
{{end}}
</td>
<td>
<form method="POST" action="/auth/tokens" style="display:inline; padding: 0; border: 0">
<input type="hidden" name="delete" value="{{.ID}}">
<button type="submit" onclick="return confirm('Delete token {{.ID}}?')">Delete</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
<h3 style="margin-bottom:1em;">Create Token</h3>
<form method="POST" action="/auth/tokens">
<input name="id" placeholder="Name/ID" required style="margin-right:1em;">
<select name="type" style="margin-right:1em;">
<option value="user">User</option>
<option value="admin">Admin</option>
<option value="service">Service</option>
</select>
<input name="scopes" placeholder="Scopes (comma separated)" style="margin-right:1em; width:260px;">
<button type="submit">Create</button>
</form>
<div style="background:#f9f9f9; border:1px solid #eee; padding:1.2em 1.5em; border-radius:6px; font-size:0.97em; line-height:1.6; margin-top:2em;">
<h4 style="margin-top:0;">Token Scopes</h4>
<p>Scopes define what a token is allowed to access. They work with the <a href="/auth/scopes">Scopes</a> page where you set what each endpoint requires.</p>
<table style="font-size:0.95em; margin:1em 0;">
<thead><tr><th>Scopes</th><th>What it means</th></tr></thead>
<tbody>
<tr><td><code>*</code></td><td>Full access — bypasses all scope checks (default for admin)</td></tr>
<tr><td><code>greeter</code></td><td>Can call any endpoint that requires the <code>greeter</code> scope</td></tr>
<tr><td><code>greeter, users</code></td><td>Can call endpoints requiring <code>greeter</code> or <code>users</code></td></tr>
<tr><td><code>admin</code></td><td>Can call endpoints requiring the <code>admin</code> scope</td></tr>
</tbody>
</table>
<p style="margin-bottom:0;">Scopes are just strings — you define them. Set the same string on a token and on an endpoint, and they match. See <a href="/auth/scopes">Scopes</a> for examples.</p>
</div>
<h4 style="margin-top:2em;">Using a Token</h4>
<pre style="background:#fff; border:1px solid #ddd; padding:1em; border-radius:4px; overflow-x:auto; font-size:0.93em;">
curl http://localhost:8080/api/greeter/Greeter/Hello \
-H "Authorization: Bearer &lt;token&gt;" \
-d '{"name": "World"}'</pre>
<script>
function copyToken(btn) {
const token = btn.getAttribute('data-token');
if (navigator.clipboard) {
navigator.clipboard.writeText(token);
btn.textContent = 'Copied!';
setTimeout(() => { btn.textContent = 'Copy'; }, 1200);
}
}
</script>
{{end}}
+40
View File
@@ -0,0 +1,40 @@
{{define "content"}}
<h2 class="text-2xl font-bold mb-4">User Accounts</h2>
<table style="margin-bottom:2em;">
<thead>
<tr><th>ID</th><th>Type</th><th>Scopes</th><th>Metadata</th><th>Delete</th></tr>
</thead>
<tbody>
{{range .Users}}
<tr>
<td>{{.ID}}</td>
<td>{{.Type}}</td>
<td>{{range .Scopes}}<code>{{.}}</code> {{end}}</td>
<td>
{{range $k, $v := .Metadata}}
{{if ne $k "password_hash"}}
<b>{{$k}}</b>: {{$v}}
{{end}}
{{end}}
</td>
<td>
<form method="POST" action="/auth/users" style="display:inline; padding: 0; border: 0">
<input type="hidden" name="delete" value="{{.ID}}">
<button type="submit" onclick="return confirm('Delete user {{.ID}}?')">Delete</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
<h3 style="margin-bottom:1em;">Create New User</h3>
<form method="POST" action="/auth/users">
<input name="id" placeholder="Username" required style="margin-right:1em;">
<input name="password" type="password" placeholder="Password" required style="margin-right:1em;">
<select name="type" style="margin-right:1em;">
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
<button type="submit">Create</button>
</form>
{{end}}
+58
View File
@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<title>Micro | {{.Title}}</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<div id="layout" style="display:flex; min-height:100vh;">
{{if not .HideSidebar}}
<nav id="sidebar" style="width:230px; background:#f5f5f5; padding:2em 1.5em 2em 2em; border:1px solid #eee;">
<h1 style="margin-bottom:1.2em;"><a href="/" id="title">Micro</a></h1>
{{if .User}}
<div style="margin-bottom:1.5em; font-size:1.05em;">
<span style="color:#888;">Logged in as</span>
<b>{{.User.ID}}</b>
<form method="POST" action="/auth/logout" style="margin-top:0.7em; display:block; background:none; box-shadow:none; padding:0; border:none;">
<button type="submit" style="padding:0.25em 0.8em; font-size:0.97em; border-radius:4px; margin:0; cursor:pointer;">Logout</button>
</form>
</div>
{{else if .AuthEnabled}}
<div style="margin-bottom:1.5em;">
<a href="/auth/login" class="micro-link">Login</a>
</div>
{{end}}
<ul class="no-bullets" style="padding-left:0; line-height:2.2;">
<li><a href="/" class="micro-link">&#127968; Home</a></li>
<li><a href="/agent" class="micro-link">&#129302; Agent</a></li>
<li><a href="/api" class="micro-link">&#128268; API</a></li>
<li><a href="/logs" class="micro-link">&#128220; Logs</a></li>
{{if .AuthEnabled}}
<li><a href="/auth/scopes" class="micro-link">&#128274; Scopes</a></li>
{{end}}
<li><a href="/services" class="micro-link">&#9881; Services</a></li>
<li><a href="/status" class="micro-link">&#128308; Status</a></li>
{{if .AuthEnabled}}
<li><a href="/auth/tokens" class="micro-link">&#128273; Tokens</a></li>
<li><a href="/auth/users" class="micro-link">&#128100; Users</a></li>
{{end}}
</ul>
{{if and .SidebarEndpoints .SidebarEndpointsEnabled}}
<hr style="margin:2em 0 1em 0;">
<div style="font-weight:bold; margin-bottom:0.5em;">API Endpoints</div>
<div style="max-height:40vh; overflow-y:auto; font-size:0.97em;">
{{range .SidebarEndpoints}}
<div style="margin-bottom:0.3em;"><a href="#{{.Anchor}}" class="micro-link">{{.Name}}</a></div>
{{end}}
</div>
{{end}}
</nav>
{{end}}
<main class="container" style="flex:1; min-width:0;">
{{template "content" .}}
</main>
</div>
</body>
</html>
+23
View File
@@ -0,0 +1,23 @@
{{define "content"}}
<h2>{{.ServiceName}}</h2>
<form action="/{{.Action}}" method="POST" data-reactive>
<h3 class="text-lg font-bold mb-2">{{.EndpointName}}</h3>
{{range .Inputs}}
<label class="block font-semibold">{{.Label}}</label>
<input name="{{.Name}}" placeholder="{{.Placeholder}}" class="border rounded px-2 py-1 mb-2 w-full" value="{{.Value}}">
{{end}}
<button class="micro-link mt-2" type="submit">Submit</button>
<div class="js-response"></div>
</form>
{{if .Error}}
<div class="mt-4 text-red-600 font-bold">Error: {{.Error}}</div>
{{end}}
{{if .Response}}
<div class="mt-4">
<h4 class="font-bold mb-2">Response</h4>
{{.ResponseTable}}
<pre class="bg-gray-100 rounded p-2 mt-2">{{.ResponseJSON}}</pre>
</div>
{{end}}
<script src="/main.js"></script>
{{end}}
+41
View File
@@ -0,0 +1,41 @@
{{define "content"}}
<h2 class="text-2xl font-bold mb-4">Home</h2>
<div style="display:flex; align-items:center; gap:2em; margin-bottom:2em;">
<div style="display:flex; align-items:center; gap:0.5em;">
<span style="font-size:2.2em; vertical-align:middle;">
{{if eq .StatusDot "green"}}
<span style="display:inline-block; width:1em; height:1em; background:#2ecc40; border-radius:50%;"></span>
{{else if eq .StatusDot "yellow"}}
<span style="display:inline-block; width:1em; height:1em; background:#ffcc00; border-radius:50%;"></span>
{{else}}
<span style="display:inline-block; width:1em; height:1em; background:#ff4136; border-radius:50%;"></span>
{{end}}
</span>
<span style="font-size:1.2em; font-weight:bold;">Status</span>
</div>
<div style="font-size:1.1em;">Services: <b>{{.ServiceCount}}</b></div>
<div style="font-size:1.1em; color:#2ecc40;">Running: <b>{{.RunningCount}}</b></div>
<div style="font-size:1.1em; color:#ff4136;">Stopped: <b>{{.StoppedCount}}</b></div>
</div>
{{if .Services}}
<h3 style="margin-top:1.5em; margin-bottom:0.8em;">Services</h3>
<table>
<thead>
<tr><th>Name</th><th></th></tr>
</thead>
<tbody>
{{range .Services}}
<tr>
<td><a href="/{{.}}" class="micro-link" style="font-weight:500;">{{.}}</a></td>
<td style="text-align:right;">
<a href="/api#{{.}}" class="micro-link" style="font-size:0.92em; color:#888;">API</a>
</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<p style="color:#888; margin-top:1.5em;">No services registered yet. Start a service and it will appear here.</p>
{{end}}
{{end}}
+5
View File
@@ -0,0 +1,5 @@
{{define "content"}}
<h2 class="text-2xl font-bold mb-4">Logs for {{.Service}}</h2>
<pre class="bg-gray-100 rounded p-2 mt-2" style="max-height: 60vh; overflow-y: auto;">{{.Log}}</pre>
<a href="/logs" class="micro-link">Back to logs</a>
{{end}}
+8
View File
@@ -0,0 +1,8 @@
{{define "content"}}
<h2 class="text-2xl font-bold mb-4">Logs</h2>
<ul class="no-bullets">
{{range .Services}}
<li><a href="/logs/{{.}}" class="micro-link">{{.}}</a></li>
{{end}}
</ul>
{{end}}
+355
View File
@@ -0,0 +1,355 @@
{{define "content"}}
<style>
#agent-chat {
display: flex;
flex-direction: column;
height: calc(100vh - 160px);
max-height: 800px;
}
#agent-messages {
flex: 1;
overflow-y: auto;
padding: 1em 0;
scroll-behavior: smooth;
}
.msg { padding: 0.7em 1em; border-radius: 8px; margin-bottom: 0.6em; line-height: 1.6; }
.msg-user { background: #f0f4ff; border: 1px solid #d0d8f0; }
.msg-user b { color: #336; }
.msg-assistant { background: #fff; border: 1px solid #ddd; }
.msg-assistant b { color: #333; }
.msg-error { background: #fff5f5; border: 1px solid #e88; color: #a00; }
.msg-thinking { background: #fafafa; border: 1px solid #e5e5e5; color: #888; font-style: italic; }
.tool-call {
background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 8px;
margin-bottom: 0.6em; font-size: 0.93em; overflow: hidden;
}
.tool-header {
padding: 0.5em 1em; cursor: pointer; display: flex; align-items: center;
gap: 0.5em; user-select: none; font-weight: 600; color: #555;
}
.tool-header:hover { background: #f0f0f0; }
.tool-header .arrow { transition: transform 0.2s; display: inline-block; }
.tool-header .arrow.open { transform: rotate(90deg); }
.tool-body { padding: 0 1em 0.8em 1em; display: none; }
.tool-body.open { display: block; }
.tool-body pre {
background: #f5f5f5; padding: 0.6em; border-radius: 4px;
overflow-x: auto; font-size: 0.9em; margin: 0.4em 0;
}
.tool-body .tool-label { font-weight: 600; color: #666; font-size: 0.85em; margin-top: 0.5em; }
.tool-status { font-size: 0.8em; font-weight: normal; margin-left: auto; }
.tool-status.ok { color: #2a2; }
.tool-status.err { color: #c00; }
#prompt-bar {
display: flex; gap: 0.5em; align-items: center;
padding: 0.8em 0 0 0; border-top: 1px solid #eee;
}
#prompt-bar input {
flex: 1; margin-bottom: 0; padding: 0.6em 0.8em;
border: 1px solid #ccc; border-radius: 6px; font-size: 1em;
}
#prompt-bar button { padding: 0.6em 1.5em; border-radius: 6px; font-size: 1em; white-space: nowrap; }
#prompt-bar button:disabled { opacity: 0.5; cursor: not-allowed; }
.tools-bar {
display: flex; gap: 0.5em; align-items: center;
padding: 0.5em 0; font-size: 0.85em; color: #888;
}
.tools-bar .tool-count { font-weight: 600; color: #555; }
.settings-toggle {
background: none; border: 1px solid #ddd; border-radius: 6px;
padding: 0.3em 0.8em; font-size: 0.85em; cursor: pointer; color: #555;
}
.settings-toggle:hover { background: #f5f5f5; }
#settings-panel {
display: none; background: #fafafa; border: 1px solid #e5e5e5;
border-radius: 8px; padding: 1em 1.2em; margin-bottom: 1em;
}
#settings-panel.open { display: block; }
#settings-panel label { display: block; font-weight: 600; margin-top: 0.6em; font-size: 0.9em; }
#settings-panel input, #settings-panel select {
width: 100%; padding: 0.4em 0.6em; font-size: 0.9em;
border: 1px solid #ddd; border-radius: 4px; margin-top: 0.2em;
}
#settings-panel .settings-row { display: flex; gap: 1em; }
#settings-panel .settings-row > div { flex: 1; }
.clear-btn {
background: none; border: none; color: #999; cursor: pointer;
font-size: 0.85em; padding: 0.3em 0.5em;
}
.clear-btn:hover { color: #c00; }
.empty-state {
display: flex; flex-direction: column; align-items: center;
justify-content: center; height: 100%; color: #aaa; text-align: center;
}
.empty-state .icon { font-size: 3em; margin-bottom: 0.3em; }
.empty-state p { margin: 0.3em 0; max-width: 400px; }
</style>
<div id="agent-chat">
<div class="tools-bar">
<span>Tools: <span class="tool-count" id="tool-count">...</span></span>
<button class="settings-toggle" onclick="toggleSettings()">Settings</button>
<button class="clear-btn" onclick="clearChat()">Clear chat</button>
</div>
<div id="settings-panel">
<div class="settings-row">
<div>
<label>Provider</label>
<select id="provider">
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic</option>
<option value="gemini">Google Gemini</option>
<option value="atlascloud">Atlas Cloud</option>
<option value="groq">Groq</option>
<option value="mistral">Mistral</option>
<option value="together">Together AI</option>
</select>
</div>
<div>
<label>Model (optional)</label>
<input type="text" id="model-name" placeholder="e.g. gpt-4o, claude-sonnet-4-20250514">
</div>
</div>
<label>API Key</label>
<input type="password" id="api-key" placeholder="sk-... or API key">
<label>Base URL (optional)</label>
<input type="text" id="base-url" placeholder="Leave blank for default">
<div style="margin-top:0.8em; display:flex; gap:0.5em; align-items:center;">
<button onclick="saveSettings()" style="padding:0.4em 1em; font-size:0.9em;">Save</button>
<span id="settings-status" style="color:#888; font-size:0.85em;"></span>
</div>
</div>
<div id="agent-messages">
<div class="empty-state" id="empty-state">
<div class="icon">&#129302;</div>
<p><b>Chat with your services</b></p>
<p>Ask the agent to interact with your microservices. It will discover and call the right tools automatically.</p>
<p id="empty-tools" style="font-size:0.85em; color:#bbb; margin-top:0.8em;"></p>
<div id="setup-hint" style="margin-top:1.2em; padding:1em; background:#f8f9fa; border-radius:8px; border:1px solid #e5e5e5; text-align:left; font-size:0.88em; max-width:400px; margin-left:auto; margin-right:auto;">
<p style="margin:0 0 0.5em; font-weight:600;">Getting started:</p>
<p style="margin:0 0 0.3em;">1. Click <b>Settings</b> above</p>
<p style="margin:0 0 0.3em;">2. Choose a provider and enter your API key</p>
<p style="margin:0 0 0.3em;">3. Type a prompt like <em>"list all services"</em></p>
<p style="margin:0.6em 0 0; color:#888; font-size:0.9em;">Or use the CLI: <code style="background:#e9ecef; padding:2px 5px; border-radius:3px;">micro chat --provider anthropic</code></p>
</div>
</div>
</div>
<div id="prompt-bar">
<input type="text" id="prompt-input" placeholder="Ask the agent to call your services..." autofocus>
<button id="prompt-btn" onclick="sendPrompt()">Send</button>
</div>
</div>
<script>
(function() {
var tools = [];
var toolCount = document.getElementById('tool-count');
loadSettings();
loadTools();
function loadSettings() {
fetch('/api/agent/settings')
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.provider) document.getElementById('provider').value = data.provider;
if (data.api_key) document.getElementById('api-key').value = data.api_key;
if (data.model) document.getElementById('model-name').value = data.model;
if (data.base_url) document.getElementById('base-url').value = data.base_url;
// Hide setup hint if API key is configured
if (data.api_key) {
var hint = document.getElementById('setup-hint');
if (hint) hint.style.display = 'none';
}
// Auto-show settings if no API key configured
if (!data.api_key) {
document.getElementById('settings-panel').classList.add('open');
}
})
.catch(function() {
document.getElementById('settings-panel').classList.add('open');
});
}
window.toggleSettings = function() {
document.getElementById('settings-panel').classList.toggle('open');
};
window.saveSettings = function() {
var status = document.getElementById('settings-status');
status.textContent = 'Saving...';
fetch('/api/agent/settings', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
provider: document.getElementById('provider').value,
api_key: document.getElementById('api-key').value,
model: document.getElementById('model-name').value,
base_url: document.getElementById('base-url').value
})
})
.then(function(r) { return r.json(); })
.then(function() {
status.textContent = 'Saved';
setTimeout(function() { status.textContent = ''; }, 2000);
})
.catch(function(err) { status.textContent = 'Error: ' + err; });
};
function loadTools() {
fetch('/mcp/tools')
.then(function(r) { return r.json(); })
.then(function(data) {
tools = data.tools || [];
toolCount.textContent = tools.length;
var emptyTools = document.getElementById('empty-tools');
if (tools.length > 0) {
var names = tools.slice(0, 5).map(function(t) { return t.name; });
var suffix = tools.length > 5 ? ' and ' + (tools.length - 5) + ' more' : '';
emptyTools.textContent = 'Available: ' + names.join(', ') + suffix;
} else {
emptyTools.textContent = 'No tools found. Start some services first.';
}
})
.catch(function() {
toolCount.textContent = '0';
});
}
window.clearChat = function() {
var container = document.getElementById('agent-messages');
container.innerHTML = '';
var emptyState = document.createElement('div');
emptyState.className = 'empty-state';
emptyState.id = 'empty-state';
emptyState.innerHTML = '<div class="icon">&#129302;</div><p><b>Chat with your services</b></p><p>Ask the agent to interact with your microservices.</p>';
container.appendChild(emptyState);
};
window.sendPrompt = function() {
var input = document.getElementById('prompt-input');
var text = input.value.trim();
if (!text) return;
input.value = '';
// Remove empty state on first message
var emptyState = document.getElementById('empty-state');
if (emptyState) emptyState.remove();
addMessage('user', escapeHtml(text));
// Show thinking indicator
var thinkingId = 'thinking-' + Date.now();
addMessage('thinking', 'Agent is thinking...', thinkingId);
var btn = document.getElementById('prompt-btn');
btn.disabled = true;
btn.textContent = 'Sending...';
var startTime = Date.now();
fetch('/api/agent/prompt', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({prompt: text})
})
.then(function(r) { return r.json(); })
.then(function(data) {
var elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
btn.disabled = false;
btn.textContent = 'Send';
input.focus();
// Remove thinking indicator
var thinking = document.getElementById(thinkingId);
if (thinking) thinking.remove();
if (data.error) {
addMessage('error', escapeHtml(data.error));
return;
}
if (data.reply) {
addMessage('assistant', escapeHtml(data.reply));
}
if (data.tool_calls && data.tool_calls.length > 0) {
for (var i = 0; i < data.tool_calls.length; i++) {
var tc = data.tool_calls[i];
addToolCall(tc, elapsed);
}
}
if (data.answer) {
addMessage('assistant', escapeHtml(data.answer));
}
})
.catch(function(err) {
btn.disabled = false;
btn.textContent = 'Send';
input.focus();
var thinking = document.getElementById(thinkingId);
if (thinking) thinking.remove();
addMessage('error', 'Error: ' + escapeHtml(String(err)));
});
};
function addToolCall(tc, elapsed) {
var container = document.getElementById('agent-messages');
var div = document.createElement('div');
div.className = 'tool-call';
var hasError = tc.result && tc.result.error;
var statusClass = hasError ? 'err' : 'ok';
var statusText = hasError ? 'error' : elapsed + 's';
var inputJson = JSON.stringify(tc.input, null, 2);
var resultJson = JSON.stringify(tc.result, null, 2);
div.innerHTML =
'<div class="tool-header" onclick="this.querySelector(\'.arrow\').classList.toggle(\'open\'); this.nextElementSibling.classList.toggle(\'open\');">' +
'<span class="arrow">&#9654;</span> ' +
'<code>' + escapeHtml(tc.tool) + '</code>' +
'<span class="tool-status ' + statusClass + '">' + statusText + '</span>' +
'</div>' +
'<div class="tool-body">' +
'<div class="tool-label">Input</div>' +
'<pre>' + escapeHtml(inputJson) + '</pre>' +
'<div class="tool-label">Result</div>' +
'<pre>' + escapeHtml(resultJson) + '</pre>' +
'</div>';
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
document.getElementById('prompt-input').addEventListener('keydown', function(e) {
if (e.key === 'Enter') { e.preventDefault(); window.sendPrompt(); }
});
function addMessage(type, html, id) {
var container = document.getElementById('agent-messages');
var div = document.createElement('div');
div.className = 'msg msg-' + type;
if (id) div.id = id;
if (type === 'user') {
div.innerHTML = '<b>You</b><br>' + html;
} else if (type === 'assistant' || type === 'answer') {
div.innerHTML = '<b>Agent</b><br>' + html;
} else if (type === 'thinking') {
div.innerHTML = html;
} else if (type === 'error') {
div.innerHTML = html;
}
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
function escapeHtml(str) {
var d = document.createElement('div');
d.textContent = str;
return d.innerHTML;
}
})();
</script>
{{end}}
+87
View File
@@ -0,0 +1,87 @@
{{define "content"}}
<h2 class="text-2xl font-bold mb-4">Scopes</h2>
<p style="color:#666; margin-bottom:1.5em;">
Set which scopes are required to call each endpoint. Tokens must carry a matching scope.
Endpoints with no scopes are open to any authenticated token.
</p>
{{if .Success}}
<div style="background:#e6ffe6; border:1px solid #5a5; padding:0.8em 1.2em; border-radius:6px; margin-bottom:1.5em; color:#050;">
&#10003; Scopes updated successfully.
</div>
{{end}}
<table style="margin-bottom:2em;">
<thead>
<tr><th>Service</th><th>Endpoint</th><th>Required Scopes</th><th></th></tr>
</thead>
<tbody>
{{range .Endpoints}}
<tr>
<td>{{.Service}}</td>
<td><code>{{.Endpoint}}</code></td>
<td>
{{if .Scopes}}
{{range .Scopes}}<code>{{.}}</code> {{end}}
{{else}}
<span style="color:#999;">none</span>
{{end}}
</td>
<td>
<form method="POST" action="/auth/scopes" style="display:inline; padding:0; border:0; box-shadow:none; background:none;">
<input type="hidden" name="endpoint" value="{{.Name}}">
<input name="scopes" value="{{.ScopesStr}}" placeholder="scope names" style="width:180px; margin-right:0.5em;">
<button type="submit">Save</button>
</form>
</td>
</tr>
{{end}}
{{if not .Endpoints}}
<tr>
<td colspan="4" style="color:#888; text-align:center; padding:2em;">No services discovered. Start some services and they will appear here.</td>
</tr>
{{end}}
</tbody>
</table>
<h3 style="margin-bottom:1em;">Bulk Set</h3>
<p style="color:#666; margin-bottom:1em;">Apply scopes to all endpoints matching a pattern. Use <code>*</code> as a suffix wildcard. Leave scopes empty to clear.</p>
<form method="POST" action="/auth/scopes/bulk" style="margin-bottom:2em;">
<input name="pattern" placeholder="Pattern (e.g. greeter.*)" required style="margin-right:0.5em; width:220px;">
<input name="scopes" placeholder="Scopes (comma separated)" style="margin-right:0.5em; width:220px;">
<button type="submit">Apply</button>
</form>
<h3 style="margin-bottom:1em;">Examples</h3>
<div style="background:#f9f9f9; border:1px solid #eee; padding:1.2em 1.5em; border-radius:6px; font-size:0.97em; line-height:1.6;">
<p style="margin-top:0;">Scopes are strings that you define. A call is allowed when at least one of the token's scopes matches one of the endpoint's required scopes.</p>
<h4 style="margin-top:1em; margin-bottom:0.5em;">Restrict a whole service</h4>
<p>Use Bulk Set with pattern <code>greeter.*</code> and scope <code>greeter</code>.<br>
Then create a <a href="/auth/tokens">token</a> with scope <code>greeter</code> — it can call any endpoint on that service.</p>
<h4 style="margin-top:1em; margin-bottom:0.5em;">Restrict a specific endpoint</h4>
<p>Set scope <code>billing</code> on <code>payments.Payments.Charge</code> using the table above.<br>
Only tokens with the <code>billing</code> scope can call that endpoint. Other payment endpoints remain unaffected.</p>
<h4 style="margin-top:1em; margin-bottom:0.5em;">Role-based access</h4>
<p>Set scope <code>admin</code> on sensitive endpoints (e.g. <code>users.Users.Delete</code>).<br>
Create tokens with <code>admin</code> scope for operators and <code>user</code> scope for regular access.<br>
An endpoint can require multiple scopes — the token only needs to match <b>one</b> of them.</p>
<h4 style="margin-top:1em; margin-bottom:0.5em;">Full access</h4>
<p>The default <code>admin</code> user has scope <code>*</code> which bypasses all checks.<br>
Create a token with <code>*</code> scope for services that need unrestricted access.</p>
<h4 style="margin-top:1em; margin-bottom:0.5em;">Where scopes are checked</h4>
<table style="font-size:0.95em; margin:0.5em 0;">
<thead><tr><th>Access method</th><th>How auth works</th></tr></thead>
<tbody>
<tr><td>API (<code>/api/service/endpoint</code>)</td><td><code>Authorization: Bearer &lt;token&gt;</code> header</td></tr>
<tr><td>MCP tools (<code>/mcp/call</code>)</td><td><code>Authorization: Bearer &lt;token&gt;</code> header</td></tr>
<tr><td>Agent playground</td><td>Uses your logged-in session and its scopes</td></tr>
</tbody>
</table>
</div>
{{end}}
+26
View File
@@ -0,0 +1,26 @@
{{define "content"}}
{{if .ServiceName}}
<h2 class="text-xl font-bold mb-2">{{.ServiceName}}</h2>
<h4 class="font-semibold mb-2">Endpoints</h4>
{{if .Endpoints}}
{{range .Endpoints}}
<div><a href="{{.Path}}" class="micro-link">{{.Name}}</a></div>
{{end}}
{{else}}
<p>No endpoints registered</p>
{{end}}
<h4 class="font-semibold mt-4 mb-2">Description</h4>
<pre class="bg-gray-100 rounded p-2">{{.Description}}</pre>
{{else}}
<h2 class="text-2xl font-bold mb-4">Services</h2>
{{if .Services}}
<ul class="no-bullets">
{{range .Services}}
<li><a href="/{{.}}" class="micro-link">{{.}}</a></li>
{{end}}
</ul>
{{else}}
<p>No services registered</p>
{{end}}
{{end}}
{{end}}
+29
View File
@@ -0,0 +1,29 @@
{{define "content"}}
<h2 class="text-2xl font-bold mb-4">Service Status</h2>
<table>
<thead>
<tr>
<th>Service</th>
<th>Directory</th>
<th>Status</th>
<th>PID</th>
<th>Uptime</th>
<th>ID</th>
<th>Logs</th>
</tr>
</thead>
<tbody>
{{range .Statuses}}
<tr>
<td>{{.Service}}</td>
<td><code>{{.Dir}}</code></td>
<td>{{.Status}}</td>
<td>{{.PID}}</td>
<td>{{.Uptime}}</td>
<td style="font-size:0.9em; color:#888;">{{.ID}}</td>
<td><a href="/logs/{{.ID}}" class="log-link">View logs</a></td>
</tr>
{{end}}
</tbody>
</table>
{{end}}