Implement event log

This commit is contained in:
Kai Vogelgesang 2022-09-25 20:02:52 +02:00
parent 8886b23434
commit 1a46cf7ef3
Signed by: kai
GPG Key ID: 0A95D3B6E62C0879
11 changed files with 197 additions and 128 deletions

View File

@ -4,13 +4,10 @@
import { user } from "./stores";
import Navbar from "./Navbar.svelte";
import P404 from "./pages/P404.svelte";
import Mining from "./pages/mining/Mining.svelte";
import Footer from "./Footer.svelte";
import BaseLayout from "./BaseLayout.svelte";
import Monitoring from "./pages/monitoring/Monitoring.svelte";
import Bar from "./pages/playground/Bar.svelte";
onMount(async () => {
const res = await fetch("/user/me");
@ -27,23 +24,6 @@
</BaseLayout>
</Route>
<Route path="foo">
<BaseLayout>
<section class="hero is-danger is-fullheight">
<div class="hero-body">
<div class="">
<p class="title">Fullheight hero</p>
<p class="subtitle">Fullheight subtitle</p>
</div>
</div>
</section>
</BaseLayout>
</Route>
<Route path="bar">
<BaseLayout><Bar /></BaseLayout>
</Route>
<Route path="monitoring/*">
<BaseLayout><Monitoring /></BaseLayout>
</Route>

View File

@ -30,8 +30,6 @@
</div>
<div class="navbar-menu {menu_open ? 'is-active' : ''}">
<div class="navbar-start">
<Navlink target="/foo" text="Foo" icon="fas fa-hippo" />
<Navlink target="/bar" text="Bar" icon="fas fa-otter" />
<Navlink target="/monitoring" text="Monitoring" icon="fas fa-binoculars" />
<Navlink target="/mining" text="Mining" icon="fas fa-person-digging" />
<Navlink target="/stats" text="Statistics" icon="fas fa-chart-line" />

View File

@ -0,0 +1,95 @@
<script lang="ts" context="module">
type Event = {
icon: string;
text: string;
};
const mappings: Map<string, (any) => Event> = new Map([
[
"computer_connect",
(value) => ({
icon: "fas fa-circle-plus",
text: `Computer ${value.computer_id} connected`,
}),
],
[
"computer_disconnect",
(value) => ({
icon: "fas fa-circle-minus",
text: `Computer ${value.computer_id} disconnected`,
}),
],
]);
</script>
<script lang="ts">
export let timestamp: string;
export let value: any;
let icon = "far fa-circle-question";
let text = "Unknown event";
if ("type" in value) {
let m = mappings.get(value.type);
if (m) {
let mapped = m(value);
icon = mapped.icon;
text = mapped.text;
}
}
let datetime = new Date(timestamp);
let expanded = false;
</script>
<div
class="entry"
on:click={() => {
expanded = !expanded;
}}
>
<div class="headers">
<div><span class="icon"><i class={icon} /></span></div>
<div class="header-title">{text}</div>
<div>{datetime}</div>
<div>
<span class="icon"
><i
class="fas {expanded ? 'fa-angle-up' : 'fa-angle-down'}"
/></span
>
</div>
</div>
{#if expanded}
<div class="expandable">
<pre>{JSON.stringify(value, null, 1)}</pre>
</div>
{/if}
</div>
<style lang="scss">
.entry {
background-color: #fafafa;
display: flex;
flex-direction: column;
&:nth-child(even) {
background-color: white;
}
border-top: 1px solid #dbdbdb;
}
.headers {
display: flex;
flex-direction: row;
align-items: center;
gap: 15px;
cursor: pointer;
}
.header-title {
flex: 1 1 0;
}
</style>

View File

@ -0,0 +1,34 @@
<script lang="ts">
import { onMount } from "svelte";
import Event from "./Event.svelte";
const ENDPOINT = "/api/events";
let events: { _id: string; timestamp: string; value: any }[] = [];
onMount(() => {
let int = setInterval(async () => {
let r = await fetch(ENDPOINT);
events = await r.json();
}, 200);
return () => {
clearInterval(int);
};
});
</script>
<div class="events">
{#each events as event (event._id)}
<Event {...event} />
{/each}
</div>
<style lang="scss">
.events {
display: flex;
justify-content: center;
flex-direction: column;
width: 100%;
}
</style>

View File

@ -1,27 +1,28 @@
<script lang="ts">
import { Link, Route, useParams } from "svelte-navigator";
import Events from "./Events.svelte";
import Viewer from "./Viewer.svelte";
const uuids = ["8b9faf9f-9470-4a50-b405-0af5f0152550"];
</script>
<section class="section">
<Route path="/">
<div class="content">
<div class="container">
<Route path="/">
<p>Yo wassup</p>
<ul>
{#each uuids as id}
<li><Link to="./{id}">{id}</Link></li>
{/each}
</ul>
</div>
</Route>
<p>eventeroni</p>
<Events />
</Route>
<Route path=":id" let:params>
<div class="container">
<Route path=":id" let:params>
<h1 class="title">ASSUMING DIRECT CONTROL</h1>
<Viewer uuid={params.id} />
<Link to="..">fuck go back</Link>
</div>
</Route>
</Route>
</div>
</section>

View File

@ -1,21 +0,0 @@
<script lang="ts">
import { onDestroy } from "svelte";
import Canvas from "./Canvas.svelte";
let colors = ["#ff6600", "#00ff66", "#6600ff"];
let i = 0;
$: color = colors[i];
let int = setInterval(() => {
i = (i + 1) % colors.length;
}, 1000);
onDestroy(() => {
clearInterval(int);
});
</script>
<section class="section">
<Canvas {color} />
</section>

View File

@ -1,24 +0,0 @@
<script lang="ts">
import { onMount } from "svelte";
export let color: string;
let canvas: HTMLCanvasElement;
let redraws = 0;
function redraw(color) {
if (!canvas) return;
redraws += 1;
let cx = canvas.getContext("2d");
cx.fillStyle = color;
cx.fillRect(0, 0, cx.canvas.width, cx.canvas.height);
}
$: redraw(color);
</script>
<canvas bind:this={canvas} />
<pre>color: {color}
redraws: {redraws}</pre>

View File

@ -9,6 +9,7 @@ from .user import user_auth
from .map_tiles import map_tiles, map_meta
from .templates import j2env
from .monitoring import monitoring, ws_manager
from .api import api
app = FastAPI()
@ -16,6 +17,7 @@ app.mount("/user/", user_auth)
app.mount("/map/", map_meta)
app.mount("/tiles/", map_tiles)
app.mount("/ipmi/", monitoring)
app.mount("/api/", api)
installer = j2env.get_template("install.lua").render(deploy_path=settings.deploy_path)

40
server/server/api.py Normal file
View File

@ -0,0 +1,40 @@
from datetime import datetime
from typing import Any
from bson import ObjectId
from fastapi import Body, FastAPI, status
from pydantic import BaseModel, Field
import pymongo
from .db import events, PyObjectId, create_event
api = FastAPI()
class Event(BaseModel):
id: PyObjectId = Field(default_factory=PyObjectId, alias="_id")
timestamp: datetime
value: Any
class Config:
json_encoders = {ObjectId: str}
@api.get("/events", response_model=list[Event])
async def get_events():
return await events.find().sort("timestamp", pymongo.DESCENDING).to_list(None)
"""
@api.get("/events/{id}", response_model=Event)
async def get_single_event(id: PyObjectId):
if (event := await events.find_one({"_id": id})) is not None:
return event
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
"""
# TODO auth
@api.post("/events", response_model=Event, status_code=status.HTTP_201_CREATED)
async def push_event(value: Any = Body(...)):
return await create_event(value)

View File

@ -1,3 +1,5 @@
from datetime import datetime
from bson import ObjectId
import motor.motor_asyncio as motor
from .settings import settings
@ -8,6 +10,16 @@ db = client["controlpanel"]
events: motor.AsyncIOMotorCollection = db["events"]
async def create_event(value: any):
event = {
"timestamp": datetime.now(),
"value": value,
}
new_event = await events.insert_one(event)
created_event = await events.find_one({"_id": new_event.inserted_id})
return created_event
class PyObjectId(ObjectId):
@classmethod
def __get_validators__(cls):

View File

@ -1,21 +1,11 @@
import asyncio
from typing import Any
from uuid import UUID
from datetime import datetime
from bson import ObjectId
from fastapi import (
Body,
FastAPI,
HTTPException,
WebSocket,
WebSocketDisconnect,
status,
)
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field, ValidationError
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from pydantic import BaseModel, ValidationError
from .db import PyObjectId, events
from .db import create_event
monitoring = FastAPI()
@ -75,7 +65,7 @@ class WSManager:
await socket.close()
return
print(f"[WS] Computer {uuid} connected")
await create_event({"type": "computer_connect", "computer_id": str(uuid)})
self.computers[uuid] = socket
if len(self.viewers.get(uuid, [])) > 0:
@ -96,7 +86,7 @@ class WSManager:
break
del self.computers[uuid]
print(f"[WS] Computer {uuid} disconnected")
await create_event({"type": "computer_disconnect", "computer_id": str(uuid)})
async def on_browser_connect(self, socket: WebSocket, uuid: UUID):
print(f"[WS] Browser connected for {uuid}")
@ -135,41 +125,3 @@ async def computer_ws(socket: WebSocket, uuid: UUID):
async def browser_ws(socket: WebSocket, uuid: UUID):
await socket.accept()
await ws_manager.on_browser_connect(socket, uuid)
class Event(BaseModel):
id: PyObjectId = Field(default_factory=PyObjectId, alias="_id")
timestamp: datetime
value: Any
class Config:
allow_population_by_field_name = True
arbitrary_types_allowed = True
json_encoders = {ObjectId: str}
@monitoring.get("/events", response_model=list[Event])
async def get_events():
print("get /events")
return await events.find().to_list(1000)
@monitoring.get("/events/{id}", response_model=Event)
async def get_single_event(id: PyObjectId):
if (event := await events.find_one({"_id": id})) is not None:
return event
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
@monitoring.post(
"/push_event", response_model=Event, status_code=status.HTTP_201_CREATED
)
async def push_event(value: Any = Body(...)):
event = {
"timestamp": datetime.now(),
"value": value,
}
new_event = await events.insert_one(event)
created_event = await events.find_one({"_id": new_event.inserted_id})
return created_event