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
72 changes: 41 additions & 31 deletions frontend_multi_user/src/admin_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,40 +370,50 @@ def admin_database_backup():
return jsonify({"error": str(e)}), 502


@admin_routes_bp.route("/ping/stream")
@admin_routes_bp.route("/ping/list")
@login_required
def ping_stream():
def ping_list():
worker_plan_url = current_app.config["WORKER_PLAN_URL"]
url = f"{worker_plan_url}/llm-list"
try:
resp = requests.get(url, timeout=(5, 30))
except Exception as exc:
logger.error("LLM ping list proxy exception: %s", exc)
return jsonify({"error": str(exc)}), 502
if resp.status_code != 200:
return jsonify({"error": f"worker_plan responded with {resp.status_code}"}), 502
return jsonify(resp.json())

def generate():
url = f"{worker_plan_url}/llm-ping"
logger.info("Proxying LLM ping stream from %s", url)
try:
with requests.get(
url,
stream=True,
timeout=(5, 300),
headers={"Accept": "text/event-stream"},
) as resp:
if resp.status_code != 200:
msg = f"worker_plan responded with {resp.status_code}"
logger.error("LLM ping proxy error: %s", msg)
yield f"data: {json.dumps({'name': 'worker_plan', 'status': 'error', 'response_time': 0, 'response': msg})}\n\n"
yield f"data: {json.dumps({'name': 'server', 'status': 'done', 'response_time': 0, 'response': ''})}\n\n"
return
for line in resp.iter_lines(decode_unicode=True):
if line is None or line.strip() == "":
continue
yield f"{line}\n\n"
except Exception as exc:
logger.error("LLM ping proxy exception: %s", exc)
error_payload = {"name": "worker_plan", "status": "error", "response_time": 0, "response": str(exc)}
yield f"data: {json.dumps(error_payload)}\n\n"
yield f"data: {json.dumps({'name': 'server', 'status': 'done', 'response_time': 0, 'response': ''})}\n\n"

response = Response(generate(), mimetype="text/event-stream")
response.headers["X-Accel-Buffering"] = "no"
return response

@admin_routes_bp.route("/ping/one")
@login_required
def ping_one():
worker_plan_url = current_app.config["WORKER_PLAN_URL"]
profile = request.args.get("profile", "")
llm_name = request.args.get("llm_name", "")
url = f"{worker_plan_url}/llm-ping-one"
try:
resp = requests.get(
url,
params={"profile": profile, "llm_name": llm_name},
timeout=(5, 300),
)
except Exception as exc:
logger.error("LLM ping-one proxy exception: %s", exc)
return jsonify({
"name": f"{profile}:{llm_name}",
"status": "error",
"response_time": 0,
"response": str(exc),
}), 502
if resp.status_code != 200:
return jsonify({
"name": f"{profile}:{llm_name}",
"status": "error",
"response_time": 0,
"response": f"worker_plan responded with {resp.status_code}",
}), 502
return jsonify(resp.json())


