|  | @@ -1,11 +1,55 @@
 | 
	
		
			
				|  |  |  <script lang="typescript">
 | 
	
		
			
				|  |  | -    import type { PollOptions, PollResult, LogEvent } from "src/routes/logs/poll";
 | 
	
		
			
				|  |  | +    import type {
 | 
	
		
			
				|  |  | +        PollOptions,
 | 
	
		
			
				|  |  | +        PollResult,
 | 
	
		
			
				|  |  | +        LogEvent,
 | 
	
		
			
				|  |  | +    } from "src/routes/logs/poll";
 | 
	
		
			
				|  |  | +    import { faSyncAlt } from "@fortawesome/free-solid-svg-icons";
 | 
	
		
			
				|  |  | +    import Icon from "svelte-awesome/components/Icon.svelte";
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    let logEvents: LogEvent[] = [];
 | 
	
		
			
				|  |  | +    const sync = (faSyncAlt as unknown) as undefined;
 | 
	
		
			
				|  |  | +    const POLL_INTERVAL = 5000;
 | 
	
		
			
				|  |  | +    let logEvents: LogEvent[] | undefined = undefined;
 | 
	
		
			
				|  |  | +    let syncing = false;
 | 
	
		
			
				|  |  | +    let poller = 0;
 | 
	
		
			
				|  |  | +    let maxMessages = 50;
 | 
	
		
			
				|  |  | +    let error = "";
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    const LEVEL_COLORS: Record<string, string> = {
 | 
	
		
			
				|  |  | +        "info": "#EFF2F7",
 | 
	
		
			
				|  |  | +        "warn": "#ECC94B",
 | 
	
		
			
				|  |  | +        "error": "#C53030",
 | 
	
		
			
				|  |  | +    };
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    function getLevelColor(level: string) {
 | 
	
		
			
				|  |  | +        if (level in LEVEL_COLORS) {
 | 
	
		
			
				|  |  | +            return LEVEL_COLORS[level];
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        return "#FFFFFF";
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    function onAutoUpdateChecked(event: Event & { target: HTMLInputElement }) {
 | 
	
		
			
				|  |  | +        if (event.target.checked) {
 | 
	
		
			
				|  |  | +            const pollCallback = async () => {
 | 
	
		
			
				|  |  | +                await pollEvents();
 | 
	
		
			
				|  |  | +                poller = setTimeout(pollCallback, POLL_INTERVAL);    
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            if (poller >= 0) {
 | 
	
		
			
				|  |  | +                poller = setTimeout(pollCallback, POLL_INTERVAL);
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +        } else {
 | 
	
		
			
				|  |  | +            clearTimeout(poller);
 | 
	
		
			
				|  |  | +            poller = -1;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      async function pollEvents() {
 | 
	
		
			
				|  |  | +        if (syncing) {
 | 
	
		
			
				|  |  | +            return;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        syncing = true;
 | 
	
		
			
				|  |  |          const params: PollOptions = {
 | 
	
		
			
				|  |  | -            limit: "50",
 | 
	
		
			
				|  |  | +            limit: maxMessages.toString(),
 | 
	
		
			
				|  |  |          };
 | 
	
		
			
				|  |  |          const query = Object.entries(params).reduce(
 | 
	
		
			
				|  |  |              (prev, [key, val]) =>
 | 
	
	
		
			
				|  | @@ -22,42 +66,94 @@
 | 
	
		
			
				|  |  |          });
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          const result = (await response.json()) as PollResult;
 | 
	
		
			
				|  |  | -        console.log(result);
 | 
	
		
			
				|  |  |          if (result.ok) {
 | 
	
		
			
				|  |  |              logEvents = result.events;
 | 
	
		
			
				|  |  | +        } else {
 | 
	
		
			
				|  |  | +            error = result.error;
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  | +        syncing = false;
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  </script>
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  <style>
 | 
	
		
			
				|  |  | +    .sync-button {
 | 
	
		
			
				|  |  | +        @apply bg-blue-600 py-1 px-3 text-white cursor-pointer rounded-sm text-sm;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        &:hover {
 | 
	
		
			
				|  |  | +            @apply bg-blue-700 text-gray-100;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    .logs-table {
 | 
	
		
			
				|  |  | +        @apply bg-black m-3 block overflow-y-auto;
 | 
	
		
			
				|  |  | +        max-height: 70%;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        td,
 | 
	
		
			
				|  |  | +        th {
 | 
	
		
			
				|  |  | +            @apply font-mono px-2 text-sm;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        thead th {
 | 
	
		
			
				|  |  | +            @apply sticky top-0 bg-black py-2;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    .error-box {
 | 
	
		
			
				|  |  | +        @apply text-gray-100 bg-red-600 py-4 px-3 rounded-sm;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    .query-row {
 | 
	
		
			
				|  |  | +        @apply flex flex-row justify-end items-center;
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    .event-log-options {
 | 
	
		
			
				|  |  | +        @apply flex flex-col;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        input[type="number"] {
 | 
	
		
			
				|  |  | +            @apply text-black w-16;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  |  </style>
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  <div class="viewport text-white">
 | 
	
		
			
				|  |  |      <h1 class="text-3xl pb-4">Event logs</h1>
 | 
	
		
			
				|  |  |      <form class="py-2" on:submit|preventDefault={pollEvents}>
 | 
	
		
			
				|  |  | -        <input
 | 
	
		
			
				|  |  | -            class="bg-blue-600 py-2 px-3 text-white cursor-pointer rounded-sm"
 | 
	
		
			
				|  |  | -            type="submit"
 | 
	
		
			
				|  |  | -            value="Query" />
 | 
	
		
			
				|  |  | +        <div class="event-log-options">
 | 
	
		
			
				|  |  | +            <label>Max messages: <input type="number" min="0" bind:value={maxMessages} /></label>
 | 
	
		
			
				|  |  | +        </div>
 | 
	
		
			
				|  |  | +        <div class="query-row">
 | 
	
		
			
				|  |  | +            {#if syncing}
 | 
	
		
			
				|  |  | +                <Icon class="mx-1" data={sync} spin />
 | 
	
		
			
				|  |  | +            {/if}
 | 
	
		
			
				|  |  | +            <input class="sync-button" type="submit" value="Query" disabled={syncing} />
 | 
	
		
			
				|  |  | +            <label class="mx-1"><input type="checkbox" on:input={onAutoUpdateChecked}/> Auto update</label>
 | 
	
		
			
				|  |  | +        </div>
 | 
	
		
			
				|  |  |      </form>
 | 
	
		
			
				|  |  | -    <table class="py-3">
 | 
	
		
			
				|  |  | -        <thead>
 | 
	
		
			
				|  |  | -            <tr>
 | 
	
		
			
				|  |  | -                <th>Source</th>
 | 
	
		
			
				|  |  | -                <th>Timestamp</th>
 | 
	
		
			
				|  |  | -                <th>Level</th>
 | 
	
		
			
				|  |  | -                <th>Message</th>
 | 
	
		
			
				|  |  | -            </tr>
 | 
	
		
			
				|  |  | -        </thead>
 | 
	
		
			
				|  |  | -        <tbody>
 | 
	
		
			
				|  |  | -            {#each logEvents as logEvent (logEvent._id)}
 | 
	
		
			
				|  |  | +    {#if error}
 | 
	
		
			
				|  |  | +        <p class="error-box">
 | 
	
		
			
				|  |  | +            Failed to query: <span class="font-mono">{error}</span>
 | 
	
		
			
				|  |  | +        </p>
 | 
	
		
			
				|  |  | +    {/if}
 | 
	
		
			
				|  |  | +    {#if logEvents}
 | 
	
		
			
				|  |  | +        <table class="logs-table">
 | 
	
		
			
				|  |  | +            <thead>
 | 
	
		
			
				|  |  |                  <tr>
 | 
	
		
			
				|  |  | -                    <td>{logEvent.label}</td>
 | 
	
		
			
				|  |  | -                    <td>{logEvent.timestamp}</td>
 | 
	
		
			
				|  |  | -                    <td>{logEvent.level}</td>
 | 
	
		
			
				|  |  | -                    <td>{logEvent.message}</td>
 | 
	
		
			
				|  |  | +                    <th>Timestamp</th>
 | 
	
		
			
				|  |  | +                    <th>Source</th>
 | 
	
		
			
				|  |  | +                    <th>Level</th>
 | 
	
		
			
				|  |  | +                    <th>Message</th>
 | 
	
		
			
				|  |  |                  </tr>
 | 
	
		
			
				|  |  | -            {/each}
 | 
	
		
			
				|  |  | -        </tbody>
 | 
	
		
			
				|  |  | -    </table>
 | 
	
		
			
				|  |  | +            </thead>
 | 
	
		
			
				|  |  | +            <tbody>
 | 
	
		
			
				|  |  | +                {#each logEvents as logEvent (logEvent._id)}
 | 
	
		
			
				|  |  | +                    <tr style="color: {getLevelColor(logEvent.level)};">
 | 
	
		
			
				|  |  | +                        <td>{logEvent.timestamp}</td>
 | 
	
		
			
				|  |  | +                        <td>{logEvent.label}</td>
 | 
	
		
			
				|  |  | +                        <td>{logEvent.level}</td>
 | 
	
		
			
				|  |  | +                        <td>{logEvent.message}</td>
 | 
	
		
			
				|  |  | +                    </tr>
 | 
	
		
			
				|  |  | +                {/each}
 | 
	
		
			
				|  |  | +            </tbody>
 | 
	
		
			
				|  |  | +        </table>
 | 
	
		
			
				|  |  | +    {/if}
 | 
	
		
			
				|  |  |  </div>
 |