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
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>
|