@admin_routes_bp.route("/admin/demo_run")
Expand Down
220 changes: 141 additions & 79 deletions frontend_multi_user/templates/ping.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,35 +16,27 @@
border: 1px solid #ddd;
padding: 8px;
text-align: left;
vertical-align: top;
}
th {
background-color: #f2f2f2;
}
tr:nth-child(even) {
background-color: #f9f9f9;
}
.status-success {
color: green;
}
.status-error {
color: red;
}
.status-pinging {
color: #666;
font-style: italic;
}
.status-success { color: green; }
.status-error { color: red; }
.status-pinging { color: #666; font-style: italic; }
.status-idle { color: #888; }
.back-link {
margin-bottom: 20px;
display: inline-block;
}
.loading {
color: #666;
font-style: italic;
}
.loading { color: #666; font-style: italic; }
@keyframes ellipsis {
0% { content: '.'; }
33% { content: '..'; }
66% { content: '...'; }
0% { content: '.'; }
33% { content: '..'; }
66% { content: '...'; }
100% { content: '.'; }
}
.pinging::after {
Expand All @@ -54,96 +46,166 @@
width: 1em;
text-align: left;
}
.server-status {
font-size: 0.8em;
font-weight: normal;
padding: 4px 8px;
border-radius: 4px;
margin-left: 10px;
display: inline-block;
.toolbar {
margin-bottom: 12px;
}
.server-status.working {
background-color: #2196F3;
color: white;
button.ping-btn,
button.ping-all-btn {
cursor: pointer;
padding: 4px 10px;
}
.server-status.done {
background-color: #4CAF50;
color: white;
button.ping-all-btn {
font-size: 1em;
padding: 8px 16px;
}
.server-status.error {
background-color: #f44336;
color: white;
button:disabled {
cursor: not-allowed;
opacity: 0.6;
}
</style>
</head>
<body>
<a href="/admin" class="back-link">← Back to Admin</a>
<h1>LLM Ping Results <span id="serverStatus" class="server-status working">Working...</span></h1>
<h1>LLM Ping Results</h1>
<div class="toolbar">
<button id="pingAllBtn" class="ping-all-btn" disabled>Ping All</button>
<span id="status" class="loading">Loading models…</span>
</div>
<table>
<thead>
<tr>
<th>LLM Name</th>
<th>Priority</th>
<th>Action</th>
<th>Status</th>
<th>Response Time</th>
<th>Response</th>
</tr>
</thead>
<tbody id="results">
<tr>
<td colspan="4" class="loading">Loading results...</td>
</tr>
</tbody>
<tbody id="results"></tbody>
</table>

<script>
const resultsTable = document.getElementById('results');
const serverStatus = document.getElementById('serverStatus');
const results = new Map();
const resultsBody = document.getElementById('results');
const pingAllBtn = document.getElementById('pingAllBtn');
const statusEl = document.getElementById('status');

let models = [];

function escapeHtml(s) {
return String(s ?? '').replace(/[&<>"']/g, c => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
}[c]));
}

function rowId(model) {
return 'row-' + model.display_name.replace(/[^a-zA-Z0-9_-]/g, '_');
}

const eventSource = new EventSource('/ping/stream');

eventSource.onmessage = function(event) {
const result = JSON.parse(event.data);

if (result.name === 'server') {
if (result.status === 'done') {
serverStatus.textContent = 'Done';
serverStatus.className = 'server-status done';
eventSource.close();
}
function setRowState(model, state) {
const row = document.getElementById(rowId(model));
if (!row) return;
const statusCell = row.querySelector('.status-cell');
const timeCell = row.querySelector('.time-cell');
const respCell = row.querySelector('.response-cell');
const btn = row.querySelector('.ping-btn');

if (state.status === 'pinging') {
statusCell.className = 'status-cell pinging';
statusCell.textContent = 'pinging';
timeCell.textContent = '';
respCell.textContent = '';
btn.disabled = true;
return;
}

statusCell.className = 'status-cell status-' + state.status;
statusCell.textContent = state.status;
timeCell.textContent = (state.response_time ?? 0) + 'ms';
respCell.textContent = state.response ?? '';
btn.disabled = false;
}

function renderTable() {
if (models.length === 0) {
resultsBody.innerHTML = '<tr><td colspan="6" class="loading">No models configured.</td></tr>';
return;
}

results.set(result.name, result);
updateTable();
};
const rows = models.map(m => `
<tr id="${rowId(m)}">
<td>${escapeHtml(m.display_name)}</td>
<td>${m.priority === null || m.priority === undefined ? '' : escapeHtml(m.priority)}</td>
<td><button class="ping-btn" data-profile="${escapeHtml(m.profile)}" data-llm="${escapeHtml(m.llm_name)}">Ping</button></td>
<td class="status-cell status-idle">idle</td>
<td class="time-cell"></td>
<td class="response-cell"></td>
</tr>
`);
resultsBody.innerHTML = rows.join('');

eventSource.onerror = function() {
eventSource.close();
if (results.size === 0) {
resultsTable.innerHTML = '<tr><td colspan="4" class="status-error">Error connecting to server</td></tr>';
resultsBody.querySelectorAll('.ping-btn').forEach(btn => {
btn.addEventListener('click', () => {
const m = {
profile: btn.dataset.profile,
llm_name: btn.dataset.llm,
display_name: btn.dataset.profile + ':' + btn.dataset.llm,
};
pingOne(m);
});
});
}

async function pingOne(model) {
setRowState(model, { status: 'pinging' });
try {
const params = new URLSearchParams({
profile: model.profile,
llm_name: model.llm_name,
});
const resp = await fetch('/ping/one?' + params.toString());
const data = await resp.json();
setRowState(model, data);
return data;
} catch (exc) {
setRowState(model, {
status: 'error',
response_time: 0,
response: String(exc),
});
}
serverStatus.textContent = 'Error';
serverStatus.className = 'server-status error';
};
}

function updateTable() {
if (results.size === 0) return;
async function pingAll() {
pingAllBtn.disabled = true;
statusEl.textContent = 'Pinging all models…';
statusEl.className = 'loading';
for (const m of models) {
await pingOne(m);
}
statusEl.textContent = 'Done.';
statusEl.className = '';
pingAllBtn.disabled = false;
}

let html = '';
for (const result of results.values()) {
const statusClass = result.status === 'pinging' ? 'pinging' : `status-${result.status}`;
html += `
<tr>
<td>${result.name}</td>
<td class="${statusClass}">${result.status}</td>
<td>${result.response_time}ms</td>
<td>${result.response}</td>
</tr>
`;
async function loadModels() {
try {
const resp = await fetch('/ping/list');
if (!resp.ok) throw new Error('HTTP ' + resp.status);
const data = await resp.json();
models = data.models || [];
renderTable();
pingAllBtn.disabled = models.length === 0;
statusEl.textContent = models.length + ' models loaded.';
statusEl.className = '';
} catch (exc) {
statusEl.textContent = 'Failed to load models: ' + exc;
statusEl.className = 'status-error';
resultsBody.innerHTML = '';
}
resultsTable.innerHTML = html;
}

pingAllBtn.addEventListener('click', pingAll);
loadModels();
</script>
</body>
</html>
</html>
Loading