1<!DOCTYPE html>
2<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
3<html>
4<head>
5        <title>Journal</title>
6        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
7        <style type="text/css">
8                div#divlogs, div#diventry {
9                        font-family: monospace;
10                        font-size: 7pt;
11                        background-color: #ffffff;
12                        padding: 1em;
13                        margin: 2em 0em;
14                        border-radius: 10px 10px 10px 10px;
15                        border: 1px solid threedshadow;
16                        white-space: nowrap;
17                        overflow-x: scroll;
18                }
19                div#diventry {
20                        display: none;
21                }
22                div#divlogs {
23                        display: block;
24                }
25                body {
26                        background-color: #ededed;
27                        color: #313739;
28                        font: message-box;
29                        margin: 3em;
30                }
31                td.timestamp {
32                        text-align: right;
33                        border-right: 1px dotted lightgrey;
34                        padding-right: 5px;
35                }
36                td.process {
37                        border-right: 1px dotted lightgrey;
38                        padding-left: 5px;
39                        padding-right: 5px;
40                }
41                td.message {
42                        padding-left: 5px;
43                }
44                td.message > a:link, td.message > a:visited {
45                        text-decoration: none;
46                        color: #313739;
47                }
48                td.message-error {
49                        padding-left: 5px;
50                        color: red;
51                        font-weight: bold;
52                }
53                td.message-error > a:link, td.message-error > a:visited {
54                        text-decoration: none;
55                        color: red;
56                }
57                td.message-highlight {
58                        padding-left: 5px;
59                        font-weight: bold;
60                }
61                td.message-highlight > a:link, td.message-highlight > a:visited {
62                        text-decoration: none;
63                        color: #313739;
64                }
65                td > a:hover, td > a:active {
66                        text-decoration: underline;
67                        color: #c13739;
68                }
69                table#tablelogs, table#tableentry {
70                        border-collapse: collapse;
71                }
72                td.field {
73                        text-align: right;
74                        border-right: 1px dotted lightgrey;
75                        padding-right: 5px;
76                }
77                td.data {
78                        padding-left: 5px;
79                }
80                div#keynav {
81                        text-align: center;
82                        font-size: 7pt;
83                        color: #818789;
84                        padding-top: 2em;
85                }
86                span.key {
87                        font-weight: bold;
88                        color: #313739;
89                }
90                div#buttonnav {
91                        text-align: center;
92                }
93                button {
94                        font-size: 18pt;
95                        font-weight: bold;
96                        width: 2em;
97                        height: 2em;
98                }
99                div#filternav {
100                        text-align: center;
101                }
102                select {
103                        width: 50em;
104                }
105        </style>
106</head>
107
108<body>
109        <!-- TODO:
110                - live display
111                - show red lines for reboots -->
112
113        <h1 id="title"></h1>
114
115        <div id="os"></div>
116        <div id="virtualization"></div>
117        <div id="cutoff"></div>
118        <div id="machine"></div>
119        <div id="usage"></div>
120        <div id="showing"></div>
121
122        <div id="filternav">
123                <select id="filter" onchange="onFilterChange(this);" onfocus="onFilterFocus(this);">
124                        <option>No filter</option>
125                </select>
126                &nbsp;&nbsp;&nbsp;&nbsp;
127                <input id="boot" type="checkbox" onchange="onBootChange(this);">Only current boot</input>
128        </div>
129
130        <div id="divlogs"><table id="tablelogs"></table></div>
131        <a name="entry"></a>
132        <div id="diventry"><table id="tableentry"></table></div>
133
134        <div id="buttonnav">
135                <button id="head" onclick="entriesLoadHead();" title="First Page">&#8676;</button>
136                <button id="previous" type="button" onclick="entriesLoadPrevious();" title="Previous Page"/>&#8592;</button>
137                <button id="next" type="button" onclick="entriesLoadNext();" title="Next Page"/>&#8594;</button>
138                <button id="tail" type="button" onclick="entriesLoadTail();" title="Last Page"/>&#8677;</button>
139                &nbsp;&nbsp;&nbsp;&nbsp;
140                <button id="more" type="button" onclick="entriesMore();" title="More Entries"/>+</button>
141                <button id="less" type="button" onclick="entriesLess();" title="Fewer Entries"/>-</button>
142        </div>
143
144        <div id="keynav">
145                <span class="key">g</span>: First Page &nbsp;&nbsp;&nbsp;&nbsp;
146                <span class="key">&#8592;, k, BACKSPACE</span>: Previous Page &nbsp;&nbsp;&nbsp;&nbsp;
147                <span class="key">&#8594;, j, SPACE</span>: Next Page &nbsp;&nbsp;&nbsp;&nbsp;
148                <span class="key">G</span>: Last Page &nbsp;&nbsp;&nbsp;&nbsp;
149                <span class="key">+</span>: More entries &nbsp;&nbsp;&nbsp;&nbsp;
150                <span class="key">-</span>: Fewer entries
151        </div>
152
153        <script type="text/javascript">
154                var first_cursor = null;
155                var last_cursor = null;
156
157                function getNEntries() {
158                        var n;
159                        n = localStorage["n_entries"];
160                        if (n == null)
161                                return 50;
162                        n = parseInt(n);
163                        if (n < 10)
164                                return 10;
165                        if (n > 1000)
166                                return 1000;
167                        return n;
168                }
169
170                function showNEntries(n) {
171                        var showing = document.getElementById("showing");
172                        showing.innerHTML = "Showing <b>" + n.toString() + "</b> entries.";
173                }
174
175                function setNEntries(n) {
176                        if (n < 10)
177                                return 10;
178                        if (n > 1000)
179                                return 1000;
180                        localStorage["n_entries"] = n.toString();
181                        showNEntries(n);
182                }
183
184                function machineLoad() {
185                        var request = new XMLHttpRequest();
186                        request.open("GET", "machine");
187                        request.onreadystatechange = machineOnResult;
188                        request.setRequestHeader("Accept", "application/json");
189                        request.send(null);
190                }
191
192                function formatBytes(u) {
193                        if (u >= 1024*1024*1024*1024)
194                                return (u/1024/1024/1024/1024).toFixed(1) + " TiB";
195                        else if (u >= 1024*1024*1024)
196                                return (u/1024/1024/1024).toFixed(1) + " GiB";
197                        else if (u >= 1024*1024)
198                                return (u/1024/1024).toFixed(1) + " MiB";
199                        else if (u >= 1024)
200                                return (u/1024).toFixed(1) + " KiB";
201                        else
202                                return u.toString() + " B";
203                }
204
205                function escapeHTML(s) {
206                        return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
207                }
208
209                function machineOnResult(event) {
210                        if ((event.currentTarget.readyState != 4) ||
211                                (event.currentTarget.status != 200 && event.currentTarget.status != 0))
212                                return;
213
214                        var d = JSON.parse(event.currentTarget.responseText);
215
216                        var title = document.getElementById("title");
217                        title.innerHTML = 'Journal of ' + escapeHTML(d.hostname);
218                        document.title = 'Journal of ' + escapeHTML(d.hostname);
219
220                        var machine = document.getElementById("machine");
221                        machine.innerHTML = 'Machine ID is <b>' + d.machine_id + '</b>, current boot ID is <b>' + d.boot_id + '</b>.';
222
223                        var cutoff = document.getElementById("cutoff");
224                        var from = new Date(parseInt(d.cutoff_from_realtime) / 1000);
225                        var to = new Date(parseInt(d.cutoff_to_realtime) / 1000);
226                        cutoff.innerHTML = 'Journal begins at <b>' + from.toLocaleString() + '</b> and ends at <b>' + to.toLocaleString() + '</b>.';
227
228                        var usage = document.getElementById("usage");
229                        usage.innerHTML = 'Disk usage is <b>' + formatBytes(parseInt(d.usage)) + '</b>.';
230
231                        var os = document.getElementById("os");
232                        os.innerHTML = 'Operating system is <b>' + escapeHTML(d.os_pretty_name) + '</b>.';
233
234                        var virtualization = document.getElementById("virtualization");
235                        virtualization.innerHTML = d.virtualization == "bare" ? "Running on <b>bare metal</b>." : "Running on virtualization <b>" + escapeHTML(d.virtualization) + "</b>.";
236                }
237
238                function entriesLoad(range) {
239
240                        if (range == null) {
241                                if (localStorage["cursor"] != null && localStorage["cursor"] != "")
242                                        range = localStorage["cursor"] + ":0";
243                                else
244                                        range = "";
245                        }
246
247                        var url = "entries";
248
249                        if (localStorage["filter"] != "" && localStorage["filter"] != null) {
250                                url += "?_SYSTEMD_UNIT=" + escape(localStorage["filter"]);
251
252                                if (localStorage["boot"] == "1")
253                                        url += "&boot";
254                        } else {
255                                if (localStorage["boot"] == "1")
256                                        url += "?boot";
257                        }
258
259                        var request = new XMLHttpRequest();
260                        request.open("GET", url);
261                        request.onreadystatechange = entriesOnResult;
262                        request.setRequestHeader("Accept", "application/json");
263                        request.setRequestHeader("Range", "entries=" + range + ":" + getNEntries().toString());
264                        request.send(null);
265                }
266
267                function entriesLoadNext() {
268                        if (last_cursor == null)
269                                entriesLoad("");
270                        else
271                                entriesLoad(last_cursor + ":1");
272                }
273
274                function entriesLoadPrevious() {
275                        if (first_cursor == null)
276                                entriesLoad("");
277                        else
278                                entriesLoad(first_cursor + ":-" + getNEntries().toString());
279                }
280
281                function entriesLoadHead() {
282                        entriesLoad("");
283                }
284
285                function entriesLoadTail() {
286                        entriesLoad(":-" + getNEntries().toString());
287                }
288
289                function entriesOnResult(event) {
290
291                        if ((event.currentTarget.readyState != 4) ||
292                                (event.currentTarget.status != 200 && event.currentTarget.status != 0))
293                                return;
294
295                        var logs = document.getElementById("tablelogs");
296
297                        var lc = null;
298                        var fc = null;
299
300                        var i, l = event.currentTarget.responseText.split('\n');
301
302                        if (l.length <= 1) {
303                                logs.innerHTML = '<tbody><tr><td colspan="3"><i>No further entries...</i></td></tr></tbody>';
304                                return;
305                        }
306
307                        var buf = '';
308
309                        for (i in l) {
310                                if (l[i] == '')
311                                        continue;
312
313                                var d = JSON.parse(l[i]);
314                                if (d.MESSAGE == undefined || d.__CURSOR == undefined)
315                                        continue;
316
317                                if (fc == null)
318                                        fc = d.__CURSOR;
319                                lc = d.__CURSOR;
320
321                                var priority;
322                                if (d.PRIORITY != undefined)
323                                        priority = parseInt(d.PRIORITY);
324                                else
325                                        priority = 6;
326
327                                var clazz;
328                                if (priority <= 3)
329                                        clazz = "message-error";
330                                else if (priority <= 5)
331                                        clazz = "message-highlight";
332                                else
333                                        clazz = "message";
334
335                                buf += '<tr><td class="timestamp">';
336
337                                if (d.__REALTIME_TIMESTAMP != undefined) {
338                                        var timestamp = new Date(parseInt(d.__REALTIME_TIMESTAMP) / 1000);
339                                        buf += timestamp.toLocaleString();
340                                }
341
342                                buf += '</td><td class="process">';
343
344                                if (d.SYSLOG_IDENTIFIER != undefined)
345                                        buf += escapeHTML(d.SYSLOG_IDENTIFIER);
346                                else if (d._COMM != undefined)
347                                        buf += escapeHTML(d._COMM);
348
349                                if (d._PID != undefined)
350                                        buf += "[" + escapeHTML(d._PID) + "]";
351                                else if (d.SYSLOG_PID != undefined)
352                                        buf += "[" + escapeHTML(d.SYSLOG_PID) + "]";
353
354                                buf += '</td><td class="' + clazz + '"><a href="#entry" onclick="onMessageClick(\'' + d.__CURSOR + '\');">';
355
356                                if (d.MESSAGE == null)
357                                        buf += "[blob data]";
358                                else if (d.MESSAGE instanceof Array)
359                                        buf += "[" + formatBytes(d.MESSAGE.length) + " blob data]";
360                                else
361                                        buf += escapeHTML(d.MESSAGE);
362
363                                buf += '</a></td></tr>';
364                        }
365
366                        logs.innerHTML = '<tbody>' + buf + '</tbody>';
367
368                        if (fc != null) {
369                                first_cursor = fc;
370                                localStorage["cursor"] = fc;
371                        }
372                        if (lc != null)
373                                last_cursor = lc;
374                }
375
376                function entriesMore() {
377                        setNEntries(getNEntries() + 10);
378                        entriesLoad(first_cursor);
379                }
380
381                function entriesLess() {
382                        setNEntries(getNEntries() - 10);
383                        entriesLoad(first_cursor);
384                }
385
386                function onResultMessageClick(event) {
387                        if ((event.currentTarget.readyState != 4) ||
388                                (event.currentTarget.status != 200 && event.currentTarget.status != 0))
389                                return;
390
391                        var d = JSON.parse(event.currentTarget.responseText);
392
393                        document.getElementById("diventry").style.display = "block";
394                        var entry = document.getElementById("tableentry");
395
396                        var buf = "";
397                        for (var key in d) {
398                                var data = d[key];
399
400                                if (data == null)
401                                        data = "[blob data]";
402                                else if (data instanceof Array)
403                                        data = "[" + formatBytes(data.length) + " blob data]";
404                                else
405                                        data = escapeHTML(data);
406
407                                buf += '<tr><td class="field">' + key + '</td><td class="data">' + data + '</td></tr>';
408                        }
409                        entry.innerHTML = '<tbody>' + buf + '</tbody>';
410                }
411
412                function onMessageClick(t) {
413                        var request = new XMLHttpRequest();
414                        request.open("GET", "entries?discrete");
415                        request.onreadystatechange = onResultMessageClick;
416                        request.setRequestHeader("Accept", "application/json");
417                        request.setRequestHeader("Range", "entries=" + t + ":0:1");
418                        request.send(null);
419                }
420
421                function onKeyUp(event) {
422                        switch (event.keyCode) {
423                                case 8:
424                                case 37:
425                                case 75:
426                                        entriesLoadPrevious();
427                                        break;
428                                case 32:
429                                case 39:
430                                case 74:
431                                        entriesLoadNext();
432                                        break;
433
434                                case 71:
435                                        if (event.shiftKey)
436                                                entriesLoadTail();
437                                        else
438                                                entriesLoadHead();
439                                        break;
440                                case 171:
441                                        entriesMore();
442                                        break;
443                                case 173:
444                                        entriesLess();
445                                        break;
446                        }
447                }
448
449                function onMouseWheel(event) {
450                        if (event.detail < 0 || event.wheelDelta > 0)
451                                entriesLoadPrevious();
452                        else
453                                entriesLoadNext();
454                }
455
456                function onResultFilterFocus(event) {
457                        if ((event.currentTarget.readyState != 4) ||
458                                (event.currentTarget.status != 200 && event.currentTarget.status != 0))
459                                return;
460
461                        var f = document.getElementById("filter");
462
463                        var l = event.currentTarget.responseText.split('\n');
464                        var buf = '<option>No filter</option>';
465                        var j = -1;
466
467                        for (i in l) {
468
469                                if (l[i] == '')
470                                      continue;
471
472                                var d = JSON.parse(l[i]);
473                                if (d._SYSTEMD_UNIT == undefined)
474                                      continue;
475
476                                buf += '<option value="' + escape(d._SYSTEMD_UNIT) + '">' + escapeHTML(d._SYSTEMD_UNIT) + '</option>';
477
478                                if (d._SYSTEMD_UNIT == localStorage["filter"])
479                                        j = i;
480                        }
481
482                        if (j < 0) {
483                                if (localStorage["filter"] != null && localStorage["filter"] != "") {
484                                          buf += '<option value="' + escape(localStorage["filter"]) + '">' + escapeHTML(localStorage["filter"]) + '</option>';
485                                          j = i + 1;
486                                } else
487                                          j = 0;
488                        }
489
490                        f.innerHTML = buf;
491                        f.selectedIndex = j;
492                }
493
494                function onFilterFocus(w) {
495                        var request = new XMLHttpRequest();
496                        request.open("GET", "fields/_SYSTEMD_UNIT");
497                        request.onreadystatechange = onResultFilterFocus;
498                        request.setRequestHeader("Accept", "application/json");
499                        request.send(null);
500                }
501
502                function onFilterChange(w) {
503                        if (w.selectedIndex <= 0)
504                                localStorage["filter"] = "";
505                        else
506                                localStorage["filter"] = unescape(w.options[w.selectedIndex].value);
507
508                        entriesLoadHead();
509                }
510
511                function onBootChange(w) {
512                        localStorage["boot"] = w.checked ? "1" : "0";
513                        entriesLoadHead();
514                }
515
516                function initFilter() {
517                        var f = document.getElementById("filter");
518
519                        var buf = '<option>No filter</option>';
520
521                        var filter = localStorage["filter"];
522                        var j;
523                        if (filter != null && filter != "") {
524                                buf += '<option value="' + escape(filter) + '">' + escapeHTML(filter) + '</option>';
525                                j = 1;
526                        } else
527                                j = 0;
528
529                        f.innerHTML = buf;
530                        f.selectedIndex = j;
531                }
532
533                function installHandlers() {
534                        document.onkeyup = onKeyUp;
535
536                        var logs = document.getElementById("divlogs");
537                        logs.addEventListener("mousewheel", onMouseWheel, false);
538                        logs.addEventListener("DOMMouseScroll", onMouseWheel, false);
539                }
540
541                machineLoad();
542                entriesLoad(null);
543                showNEntries(getNEntries());
544                initFilter();
545                installHandlers();
546        </script>
547</body>
548</html>
549