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
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:
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 <token></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}}
|
||||
@@ -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}}
|
||||
@@ -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 <token>" \
|
||||
-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}}
|
||||
@@ -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}}
|
||||
@@ -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">🏠 Home</a></li>
|
||||
<li><a href="/agent" class="micro-link">🤖 Agent</a></li>
|
||||
<li><a href="/api" class="micro-link">🔌 API</a></li>
|
||||
<li><a href="/logs" class="micro-link">📜 Logs</a></li>
|
||||
{{if .AuthEnabled}}
|
||||
<li><a href="/auth/scopes" class="micro-link">🔒 Scopes</a></li>
|
||||
{{end}}
|
||||
<li><a href="/services" class="micro-link">⚙ Services</a></li>
|
||||
<li><a href="/status" class="micro-link">🔴 Status</a></li>
|
||||
{{if .AuthEnabled}}
|
||||
<li><a href="/auth/tokens" class="micro-link">🔑 Tokens</a></li>
|
||||
<li><a href="/auth/users" class="micro-link">👤 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>
|
||||
@@ -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}}
|
||||
@@ -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}}
|
||||
@@ -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}}
|
||||
@@ -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}}
|
||||
@@ -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">🤖</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">🤖</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">▶</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}}
|
||||
@@ -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;">
|
||||
✓ 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 <token></code> header</td></tr>
|
||||
<tr><td>MCP tools (<code>/mcp/call</code>)</td><td><code>Authorization: Bearer <token></code> header</td></tr>
|
||||
<tr><td>Agent playground</td><td>Uses your logged-in session and its scopes</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -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}}
|
||||
@@ -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}}
|
||||
Reference in New Issue
Block a user