You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

435 lines
15 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<link href="./tailwind.min.css" rel="stylesheet" type="text/css" />
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/dayjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/plugin/calendar.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/plugin/relativeTime.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/plugin/updateLocale.js"></script>
<script>
globalThis.dayjs.extend(window.dayjs_plugin_calendar);
globalThis.dayjs.extend(window.dayjs_plugin_updateLocale);
globalThis.dayjs.extend(window.dayjs_plugin_relativeTime, {
thresholds: [
{ l: "s", r: 1 },
{ l: "ss", r: 59, d: "second" },
{ l: "m", r: 1 },
{ l: "mm", r: 59, d: "minute" },
{ l: "h", r: 1 },
{ l: "hh", r: 23, d: "hour" },
{ l: "d", r: 1 },
{ l: "dd", r: 29, d: "day" },
{ l: "M", r: 1 },
{ l: "MM", r: 11, d: "month" },
{ l: "y", r: 1 },
{ l: "yy", d: "year" },
],
});
dayjs.updateLocale("en", {
relativeTime: {
future: "in %s",
past: "%s",
s: "a second",
ss: "%d seconds",
m: "a minute",
mm: "%d minutes",
h: "an hour",
hh: "%d hours",
d: "a day",
dd: "%d days",
M: "a month",
MM: "%d months",
y: "a year",
yy: "%d years",
},
});
</script>
<link href="https://unpkg.com/css.gg/icons/all.css" rel="stylesheet" />
<!-- prettier-ignore-attribute -->
<script>
document.addEventListener("alpine:init", () => {
Alpine.store("data", {
// prettier-ignore
logs: {{.List}},
// prettier-ignore
apps: {{.AllowedApps}},
});
Alpine.data("body", () => ({
logsApp: [],
page: 20,
pageEnd: false,
selectedLog: null,
selectedAppID: null,
darkMode: null,
init() {
let appIDs = Object.keys(this.$store.data.apps);
if (appIDs.length) {
this.selectApp(appIDs[0]);
}
if (
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
) {
this.darkMode = true;
}
},
selectApp(id) {
this.selectedAppID = id;
this.selectedLog = null;
this.page = 20;
this.pageEnd = false;
this.setList();
},
selectLog(item) {
this.selectedLog = item
document.getElementById("my-drawer").checked = false
},
setList() {
const id = this.selectedAppID;
if (!id) {
this.logsApp = [];
}
const dataLogsLen = this.$store.data.logs.length;
let i = 0,
arr = [];
while (i < dataLogsLen && arr.length < this.page) {
let item = this.$store.data.logs[i];
if (item.app == id) arr.push(item);
i++;
if (i == dataLogsLen) this.pageEnd = true;
}
this.logsApp = arr;
},
next() {
this.page += 10;
this.setList();
},
handleScroll(e) {
if (!this.pageEnd && this.atBottom(e.target)) {
this.$dispatch("scrolled-to-bottom");
}
},
// utils
day(timestamp) {
const d = globalThis.dayjs(timestamp);
const now = globalThis.dayjs()
if (now.diff(d, "day") < 1) {
return d.from(now, true)
}
return d.calendar(null, {
nextWeek: "YYYY-MM-DD",
nextDay: "YYYY-MM-DD",
sameDay: "HH:mm ss",
lastDay: "ddd HH:mm",
lastWeek: "ddd HH:mm",
sameElse: "YYYY-MM-DD",
});
},
atBottom(el) {
let sh = el.scrollHeight,
st = el.scrollTop,
ht = el.offsetHeight;
if (ht == 0) return true;
return st == sh - ht;
},
async countOccurrence(hash) {
if (!hash) return '0'
let i = 0,
count = 0;
const dataLogsLen = this.$store.data.logs.length;
while (i < dataLogsLen && count <= 100 ) {
let item = this.$store.data.logs[i];
if (item.hash == hash) count++;
i++;
}
if (count === 100) return '99+'
return ''+count
},
}));
});
</script>
<script
defer
src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"
></script>
<link rel="icon" type="image/x-icon" href="./logo.webp" />
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body
class="bg-base-200"
x-data="body"
x-bind:data-theme="darkMode ===true ? 'dark': darkMode === false ?'light':''"
>
<div class="drawer drawer-mobile">
<input id="my-drawer" type="checkbox" class="drawer-toggle" />
<div class="drawer-content bg-base-200">
<nav class="navbar bg-base-100">
<div class="navbar-start">
<label class="btn btn-square btn-ghost lg:hidden" for="my-drawer">
<i class="gg-menu"></i>
</label>
<img
class="h-12 w-12 md:ml-2"
alt="the eye of the golang hamster mascot looking through a telescope at stars"
src="./logo.webp"
/>
<span class="font-bold text-xl ml-2">GoScope2</span>
</div>
<div class="navbar-end">
<button
class="mr-4 btn btn-circle bg-base-content text-base-100 hover:text-base-content hover:bg-base-100 btn-sm"
@click.prevent="darkMode =!darkMode"
>
<i class="gg-drop-invert"></i>
</button>
<div class="dropdown dropdown-end">
<button class="btn btn-square btn-primary rounded-xl">
<i class="gg-album"></i>
</button>
<ul
tabindex="0"
class="menu menu-compact dropdown-content mt-3 p-2 shadow bg-base-100 rounded-box w-52"
>
<template x-for="(app, appID) in $store.data.apps">
<li>
<a
:class="{'active': selectedAppID == appID}"
@click.prevent="selectApp(appID)"
x-text="app"
></a>
</li>
</template>
</ul>
</div>
</div>
</nav>
<main class="p-4">
<template x-if="selectedLog">
<div class="max-w-screen-md mr-auto bg-base-100 rounded p-4">
<div class="mb-4 flex items-center rounded-lg bg-primary/10 p-1">
<h1
class="w-full truncate text-2xl"
x-text="selectedLog.message"
></h1>
<button
class="btn btn-sm btn-square btn-ghost"
@click="navigator.clipboard.writeText(selectedLog.message);"
>
<i class="gg-copy"></i>
</button>
</div>
<div class="flex flex-col md:flex-row-reverse">
<div
class="w-full md:w-1/3 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-1"
>
<div class="mb-4 py-4 px-8">
<div class="text-sm">Severity</div>
<div
class="text-3xl font-bold text-error"
:class="{
'text-secondary': selectedLog.severity == 'FATAL',
'text-error': selectedLog.severity == 'ERROR',
'text-warning': selectedLog.severity == 'WARNING',
'text-info': selectedLog.severity == 'INFO',
}"
x-text="selectedLog.severity"
></div>
</div>
<div class="mb-4 py-4 px-8">
<div class="text-sm">Occurrence</div>
<div class="text-3xl font-bold">
<span x-text="countOccurrence(selectedLog.hash)"></span>
<span class="text-xl text-base-content/80">Logged</span>
</div>
</div>
</div>
<dl class="grid sm:grid-cols-2 flex-grow">
<dt class="mb-1 font-bold" x-show="selectedLog.status">
HTTP Status:
</dt>
<dd
class="ml-8 sm:ml-0 sm:text-right mb-2"
x-show="selectedLog.status"
>
<span
class="font-bold badge text-lg py-3"
:class="{
'badge-error': selectedLog.status >= 500,
'badge-warning': selectedLog.status >= 400 && selectedLog.status < 500,
'badge-info': selectedLog.status < 400,
}"
x-text="selectedLog.status"
></span>
</dd>
<dt class="mb-1 font-bold">Timestamp:</dt>
<dd
class="ml-8 sm:ml-0 sm:text-right mb-2"
x-text="globalThis.dayjs(selectedLog.created_at).toISOString()"
></dd>
<dt
class="mb-1 font-bold sm:col-span-2"
x-show="selectedLog.user_agent"
>
User Agent:
</dt>
<dd
class="ml-8 sm:ml-0 mb-2 sm:col-span-2"
x-show="selectedLog.user_agent"
>
<textarea
readonly
class="textarea bg-primary/10 w-full"
@click="$event.target.select()"
x-text="selectedLog.user_agent"
></textarea>
</dd>
<dt class="mb-1 font-bold" x-show="selectedLog.origin">
Origin:
</dt>
<dd
class="ml-8 sm:ml-0 sm:text-right mb-2 font-bold fond-mono"
x-show="selectedLog.origin"
x-text="selectedLog.origin"
></dd>
<dt
class="mb-1 font-bold sm:col-span-2"
x-show="selectedLog.url"
>
Url:
</dt>
<dd
class="ml-8 sm:ml-0 sm:text-right mb-2 sm:col-span-2"
x-show="selectedLog.url"
>
<a
class="link"
x-bind:href="selectedLog.user_agent"
x-text="selectedLog.url"
></a>
</dd>
</dl>
</div>
</div>
</template>
</main>
</div>
<aside class="drawer-side">
<div class="drawer-overlay"></div>
<div class="flex flex-col h-screen bg-base-100 w-80">
<div class="navbar flex-row justify-between">
<span class="text-xl px-4">Errors</span>
<label
class="btn btn-square btn-ghost bg-base-200 lg:hidden"
for="my-drawer"
>
<i class="gg-chevron-left"></i>
</label>
</div>
<div
class="h-full overflow-y-auto"
@scrolled-to-bottom.debounce="next"
@scroll="handleScroll"
>
<ul class="h-full text-base-content">
<template x-for="item in logsApp" :key="item.id">
<li>
<button
class="px-4 h-20 w-full group hover:bg-base-300 border-b border-base-300/80"
:class="{
'bg-primary/10 hover:bg-primary/20': selectedLog &&item.id ==selectedLog.id
}"
@click.prevent="selectLog(item)"
>
<div class="w-full flex justify-between">
<div
class="w-full text-left truncate mb-2"
:class="{
'font-bold': selectedLog && item.id == selectedLog.id
}"
x-text="item.message"
></div>
<span
x-text="item.status"
x-show="item.status"
class="font-bold badge"
:class="{
'badge-error': item.status >= 500,
'badge-warning': item.status >= 400 && item.status < 500,
'badge-info': item.status < 400,
}"
></span>
</div>
<div class="w-full flex justify-between">
<div>
<span
class="badge"
:class="{
'badge-secondary': item.severity == 'FATAL',
'badge-error': item.severity == 'ERROR',
'badge-warning': item.severity == 'WARNING',
'badge-ghost': item.severity == 'INFO',
}"
x-text="item.severity"
></span>
</div>
<div>
<span
class="inline-flex bg-base-300 group-hover:bg-base-100 rounded-sm font-mono text-sm"
>
<span
class="px-2"
x-text="day(item.created_at)"
></span>
<span
class="px-2 text-base-100"
:class="item.type == 'gin'?'bg-primary':
item.type == 'js'?'bg-secondary':
item.type == 'log'?'bg-accent':
'bg-base-content'"
x-text="item.type"
></span>
</span>
</div>
</div>
</button>
</li>
</template>
<li
class="flex justify-center py-4"
x-show="!pageEnd && logsApp.length"
>
<i class="gg-loadbar-alt"></i>
</li>
<template x-if="logsApp.length == 0">
<li class="w-full h-full flex justify-center items-center">
<div class="flex flex-col items-center text-xl">
<i class="gg-bot"></i>
<p class="mt-2">Empty</p>
</div>
</li>
</template>
</ul>
</div>
</div>
</aside>
</div>
</body>
</html>