diff options
Diffstat (limited to 'src/journal-remote/browse.html')
-rw-r--r-- | src/journal-remote/browse.html | 548 |
1 files changed, 548 insertions, 0 deletions
diff --git a/src/journal-remote/browse.html b/src/journal-remote/browse.html new file mode 100644 index 0000000..4fe2cd8 --- /dev/null +++ b/src/journal-remote/browse.html @@ -0,0 +1,548 @@ +<!DOCTYPE html> +<!-- SPDX-License-Identifier: LGPL-2.1-or-later --> +<html> +<head> + <title>Journal</title> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> + <style type="text/css"> + div#divlogs, div#diventry { + font-family: monospace; + font-size: 7pt; + background-color: #ffffff; + padding: 1em; + margin: 2em 0em; + border-radius: 10px 10px 10px 10px; + border: 1px solid threedshadow; + white-space: nowrap; + overflow-x: scroll; + } + div#diventry { + display: none; + } + div#divlogs { + display: block; + } + body { + background-color: #ededed; + color: #313739; + font: message-box; + margin: 3em; + } + td.timestamp { + text-align: right; + border-right: 1px dotted lightgrey; + padding-right: 5px; + } + td.process { + border-right: 1px dotted lightgrey; + padding-left: 5px; + padding-right: 5px; + } + td.message { + padding-left: 5px; + } + td.message > a:link, td.message > a:visited { + text-decoration: none; + color: #313739; + } + td.message-error { + padding-left: 5px; + color: red; + font-weight: bold; + } + td.message-error > a:link, td.message-error > a:visited { + text-decoration: none; + color: red; + } + td.message-highlight { + padding-left: 5px; + font-weight: bold; + } + td.message-highlight > a:link, td.message-highlight > a:visited { + text-decoration: none; + color: #313739; + } + td > a:hover, td > a:active { + text-decoration: underline; + color: #c13739; + } + table#tablelogs, table#tableentry { + border-collapse: collapse; + } + td.field { + text-align: right; + border-right: 1px dotted lightgrey; + padding-right: 5px; + } + td.data { + padding-left: 5px; + } + div#keynav { + text-align: center; + font-size: 7pt; + color: #818789; + padding-top: 2em; + } + span.key { + font-weight: bold; + color: #313739; + } + div#buttonnav { + text-align: center; + } + button { + font-size: 18pt; + font-weight: bold; + width: 2em; + height: 2em; + } + div#filternav { + text-align: center; + } + select { + width: 50em; + } + </style> +</head> + +<body> + <!-- TODO: + - live display + - show red lines for reboots --> + + <h1 id="title"></h1> + + <div id="os"></div> + <div id="virtualization"></div> + <div id="cutoff"></div> + <div id="machine"></div> + <div id="usage"></div> + <div id="showing"></div> + + <div id="filternav"> + <select id="filter" onchange="onFilterChange(this);" onfocus="onFilterFocus(this);"> + <option>No filter</option> + </select> + + <input id="boot" type="checkbox" onchange="onBootChange(this);">Only current boot</input> + </div> + + <div id="divlogs"><table id="tablelogs"></table></div> + <a name="entry"></a> + <div id="diventry"><table id="tableentry"></table></div> + + <div id="buttonnav"> + <button id="head" onclick="entriesLoadHead();" title="First Page">⇤</button> + <button id="previous" type="button" onclick="entriesLoadPrevious();" title="Previous Page"/>←</button> + <button id="next" type="button" onclick="entriesLoadNext();" title="Next Page"/>→</button> + <button id="tail" type="button" onclick="entriesLoadTail();" title="Last Page"/>⇥</button> + + <button id="more" type="button" onclick="entriesMore();" title="More Entries"/>+</button> + <button id="less" type="button" onclick="entriesLess();" title="Fewer Entries"/>-</button> + </div> + + <div id="keynav"> + <span class="key">g</span>: First Page + <span class="key">←, k, BACKSPACE</span>: Previous Page + <span class="key">→, j, SPACE</span>: Next Page + <span class="key">G</span>: Last Page + <span class="key">+</span>: More entries + <span class="key">-</span>: Fewer entries + </div> + + <script type="text/javascript"> + var first_cursor = null; + var last_cursor = null; + + function getNEntries() { + var n; + n = localStorage["n_entries"]; + if (n == null) + return 50; + n = parseInt(n); + if (n < 10) + return 10; + if (n > 1000) + return 1000; + return n; + } + + function showNEntries(n) { + var showing = document.getElementById("showing"); + showing.innerHTML = "Showing <b>" + n.toString() + "</b> entries."; + } + + function setNEntries(n) { + if (n < 10) + return 10; + if (n > 1000) + return 1000; + localStorage["n_entries"] = n.toString(); + showNEntries(n); + } + + function machineLoad() { + var request = new XMLHttpRequest(); + request.open("GET", "machine"); + request.onreadystatechange = machineOnResult; + request.setRequestHeader("Accept", "application/json"); + request.send(null); + } + + function formatBytes(u) { + if (u >= 1024*1024*1024*1024) + return (u/1024/1024/1024/1024).toFixed(1) + " TiB"; + else if (u >= 1024*1024*1024) + return (u/1024/1024/1024).toFixed(1) + " GiB"; + else if (u >= 1024*1024) + return (u/1024/1024).toFixed(1) + " MiB"; + else if (u >= 1024) + return (u/1024).toFixed(1) + " KiB"; + else + return u.toString() + " B"; + } + + function escapeHTML(s) { + return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); + } + + function machineOnResult(event) { + if ((event.currentTarget.readyState != 4) || + (event.currentTarget.status != 200 && event.currentTarget.status != 0)) + return; + + var d = JSON.parse(event.currentTarget.responseText); + + var title = document.getElementById("title"); + title.innerHTML = 'Journal of ' + escapeHTML(d.hostname); + document.title = 'Journal of ' + escapeHTML(d.hostname); + + var machine = document.getElementById("machine"); + machine.innerHTML = 'Machine ID is <b>' + d.machine_id + '</b>, current boot ID is <b>' + d.boot_id + '</b>.'; + + var cutoff = document.getElementById("cutoff"); + var from = new Date(parseInt(d.cutoff_from_realtime) / 1000); + var to = new Date(parseInt(d.cutoff_to_realtime) / 1000); + cutoff.innerHTML = 'Journal begins at <b>' + from.toLocaleString() + '</b> and ends at <b>' + to.toLocaleString() + '</b>.'; + + var usage = document.getElementById("usage"); + usage.innerHTML = 'Disk usage is <b>' + formatBytes(parseInt(d.usage)) + '</b>.'; + + var os = document.getElementById("os"); + os.innerHTML = 'Operating system is <b>' + escapeHTML(d.os_pretty_name) + '</b>.'; + + var virtualization = document.getElementById("virtualization"); + virtualization.innerHTML = d.virtualization == "bare" ? "Running on <b>bare metal</b>." : "Running on virtualization <b>" + escapeHTML(d.virtualization) + "</b>."; + } + + function entriesLoad(range) { + + if (range == null) { + if (localStorage["cursor"] != null && localStorage["cursor"] != "") + range = localStorage["cursor"] + ":0"; + else + range = ""; + } + + var url = "entries"; + + if (localStorage["filter"] != "" && localStorage["filter"] != null) { + url += "?_SYSTEMD_UNIT=" + escape(localStorage["filter"]); + + if (localStorage["boot"] == "1") + url += "&boot"; + } else { + if (localStorage["boot"] == "1") + url += "?boot"; + } + + var request = new XMLHttpRequest(); + request.open("GET", url); + request.onreadystatechange = entriesOnResult; + request.setRequestHeader("Accept", "application/json"); + request.setRequestHeader("Range", "entries=" + range + ":" + getNEntries().toString()); + request.send(null); + } + + function entriesLoadNext() { + if (last_cursor == null) + entriesLoad(""); + else + entriesLoad(last_cursor + ":1"); + } + + function entriesLoadPrevious() { + if (first_cursor == null) + entriesLoad(""); + else + entriesLoad(first_cursor + ":-" + getNEntries().toString()); + } + + function entriesLoadHead() { + entriesLoad(""); + } + + function entriesLoadTail() { + entriesLoad(":-" + getNEntries().toString()); + } + + function entriesOnResult(event) { + + if ((event.currentTarget.readyState != 4) || + (event.currentTarget.status != 200 && event.currentTarget.status != 0)) + return; + + var logs = document.getElementById("tablelogs"); + + var lc = null; + var fc = null; + + var i, l = event.currentTarget.responseText.split('\n'); + + if (l.length <= 1) { + logs.innerHTML = '<tbody><tr><td colspan="3"><i>No further entries...</i></td></tr></tbody>'; + return; + } + + var buf = ''; + + for (i in l) { + if (l[i] == '') + continue; + + var d = JSON.parse(l[i]); + if (d.MESSAGE == undefined || d.__CURSOR == undefined) + continue; + + if (fc == null) + fc = d.__CURSOR; + lc = d.__CURSOR; + + var priority; + if (d.PRIORITY != undefined) + priority = parseInt(d.PRIORITY); + else + priority = 6; + + var clazz; + if (priority <= 3) + clazz = "message-error"; + else if (priority <= 5) + clazz = "message-highlight"; + else + clazz = "message"; + + buf += '<tr><td class="timestamp">'; + + if (d.__REALTIME_TIMESTAMP != undefined) { + var timestamp = new Date(parseInt(d.__REALTIME_TIMESTAMP) / 1000); + buf += timestamp.toLocaleString(); + } + + buf += '</td><td class="process">'; + + if (d.SYSLOG_IDENTIFIER != undefined) + buf += escapeHTML(d.SYSLOG_IDENTIFIER); + else if (d._COMM != undefined) + buf += escapeHTML(d._COMM); + + if (d._PID != undefined) + buf += "[" + escapeHTML(d._PID) + "]"; + else if (d.SYSLOG_PID != undefined) + buf += "[" + escapeHTML(d.SYSLOG_PID) + "]"; + + buf += '</td><td class="' + clazz + '"><a href="#entry" onclick="onMessageClick(\'' + d.__CURSOR + '\');">'; + + if (d.MESSAGE == null) + buf += "[blob data]"; + else if (d.MESSAGE instanceof Array) + buf += "[" + formatBytes(d.MESSAGE.length) + " blob data]"; + else + buf += escapeHTML(d.MESSAGE); + + buf += '</a></td></tr>'; + } + + logs.innerHTML = '<tbody>' + buf + '</tbody>'; + + if (fc != null) { + first_cursor = fc; + localStorage["cursor"] = fc; + } + if (lc != null) + last_cursor = lc; + } + + function entriesMore() { + setNEntries(getNEntries() + 10); + entriesLoad(first_cursor); + } + + function entriesLess() { + setNEntries(getNEntries() - 10); + entriesLoad(first_cursor); + } + + function onResultMessageClick(event) { + if ((event.currentTarget.readyState != 4) || + (event.currentTarget.status != 200 && event.currentTarget.status != 0)) + return; + + var d = JSON.parse(event.currentTarget.responseText); + + document.getElementById("diventry").style.display = "block"; + var entry = document.getElementById("tableentry"); + + var buf = ""; + for (var key in d) { + var data = d[key]; + + if (data == null) + data = "[blob data]"; + else if (data instanceof Array) + data = "[" + formatBytes(data.length) + " blob data]"; + else + data = escapeHTML(data); + + buf += '<tr><td class="field">' + key + '</td><td class="data">' + data + '</td></tr>'; + } + entry.innerHTML = '<tbody>' + buf + '</tbody>'; + } + + function onMessageClick(t) { + var request = new XMLHttpRequest(); + request.open("GET", "entries?discrete"); + request.onreadystatechange = onResultMessageClick; + request.setRequestHeader("Accept", "application/json"); + request.setRequestHeader("Range", "entries=" + t + ":0:1"); + request.send(null); + } + + function onKeyUp(event) { + switch (event.keyCode) { + case 8: + case 37: + case 75: + entriesLoadPrevious(); + break; + case 32: + case 39: + case 74: + entriesLoadNext(); + break; + + case 71: + if (event.shiftKey) + entriesLoadTail(); + else + entriesLoadHead(); + break; + case 171: + entriesMore(); + break; + case 173: + entriesLess(); + break; + } + } + + function onMouseWheel(event) { + if (event.detail < 0 || event.wheelDelta > 0) + entriesLoadPrevious(); + else + entriesLoadNext(); + } + + function onResultFilterFocus(event) { + if ((event.currentTarget.readyState != 4) || + (event.currentTarget.status != 200 && event.currentTarget.status != 0)) + return; + + var f = document.getElementById("filter"); + + var l = event.currentTarget.responseText.split('\n'); + var buf = '<option>No filter</option>'; + var j = -1; + + for (i in l) { + + if (l[i] == '') + continue; + + var d = JSON.parse(l[i]); + if (d._SYSTEMD_UNIT == undefined) + continue; + + buf += '<option value="' + escape(d._SYSTEMD_UNIT) + '">' + escapeHTML(d._SYSTEMD_UNIT) + '</option>'; + + if (d._SYSTEMD_UNIT == localStorage["filter"]) + j = i; + } + + if (j < 0) { + if (localStorage["filter"] != null && localStorage["filter"] != "") { + buf += '<option value="' + escape(localStorage["filter"]) + '">' + escapeHTML(localStorage["filter"]) + '</option>'; + j = i + 1; + } else + j = 0; + } + + f.innerHTML = buf; + f.selectedIndex = j; + } + + function onFilterFocus(w) { + var request = new XMLHttpRequest(); + request.open("GET", "fields/_SYSTEMD_UNIT"); + request.onreadystatechange = onResultFilterFocus; + request.setRequestHeader("Accept", "application/json"); + request.send(null); + } + + function onFilterChange(w) { + if (w.selectedIndex <= 0) + localStorage["filter"] = ""; + else + localStorage["filter"] = unescape(w.options[w.selectedIndex].value); + + entriesLoadHead(); + } + + function onBootChange(w) { + localStorage["boot"] = w.checked ? "1" : "0"; + entriesLoadHead(); + } + + function initFilter() { + var f = document.getElementById("filter"); + + var buf = '<option>No filter</option>'; + + var filter = localStorage["filter"]; + var j; + if (filter != null && filter != "") { + buf += '<option value="' + escape(filter) + '">' + escapeHTML(filter) + '</option>'; + j = 1; + } else + j = 0; + + f.innerHTML = buf; + f.selectedIndex = j; + } + + function installHandlers() { + document.onkeyup = onKeyUp; + + var logs = document.getElementById("divlogs"); + logs.addEventListener("mousewheel", onMouseWheel, false); + logs.addEventListener("DOMMouseScroll", onMouseWheel, false); + } + + machineLoad(); + entriesLoad(null); + showNEntries(getNEntries()); + initFilter(); + installHandlers(); + </script> +</body> +</html> |