mirror of
https://github.com/yacy/yacy_search_server.git
synced 2026-08-02 12:44:36 -04:00
412 lines
16 KiB
HTML
412 lines
16 KiB
HTML
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "DTD/xhtml1-transitional.dtd">
|
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
|
<head>
|
|
<title>YaCy '#[clientname]#': Log Reports</title>
|
|
#%env/templates/metas.template%#
|
|
<link rel="stylesheet" href="js/styles/a11y-dark.min.css" type="text/css" />
|
|
<link rel="stylesheet" href="env/markdown.css" type="text/css" />
|
|
<style type="text/css">
|
|
.logreport-feeds {
|
|
margin: 0 0 16px 0;
|
|
}
|
|
/* two-column layout: navigation of existing reports left, selected report right */
|
|
.logreport-layout {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 16px;
|
|
}
|
|
.logreport-nav {
|
|
flex: 0 0 260px;
|
|
max-height: 640px;
|
|
overflow: auto;
|
|
border: 1px solid #ddd;
|
|
background: #fff;
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
}
|
|
.logreport-nav li {
|
|
position: relative;
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
.logreport-nav a {
|
|
display: block;
|
|
padding: 8px 26px 8px 10px;
|
|
text-decoration: none;
|
|
color: #333;
|
|
}
|
|
.logreport-nav a:hover {
|
|
background: #f5f5f5;
|
|
}
|
|
/* small per-report delete button, deletes immediately without confirmation */
|
|
.logreport-nav a.nav-delete {
|
|
position: absolute;
|
|
top: 50%;
|
|
right: 4px;
|
|
transform: translateY(-50%);
|
|
padding: 0 6px;
|
|
font-size: 1.2em;
|
|
line-height: 1.2;
|
|
color: #999;
|
|
}
|
|
.logreport-nav a.nav-delete:hover {
|
|
color: #a94442;
|
|
background: none;
|
|
}
|
|
.logreport-nav li.selected a {
|
|
background: #eef4fa;
|
|
border-left: 4px solid #337ab7;
|
|
padding-left: 6px;
|
|
font-weight: bold;
|
|
}
|
|
.logreport-nav .nav-date {
|
|
display: block;
|
|
font-size: 0.9em;
|
|
}
|
|
.logreport-nav .nav-type {
|
|
display: block;
|
|
color: #888;
|
|
font-size: 0.8em;
|
|
}
|
|
.logreport-main {
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
}
|
|
.logreport-card {
|
|
border: 1px solid #ddd;
|
|
margin: 0 0 16px 0;
|
|
background: #fff;
|
|
}
|
|
.logreport-header {
|
|
background: #f5f5f5;
|
|
border-bottom: 1px solid #ddd;
|
|
padding: 10px 12px;
|
|
}
|
|
.logreport-title {
|
|
font-weight: bold;
|
|
margin: 0 0 4px 0;
|
|
}
|
|
.logreport-meta {
|
|
color: #666;
|
|
font-size: 0.9em;
|
|
}
|
|
.logreport-body {
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
max-height: 520px;
|
|
overflow: auto;
|
|
margin: 0;
|
|
padding: 12px;
|
|
background: #fff;
|
|
border: 0;
|
|
font-family: Menlo, Consolas, monospace;
|
|
font-size: 12px;
|
|
line-height: 1.45;
|
|
}
|
|
/* markdown output styles come from the shared env/markdown.css (.markdown-body);
|
|
only the report container itself is styled locally. white-space must be
|
|
repeated here: the .logreport-body pre-wrap rule (raw markdown fallback)
|
|
would otherwise win over the shared .markdown-body rule, which renders the
|
|
newlines between the generated HTML blocks as visible empty lines */
|
|
.logreport-body.markdown-body {
|
|
max-width: 100%;
|
|
padding: 12px;
|
|
white-space: normal;
|
|
}
|
|
.logreport-empty {
|
|
border: 1px solid #ddd;
|
|
background: #f9f9f9;
|
|
padding: 14px;
|
|
}
|
|
.logreport-warning {
|
|
border: 1px solid #f0ad4e;
|
|
border-left: 6px solid #f0ad4e;
|
|
background: #fcf8e3;
|
|
color: #6b4f13;
|
|
padding: 12px;
|
|
margin: 10px 0 16px 0;
|
|
}
|
|
.logreport-actions {
|
|
margin: 0 0 16px 0;
|
|
}
|
|
.logreport-progress {
|
|
display: none;
|
|
align-items: center;
|
|
gap: 10px;
|
|
border: 1px solid #5bc0de;
|
|
border-left: 6px solid #5bc0de;
|
|
background: #eef9fc;
|
|
color: #245269;
|
|
padding: 12px;
|
|
margin: 10px 0 16px 0;
|
|
}
|
|
.logreport-progress .spinner {
|
|
width: 18px;
|
|
height: 18px;
|
|
border: 3px solid #b7dff0;
|
|
border-top-color: #31708f;
|
|
border-radius: 50%;
|
|
animation: logreport-spin 0.8s linear infinite;
|
|
}
|
|
@keyframes logreport-spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
.logreport-message {
|
|
border: 1px solid #5bc0de;
|
|
border-left: 6px solid #5bc0de;
|
|
background: #eef9fc;
|
|
color: #245269;
|
|
padding: 12px;
|
|
margin: 10px 0 16px 0;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body id="LogReports">
|
|
#%env/templates/header.template%#
|
|
#%env/templates/submenuComputation.template%#
|
|
|
|
<h2>Log Reports</h2>
|
|
|
|
<!-- the report computation runs asynchronously on the server (LogReportService
|
|
manual job); this form only triggers it and returns immediately. While the
|
|
job is running the button is disabled, the progress box is shown and the
|
|
script below polls the page until the result is available -->
|
|
<form action="LogReports_p.html" method="post" accept-charset="UTF-8" class="logreport-actions" id="runReportForm">
|
|
<button type="submit" name="runReportNow" value="1" class="btn btn-primary" id="runReportButton" #(reportRunning)#::disabled="disabled"#(/reportRunning)#>run report now</button>
|
|
</form>
|
|
|
|
<div class="logreport-progress" id="runReportProgress" #(reportRunning)#::style="display:flex"#(/reportRunning)#>
|
|
<div class="spinner"></div>
|
|
<div>Generating report from the current-hour log lines — the LLM call can take a while …
|
|
<span id="runReportElapsed">#(reportRunning)#0::#[elapsedSeconds]##(/reportRunning)#</span> seconds elapsed</div>
|
|
</div>
|
|
|
|
<script type="text/javascript">
|
|
// Live view of the manual report generation: the job runs asynchronously on
|
|
// the server, the model output is streamed into a buffer there, and this
|
|
// script polls api/logreportstatus.json in the background. No hard page
|
|
// reloads - the partially generated report is rendered in place so the user
|
|
// can read and scroll in it while it is still being completed.
|
|
(function () {
|
|
var button = document.getElementById("runReportButton");
|
|
var progress = document.getElementById("runReportProgress");
|
|
var elapsedElement = document.getElementById("runReportElapsed");
|
|
var liveCard = document.getElementById("liveReportCard");
|
|
var liveBody = document.getElementById("liveReportMarkdown");
|
|
var liveTitle = document.getElementById("liveReportTitle");
|
|
var liveMeta = document.getElementById("liveReportMeta");
|
|
var selectedCard = document.getElementById("selectedReportCard");
|
|
var pollTimer = null;
|
|
|
|
function setRunningUI(running) {
|
|
button.disabled = running;
|
|
progress.style.display = running ? "flex" : "none";
|
|
}
|
|
|
|
function renderPartial(partial) {
|
|
// the shared markdown module is loaded at the end of the page and might
|
|
// not be ready when the very first poll response arrives
|
|
if (!liveCard || !partial || typeof YaCyMarkdown === "undefined") return;
|
|
liveCard.style.display = "block";
|
|
if (selectedCard) selectedCard.style.display = "none";
|
|
// re-rendering the grown markdown into the same element preserves the
|
|
// scroll position, so the user can read while the report is completed
|
|
liveBody.innerHTML = YaCyMarkdown.render(partial, { breaks: false });
|
|
liveBody.classList.add("markdown-body");
|
|
}
|
|
|
|
function showResultBox(cssClass, text) {
|
|
var box = document.createElement("div");
|
|
box.className = cssClass;
|
|
box.textContent = text;
|
|
progress.parentNode.insertBefore(box, progress);
|
|
}
|
|
|
|
function finish(status) {
|
|
clearInterval(pollTimer);
|
|
setRunningUI(false);
|
|
if (status.outcome === 1) {
|
|
if (liveTitle) liveTitle.textContent = "YaCy hourly log report (just generated)";
|
|
if (liveMeta) liveMeta.textContent = status.message + " - " + status.durationSeconds + " seconds";
|
|
showResultBox("logreport-message", "Current-hour report written: " + status.message + " (" + status.durationSeconds + " seconds)");
|
|
// add the new report to the navigation column; a delete button is
|
|
// omitted here on purpose, a page reload will offer it
|
|
var nav = document.querySelector(".logreport-nav");
|
|
if (nav) {
|
|
var li = document.createElement("li");
|
|
var a = document.createElement("a");
|
|
a.href = "LogReports_p.html?report=" + encodeURIComponent(status.message);
|
|
a.title = status.message;
|
|
var date = document.createElement("span");
|
|
date.className = "nav-date";
|
|
date.textContent = status.message.replace(/^report-(\d{4}-\d{2}-\d{2})-(\d{2})\.md$/, "$1 $2:00");
|
|
var type = document.createElement("span");
|
|
type.className = "nav-type";
|
|
type.textContent = "hourly report";
|
|
a.appendChild(date);
|
|
a.appendChild(type);
|
|
li.appendChild(a);
|
|
nav.insertBefore(li, nav.firstChild);
|
|
}
|
|
} else if (status.outcome === 2) {
|
|
showResultBox("logreport-warning", "No log lines were found for the current hour.");
|
|
} else if (status.outcome === 3) {
|
|
showResultBox("logreport-warning", "No production model is configured for the log-report role.");
|
|
} else if (status.outcome === 4) {
|
|
showResultBox("logreport-warning", "Report generation failed after " + status.durationSeconds + " seconds: " + status.message);
|
|
}
|
|
}
|
|
|
|
function poll() {
|
|
fetch("api/logreportstatus.json", { cache: "no-store" })
|
|
.then(function (response) { return response.json(); })
|
|
.then(function (status) {
|
|
if (status.running) {
|
|
elapsedElement.textContent = status.elapsedSeconds;
|
|
renderPartial(status.partial);
|
|
} else {
|
|
renderPartial(status.partial); // final render of the complete text
|
|
finish(status);
|
|
}
|
|
})
|
|
.catch(function (err) { console.warn("report status poll failed", err); });
|
|
}
|
|
|
|
function startPolling() {
|
|
setRunningUI(true);
|
|
if (pollTimer) clearInterval(pollTimer);
|
|
pollTimer = setInterval(poll, 2000);
|
|
poll();
|
|
}
|
|
|
|
// start the job via fetch instead of a regular form submit, so the page
|
|
// is not reloaded and the live view keeps its state
|
|
document.getElementById("runReportForm").addEventListener("submit", function (event) {
|
|
event.preventDefault();
|
|
setRunningUI(true);
|
|
fetch("LogReports_p.html", { method: "POST", body: new URLSearchParams({ runReportNow: "1" }) })
|
|
.then(function () { startPolling(); })
|
|
.catch(function (err) {
|
|
console.warn("could not start report generation", err);
|
|
setRunningUI(false);
|
|
});
|
|
});
|
|
|
|
// a job may already be running (started earlier or by another admin)
|
|
var reportRunning = #(reportRunning)#false::true#(/reportRunning)#;
|
|
if (reportRunning) startPolling();
|
|
})();
|
|
</script>
|
|
|
|
#(runReportResult)#
|
|
::
|
|
<div class="logreport-message">Current-hour report written: #[runReportFile]# (#[runReportDurationSeconds]# seconds)</div>
|
|
::
|
|
<div class="logreport-warning">No log lines were found for the current hour.</div>
|
|
::
|
|
<div class="logreport-warning">
|
|
No production model is configured for the log-report role. Assign one in the
|
|
<a href="LLMSelection_p.html#availableModels">Production Models Matrix</a>.
|
|
</div>
|
|
::
|
|
<div class="logreport-warning">Report generation failed after #[runReportDurationSeconds]# seconds: #[runReportFile]#</div>
|
|
#(/runReportResult)#
|
|
|
|
#(modelConfigured)#
|
|
<div class="logreport-warning">
|
|
No production model is configured for the log-report role. Log report generation stays inactive until a model is assigned in the
|
|
<a href="LLMSelection_p.html#availableModels">Production Models Matrix</a>.
|
|
</div>
|
|
::
|
|
#(/modelConfigured)#
|
|
|
|
<p class="logreport-feeds">
|
|
Reports found: #[reportCount]#<br />
|
|
Directory: #[reportdir]#<br />
|
|
Feeds:
|
|
<a href="#[jsonFeed]#">JSON</a>
|
|
|
|
|
<a href="#[rssFeed]#">RSS</a>
|
|
</p>
|
|
|
|
#(reportdirMissing)#
|
|
::
|
|
<div class="logreport-empty">The report directory does not exist yet. Reports will appear here after the scheduler has generated the first completed hourly report.</div>
|
|
#(/reportdirMissing)#
|
|
|
|
#(hasReports)#
|
|
::
|
|
<div class="logreport-layout">
|
|
<ul class="logreport-nav">
|
|
#{reports}#
|
|
<li #(selected)#::class="selected"#(/selected)#>
|
|
<a href="LogReports_p.html?report=#[filename]#" title="#[filename]#">
|
|
<span class="nav-date">#[date]#</span>
|
|
<span class="nav-type">#[type]# report</span>
|
|
</a>
|
|
<a class="nav-delete" href="LogReports_p.html?deleteReport=#[filename]#" title="delete this report">×</a>
|
|
</li>
|
|
#{/reports}#
|
|
</ul>
|
|
|
|
<div class="logreport-main">
|
|
<!-- live view of the report which is currently being generated: the polling
|
|
script streams the partial model output into this card. Intentionally
|
|
without a delete button - the report file does not exist yet -->
|
|
<div class="logreport-card" id="liveReportCard" style="display:none">
|
|
<div class="logreport-header">
|
|
<div class="logreport-title" id="liveReportTitle">Report generation in progress …</div>
|
|
<div class="logreport-meta" id="liveReportMeta">the report below is completed live while the model is writing</div>
|
|
</div>
|
|
<div class="logreport-body" id="liveReportMarkdown"></div>
|
|
</div>
|
|
|
|
#(selectedReport)#
|
|
::
|
|
<div class="logreport-card" id="selectedReportCard">
|
|
<div class="logreport-header">
|
|
<div class="logreport-title">#[title]#</div>
|
|
<div class="logreport-meta">
|
|
#[type]# report - #[filename]# - #[published]# - #[chars]# characters
|
|
</div>
|
|
</div>
|
|
<!-- the template writes the escaped markdown source into this element; the
|
|
script below replaces it with rendered markdown (same pipeline as the
|
|
chat messages in yacychat.html) and adds the markdown-body class. Without
|
|
JavaScript the raw markdown stays readable through the pre-wrap rule -->
|
|
<div class="logreport-body" id="reportMarkdown">#[content]#</div>
|
|
</div>
|
|
#(/selectedReport)#
|
|
</div>
|
|
</div>
|
|
#(/hasReports)#
|
|
|
|
#(noReports)#
|
|
::
|
|
<div class="logreport-empty">No generated log reports were found.</div>
|
|
#(/noReports)#
|
|
|
|
<script src="js/highlight.min.js"></script>
|
|
<script src="js/marked.umd.js"></script>
|
|
<script src="js/yacy-markdown.js"></script>
|
|
<script type="text/javascript">
|
|
// Render the selected report with the shared markdown pipeline (js/yacy-markdown.js),
|
|
// the same one used for the chat messages in yacychat.html. textContent yields the
|
|
// raw markdown source which the server-side template wrote in HTML-escaped form.
|
|
(function () {
|
|
var container = document.getElementById("reportMarkdown");
|
|
if (!container || typeof marked === "undefined") return;
|
|
// breaks:false because report files have hard-wrapped source lines which
|
|
// must flow as continuous text, not render as one <br> per source line
|
|
container.innerHTML = YaCyMarkdown.render(container.textContent, { breaks: false });
|
|
// the class switches the container from the pre-wrap raw-text fallback
|
|
// to the rendered-markdown styles from env/markdown.css
|
|
container.classList.add("markdown-body");
|
|
YaCyMarkdown.highlightAll(container);
|
|
})();
|
|
</script>
|
|
|
|
#%env/templates/footer.template%#
|
|
</body>
|
|
</html>
|