diff options
| -rw-r--r-- | defaults/yacy.init | 33 | ||||
| -rw-r--r-- | htroot/LogReports_p.html | 173 | ||||
| -rw-r--r-- | htroot/js/yacy-markdown.js | 85 | ||||
| -rw-r--r-- | htroot/yacychat.html | 94 | ||||
| -rw-r--r-- | source/net/yacy/ai/LogReportService.java | 57 | ||||
| -rw-r--r-- | source/net/yacy/htroot/LogReports_p.java | 47 |
6 files changed, 326 insertions, 163 deletions
diff --git a/defaults/yacy.init b/defaults/yacy.init index 9afd2c82c..874d7ffa2 100644 --- a/defaults/yacy.init +++ b/defaults/yacy.init @@ -1437,23 +1437,22 @@ decoration.grafics.linkstructure = true decoration.hostanalysis = false
decoration.simpleheadernavbar = navbar-default
-# ai settings -ai.production_models = [] -ai.system-prompt = You are a smart and helpful chatbot. If possible, use friendly emojies. -ai.llm-system-prefix = \n\nYou may receive additional expert knowledge in the user prompt after a 'Additional Information' headline to enhance your knowledge. Use it only if applicable. -ai.llm-user-prefix = \n\nAdditional Information:\n\nbelow you find a collection of texts that might be useful to generate a response. Do not discuss these documents, just use them to answer the question above.\n\n -ai.llm-query-generator-prefix = Make a list of search words with low document frequency for the following prompt; use a JSON Array: -ai.logreport.enabled = true -ai.logreport.dir = DATA/REPORTS/log -ai.logreport.initial_delay_minutes = 5 -ai.logreport.period_minutes = 60 -ai.logreport.max_bucket_lines = 100000 -ai.logreport.max_tokens = 4096 -ai.logreport.daily_compression.enabled = true -ai.logreport.daily_compression.delete_hourly = false -ai.logreport.feed.max_entries = 100 -ai.shield.allow-nonlocalhost = false -ai.shield.show-chat-link = false +# ai settings
+ai.production_models = []
+ai.system-prompt = You are a smart and helpful chatbot. If possible, use friendly emojies.
+ai.llm-system-prefix = \n\nYou may receive additional expert knowledge in the user prompt after a 'Additional Information' headline to enhance your knowledge. Use it only if applicable.
+ai.llm-user-prefix = \n\nAdditional Information:\n\nbelow you find a collection of texts that might be useful to generate a response. Do not discuss these documents, just use them to answer the question above.\n\n
+ai.llm-query-generator-prefix = Make a list of search words with low document frequency for the following prompt; use a JSON Array:
+ai.logreport.enabled = true
+ai.logreport.dir = DATA/REPORTS/log
+ai.logreport.initial_delay_minutes = 5
+ai.logreport.period_minutes = 60
+ai.logreport.max_bucket_lines = 100000
+ai.logreport.max_tokens = 4096
+ai.logreport.daily_compression.enabled = true
+ai.logreport.feed.max_entries = 100
+ai.shield.allow-nonlocalhost = false
+ai.shield.show-chat-link = false
ai.shield.rate.per-minute = 4
ai.shield.rate.per-hour = 60
ai.shield.rate.per-day = 120
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 — the LLM call can take a while … + <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 : ''; diff --git a/source/net/yacy/ai/LogReportService.java b/source/net/yacy/ai/LogReportService.java index ddee5db9c..81b7eb358 100644 --- a/source/net/yacy/ai/LogReportService.java +++ b/source/net/yacy/ai/LogReportService.java @@ -63,7 +63,6 @@ public class LogReportService { public static final String CONFIG_PERIOD_MINUTES = "ai.logreport.period_minutes"; public static final String CONFIG_MAX_TOKENS = "ai.logreport.max_tokens"; public static final String CONFIG_DAILY_COMPRESSION_ENABLED = "ai.logreport.daily_compression.enabled"; - public static final String CONFIG_DELETE_HOURLY_AFTER_DAILY = "ai.logreport.daily_compression.delete_hourly"; public static final String CONFIG_FEED_MAX_ENTRIES = "ai.logreport.feed.max_entries"; public static final String DEFAULT_REPORT_DIR = "DATA/REPORTS/log"; public static final int DEFAULT_MAX_BUCKET_LINES = 100000; @@ -348,21 +347,24 @@ public class LogReportService { return Collections.emptyList(); } - final Map<LocalDate, List<File>> completeDays = completeHourlyReportSets(reportDirectory); - log.info("runId=" + runId + " event=daily-reports phase=scan completeDays=" + completeDays.size() + " path=" + reportDirectory.getAbsolutePath()); - if (completeDays.isEmpty()) { - log.info("runId=" + runId + " event=daily-reports phase=skip reason=no-complete-day durationMs=" + elapsed(start)); + final Map<LocalDate, List<File>> pastDays = pastDayHourlyReportSets(reportDirectory); + log.info("runId=" + runId + " event=daily-reports phase=scan pastDays=" + pastDays.size() + " path=" + reportDirectory.getAbsolutePath()); + if (pastDays.isEmpty()) { + log.info("runId=" + runId + " event=daily-reports phase=skip reason=no-past-day-hourly-reports durationMs=" + elapsed(start)); return Collections.emptyList(); } final List<File> writtenReports = new ArrayList<>(); int skippedExisting = 0; int failedReports = 0; - for (final Map.Entry<LocalDate, List<File>> day : completeDays.entrySet()) { + for (final Map.Entry<LocalDate, List<File>> day : pastDays.entrySet()) { final File dailyReportFile = new File(reportDirectory, dailyReportFilename(day.getKey())); if (dailyReportFile.exists()) { + // the day was already compressed earlier; only clean up leftover hourly + // reports (e.g. written by a manual "run report now" after the compression) skippedExisting++; - log.info("runId=" + runId + " event=daily-report phase=skip day=" + day.getKey() + " reason=report-exists file=" + dailyReportFile.getAbsolutePath()); + deleteHourlyReports(day.getValue()); + log.info("runId=" + runId + " event=daily-report phase=skip day=" + day.getKey() + " reason=report-exists deletedLeftoverHourly=" + day.getValue().size() + " file=" + dailyReportFile.getAbsolutePath()); continue; } try { @@ -379,16 +381,15 @@ public class LogReportService { writeReport(dailyReportFile, dailyReportDocument(day.getKey(), day.getValue().size(), report)); writtenReports.add(dailyReportFile); log.info("runId=" + runId + " event=daily-report phase=write result=success day=" + day.getKey() + " sourceReports=" + day.getValue().size() + " outputChars=" + report.length() + " file=" + dailyReportFile.getAbsolutePath()); - if (this.sb.getConfigBool(CONFIG_DELETE_HOURLY_AFTER_DAILY, false)) { - deleteHourlyReports(day.getValue()); - log.info("runId=" + runId + " event=daily-report phase=delete-hourly day=" + day.getKey() + " sourceReports=" + day.getValue().size()); - } + // the hourly reports are now bundled into the daily report and must be deleted + deleteHourlyReports(day.getValue()); + log.info("runId=" + runId + " event=daily-report phase=delete-hourly day=" + day.getKey() + " sourceReports=" + day.getValue().size()); } catch (final IOException e) { failedReports++; log.warn("runId=" + runId + " event=daily-report phase=write result=failure day=" + day.getKey() + " file=" + LogRedaction.redact(dailyReportFile.getAbsolutePath()) + " errorClass=" + e.getClass().getName() + " reason=" + LogRedaction.redactMessage(e)); } } - log.info("runId=" + runId + " event=daily-reports phase=end result=success completeDays=" + completeDays.size() + " written=" + writtenReports.size() + " skippedExisting=" + skippedExisting + " failed=" + failedReports + " durationMs=" + elapsed(start)); + log.info("runId=" + runId + " event=daily-reports phase=end result=success pastDays=" + pastDays.size() + " written=" + writtenReports.size() + " skippedExisting=" + skippedExisting + " failed=" + failedReports + " durationMs=" + elapsed(start)); return writtenReports; } @@ -573,16 +574,25 @@ public class LogReportService { return text.toString(); } - private static Map<LocalDate, List<File>> completeHourlyReportSets(final File reportDirectory) { + /** + * Group all hourly reports of past days (strictly before today) by day, ordered + * chronologically within each day. Every past day that still has hourly reports is + * a compression candidate, no matter how many hours were actually reported - the + * peer may have been offline for parts of the day. Only the current day is excluded + * because its hourly reports are still accumulating. + */ + private static Map<LocalDate, List<File>> pastDayHourlyReportSets(final File reportDirectory) { final File[] files = reportDirectory.listFiles(); if (files == null || files.length == 0) return Collections.emptyMap(); + final LocalDate today = LocalDate.now(); final Map<LocalDate, List<File>> candidates = new TreeMap<>(); for (final File file : files) { if (!file.isFile()) continue; final LocalDateTime hour = parseHourlyReportFilename(file.getName()); if (hour == null) continue; final LocalDate day = hour.toLocalDate(); + if (!day.isBefore(today)) continue; List<File> dayFiles = candidates.get(day); if (dayFiles == null) { dayFiles = new ArrayList<>(); @@ -590,14 +600,11 @@ public class LogReportService { } dayFiles.add(file); } - - final Map<LocalDate, List<File>> complete = new TreeMap<>(); - for (final Map.Entry<LocalDate, List<File>> entry : candidates.entrySet()) { - if (entry.getValue().size() != 24) continue; - final List<File> ordered = hourlyFilesForDay(reportDirectory, entry.getKey()); - if (ordered.size() == 24) complete.put(entry.getKey(), ordered); + // the hourly filename pattern sorts chronologically + for (final List<File> dayFiles : candidates.values()) { + dayFiles.sort((a, b) -> a.getName().compareTo(b.getName())); } - return complete; + return candidates; } private static LocalDateTime parseHourlyReportFilename(final String filename) { @@ -644,16 +651,6 @@ public class LogReportService { return new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); } - private static List<File> hourlyFilesForDay(final File reportDirectory, final LocalDate day) { - final List<File> files = new ArrayList<>(24); - for (int hour = 0; hour < 24; hour++) { - final File file = new File(reportDirectory, hourlyReportFilename(day.atTime(hour, 0))); - if (!file.isFile()) return Collections.emptyList(); - files.add(file); - } - return files; - } - private static String dailyPrompt(final LocalDate day, final List<File> hourlyReports) throws IOException { final StringBuilder prompt = new StringBuilder(4096); prompt.append("Create one consolidated YaCy self-enhancement report for ") diff --git a/source/net/yacy/htroot/LogReports_p.java b/source/net/yacy/htroot/LogReports_p.java index 2a62e20d1..5ef559611 100644 --- a/source/net/yacy/htroot/LogReports_p.java +++ b/source/net/yacy/htroot/LogReports_p.java @@ -30,9 +30,8 @@ public class LogReports_p { final LogReportService service = new LogReportService(sb); final File reportDirectory = service.getReportDirectory(); - final int configuredMax = sb.getConfigInt(LogReportService.CONFIG_FEED_MAX_ENTRIES, LogReportService.DEFAULT_FEED_MAX_ENTRIES); - final int requestedMax = post == null ? Math.min(20, configuredMax) : post.getInt("count", Math.min(20, configuredMax)); - final int maxEntries = Math.max(0, Math.min(requestedMax, configuredMax)); + // fixed display limit; the navigation column shows at most this many reports + final int maxEntries = 100; sb.setConfig("ui.LogReports_p.visited", "true"); @@ -66,25 +65,47 @@ public class LogReports_p { final List<ReportEntry> reports = service.discoverReports(maxEntries); prop.put("modelConfigured", LogReportService.hasConfiguredLogReportModel() ? "1" : "0"); - prop.putNum("count", maxEntries); - prop.putNum("maxcount", configuredMax); prop.putHTML("reportdir", reportDirectory.getAbsolutePath()); prop.put("reportdirExists", reportDirectory.isDirectory() ? "1" : "0"); prop.put("reportdirMissing", reportDirectory.isDirectory() ? "0" : "1"); prop.put("jsonFeed", "api/logreports.json?count=" + maxEntries); prop.put("rssFeed", "api/logreports.rss?count=" + maxEntries); + // the report to show in the main view is selected by filename from the navigation + // column; matching against the discovered list only prevents path traversal. + // default is the newest report (first entry, list is sorted newest first) + final String requestedReport = post == null ? "" : post.get("report", ""); + ReportEntry selected = null; + for (final ReportEntry report : reports) { + if (report.filename.equals(requestedReport)) { + selected = report; + break; + } + } + if (selected == null && !reports.isEmpty()) selected = reports.get(0); + + // navigation list and selected report live inside the #(hasReports)# alternative, + // therefore all template keys carry the "hasReports_" prefix for (int i = 0; i < reports.size(); i++) { final ReportEntry report = reports.get(i); - prop.putHTML("reports_" + i + "_filename", report.filename); - prop.putHTML("reports_" + i + "_title", report.title); - prop.putHTML("reports_" + i + "_published", report.published.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); - prop.putHTML("reports_" + i + "_type", report.daily ? "daily" : "hourly"); - prop.put("reports_" + i + "_daily", report.daily ? "1" : "0"); - prop.putNum("reports_" + i + "_chars", report.content.length()); - prop.putHTML("reports_" + i + "_content", report.content); + final String p = "hasReports_reports_" + i + "_"; + prop.putHTML(p + "filename", report.filename); + prop.putHTML(p + "title", report.title); + prop.putHTML(p + "date", report.published.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))); + prop.putHTML(p + "type", report.daily ? "daily" : "hourly"); + prop.put(p + "selected", report == selected ? "1" : "0"); + } + prop.put("hasReports_reports", reports.size()); + + prop.put("hasReports_selectedReport", selected == null ? "0" : "1"); + if (selected != null) { + prop.putHTML("hasReports_selectedReport_filename", selected.filename); + prop.putHTML("hasReports_selectedReport_title", selected.title); + prop.putHTML("hasReports_selectedReport_published", selected.published.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); + prop.putHTML("hasReports_selectedReport_type", selected.daily ? "daily" : "hourly"); + prop.putNum("hasReports_selectedReport_chars", selected.content.length()); + prop.putHTML("hasReports_selectedReport_content", selected.content); } - prop.put("reports", reports.size()); prop.putNum("reportCount", reports.size()); prop.put("hasReports", reports.isEmpty() ? "0" : "1"); prop.put("noReports", reports.isEmpty() ? "1" : "0"); |
