summaryrefslogtreecommitdiff
path: root/htroot
diff options
context:
space:
mode:
Diffstat (limited to 'htroot')
-rw-r--r--htroot/LogReports_p.html173
-rw-r--r--htroot/js/yacy-markdown.js85
-rw-r--r--htroot/yacychat.html94
3 files changed, 249 insertions, 103 deletions
diff --git a/htroot/LogReports_p.html b/htroot/LogReports_p.html
index a99a20eb4..45e86c16a 100644
--- a/htroot/LogReports_p.html
+++ b/htroot/LogReports_p.html
@@ -3,10 +3,59 @@
<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 {
+ border-bottom: 1px solid #eee;
+ }
+ .logreport-nav a {
+ display: block;
+ padding: 8px 10px;
+ text-decoration: none;
+ color: #333;
+ }
+ .logreport-nav a:hover {
+ background: #f5f5f5;
+ }
+ .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;
@@ -38,6 +87,16 @@
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;
@@ -54,6 +113,28 @@
.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;
@@ -70,18 +151,31 @@
<h2>Log Reports</h2>
- <form action="LogReports_p.html" method="get" accept-charset="UTF-8">
- <fieldset>
- <label for="count">Reports</label>
- <input type="number" name="count" id="count" min="0" max="#[maxcount]#" value="#[count]#" />
- <input type="submit" value="refresh" />
- </fieldset>
+ <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">run report now</button>
</form>
- <form action="LogReports_p.html" method="post" accept-charset="UTF-8" class="logreport-actions">
- <input type="hidden" name="count" value="#[count]#" />
- <button type="submit" name="runReportNow" value="1" class="btn btn-primary">run report now</button>
- </form>
+ <!-- shown while the synchronous report generation request is running; the page
+ reloads with the result when the server is done, which removes this again -->
+ <div class="logreport-progress" id="runReportProgress">
+ <div class="spinner"></div>
+ <div>Generating report from the current-hour log lines &mdash; the LLM call can take a while &hellip;
+ <span id="runReportElapsed">0</span> seconds elapsed</div>
+ </div>
+
+ <script type="text/javascript">
+ document.getElementById("runReportForm").addEventListener("submit", function () {
+ var button = document.getElementById("runReportButton");
+ // a submit button disabled during the submit event would drop its
+ // runReportNow=1 value from the request, so disable it one tick later
+ setTimeout(function () { button.disabled = true; }, 0);
+ document.getElementById("runReportProgress").style.display = "flex";
+ var started = Date.now();
+ setInterval(function () {
+ document.getElementById("runReportElapsed").textContent = Math.floor((Date.now() - started) / 1000);
+ }, 1000);
+ });
+ </script>
#(runReportResult)#
::
@@ -119,23 +213,66 @@
<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)#
- #{reports}#
- <div class="logreport-card">
- <div class="logreport-header">
- <div class="logreport-title">#[title]#</div>
- <div class="logreport-meta">
- #[type]# report - #[filename]# - #[published]# - #[chars]# characters
+ #(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>
+ </li>
+ #{/reports}#
+ </ul>
+
+ <div class="logreport-main">
+ #(selectedReport)#
+ ::
+ <div class="logreport-card">
+ <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>
- <pre class="logreport-body">#[content]#</pre>
</div>
- #{/reports}#
+ #(/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>
diff --git a/htroot/js/yacy-markdown.js b/htroot/js/yacy-markdown.js
new file mode 100644
index 000000000..18fd40676
--- /dev/null
+++ b/htroot/js/yacy-markdown.js
@@ -0,0 +1,85 @@
+/*
+ * yacy-markdown.js
+ * Shared markdown rendering helpers, used by yacychat.html and LogReports_p.html.
+ * Renders LLM-generated markdown to sanitized HTML: marked for parsing, a
+ * sanitizer that strips active content (the source is model output, not trusted
+ * input), and highlight.js for fenced code blocks. Style rules for the rendered
+ * output live in env/markdown.css, scoped to the .markdown-body class.
+ * Requires js/marked.umd.js; js/highlight.min.js is optional.
+ */
+(function (global) {
+ "use strict";
+
+ const markedOptions = {
+ gfm: true,
+ breaks: true,
+ smartLists: true,
+ mangle: false,
+ headerIds: false
+ };
+
+ function escapeHTML(value) {
+ const div = document.createElement('div');
+ div.textContent = value || '';
+ return div.innerHTML;
+ }
+
+ function sanitizeHTML(html) {
+ if (!html) return '';
+ const template = document.createElement('template');
+ template.innerHTML = html;
+ const blockedTags = new Set(['script', 'style', 'iframe', 'object', 'embed', 'link', 'meta']);
+ const walker = document.createTreeWalker(template.content, NodeFilter.SHOW_ELEMENT, null);
+ const toRemove = [];
+ while (walker.nextNode()) {
+ const el = walker.currentNode;
+ if (!el || !el.tagName) continue;
+ const tag = el.tagName.toLowerCase();
+ if (blockedTags.has(tag)) {
+ toRemove.push(el);
+ continue;
+ }
+ for (const attr of Array.from(el.attributes)) {
+ const name = attr.name.toLowerCase();
+ const value = attr.value || '';
+ if (name.startsWith('on')) {
+ el.removeAttribute(attr.name);
+ continue;
+ }
+ if ((name === 'href' || name === 'src') && /^\s*javascript:/i.test(value)) {
+ el.removeAttribute(attr.name);
+ }
+ }
+ }
+ toRemove.forEach(node => node.remove());
+ return template.innerHTML;
+ }
+
+ /**
+ * Render markdown source to sanitized HTML; falls back to escaped plain text
+ * when marked is not available or parsing fails.
+ * optionOverrides can adjust the shared marked options per call, e.g.
+ * { breaks: false } for documents with hard-wrapped source lines where a
+ * single newline must not become a visible line break.
+ */
+ function render(source, optionOverrides) {
+ const text = typeof source === 'string' ? source : '';
+ if (typeof marked === 'undefined') return escapeHTML(text);
+ try {
+ marked.setOptions(Object.assign({}, markedOptions, optionOverrides || {}));
+ return sanitizeHTML(marked.parse(text));
+ } catch (err) {
+ console.warn('Markdown rendering failed', err);
+ return escapeHTML(text);
+ }
+ }
+
+ /** Apply highlight.js to all fenced code blocks below the given container. */
+ function highlightAll(container) {
+ if (!container || typeof hljs === 'undefined') return;
+ hljs.configure({ ignoreUnescapedHTML: true });
+ container.querySelectorAll('pre code').forEach(block => hljs.highlightElement(block));
+ }
+
+ global.YaCyMarkdown = { markedOptions, escapeHTML, sanitizeHTML, render, highlightAll };
+})(window);
diff --git a/htroot/yacychat.html b/htroot/yacychat.html
index 072e25317..d4b681827 100644
--- a/htroot/yacychat.html
+++ b/htroot/yacychat.html
@@ -4,6 +4,7 @@
<title>YaCy '#[clientname]#': Chat</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">
/* 1. Global Container */
.chat-panel {
@@ -121,16 +122,9 @@
font-family: 'Roboto Mono', 'Menlo', 'Consolas', monospace;
}
- .chat-body.markdown {
- white-space: normal;
- }
-
- .chat-body.markdown,
- .chat-body.markdown * {
- font-family: 'Roboto Mono', 'Menlo', 'Consolas', monospace !important; /* Force markdown output and descendants to stay monospace, FTW! */
- }
-
- .chat-body.markdown.markdown-body {
+ /* markdown output styles come from the shared env/markdown.css (.markdown-body);
+ only the chat-specific container properties are set here */
+ .chat-body.markdown-body {
max-width: 100%;
padding: 0;
}
@@ -224,38 +218,6 @@
line-height: 1.4;
}
- .chat-body.markdown h1,
- .chat-body.markdown h2,
- .chat-body.markdown h3,
- .chat-body.markdown h4,
- .chat-body.markdown h5,
- .chat-body.markdown h6,
- .chat-body.markdown p,
- .chat-body.markdown li,
- .chat-body.markdown pre,
- .chat-body.markdown code {
- font-size: 1.2rem;
- line-height: 1.6;
- }
-
- .chat-body pre {
- background-color: #2c3e50;
- border: 1px solid #dfe2e5;
- border-radius: 4px;
- padding: 2px;
- overflow-x: auto;
- font-family: 'Roboto Mono', 'Menlo', 'Consolas', monospace;
- white-space: pre;
- }
-
- .chat-body code {
- font-family: 'Roboto Mono', 'Menlo', 'Consolas', monospace;
- font-size: 0.95rem;
- background-color: rgba(27,31,35,0.05);
- padding: 2px 4px;
- border-radius: 4px;
- }
-
/* 6. Composer Row (inside User fieldset) */
.composer {
display: flex;
@@ -816,6 +778,7 @@
<script src="js/highlight.min.js"></script>
<script src="js/marked.umd.js"></script>
+ <script src="js/yacy-markdown.js"></script>
<script src="js/index.umd.min.js"></script>
<script src="js/vfs.js"></script>
<script type="text/javascript">
@@ -996,52 +959,13 @@
} catch (err) {
console.warn('Failed to initialize syntax highlighting', err);
}
- marked.setOptions({
- gfm: true,
- breaks: true,
- smartLists: true,
- mangle: false,
- headerIds: false
- });
+ marked.setOptions(YaCyMarkdown.markedOptions);
return { enabled: true };
})();
- function escapeHTML(value) {
- const div = document.createElement('div');
- div.textContent = value || '';
- return div.innerHTML;
- }
-
- function sanitizeHTML(html) {
- if (!html) return '';
- const template = document.createElement('template');
- template.innerHTML = html;
- const blockedTags = new Set(['script', 'style', 'iframe', 'object', 'embed', 'link', 'meta']);
- const walker = document.createTreeWalker(template.content, NodeFilter.SHOW_ELEMENT, null);
- const toRemove = [];
- while (walker.nextNode()) {
- const el = walker.currentNode;
- if (!el || !el.tagName) continue;
- const tag = el.tagName.toLowerCase();
- if (blockedTags.has(tag)) {
- toRemove.push(el);
- continue;
- }
- for (const attr of Array.from(el.attributes)) {
- const name = attr.name.toLowerCase();
- const value = attr.value || '';
- if (name.startsWith('on')) {
- el.removeAttribute(attr.name);
- continue;
- }
- if ((name === 'href' || name === 'src') && /^\s*javascript:/i.test(value)) {
- el.removeAttribute(attr.name);
- }
- }
- }
- toRemove.forEach(node => node.remove());
- return template.innerHTML;
- }
+ // shared markdown helpers from js/yacy-markdown.js, also used by LogReports_p.html
+ const escapeHTML = YaCyMarkdown.escapeHTML;
+ const sanitizeHTML = YaCyMarkdown.sanitizeHTML;
function renderMarkdown(content) {
const source = typeof content === 'string' ? content : '';