-
+
+
+
diff --git a/admin/ui/src/components/InstanceCard.vue b/admin/ui/src/components/InstanceCard.vue
new file mode 100644
index 0000000..edc440d
--- /dev/null
+++ b/admin/ui/src/components/InstanceCard.vue
@@ -0,0 +1,126 @@
+
+
+
+
+
{{ instance.name }}
+
+
+
{{ instance.address }}
+
+
+
+ Version
+ {{ instance.version }}
+
+
+ Last seen
+ {{
+ formatTime(instance.last_seen_at)
+ }}
+
+
+
+
+ Edit
+
+
+ Remove
+
+
+
+
+
+
+
+
diff --git a/admin/ui/src/components/InstanceTable.vue b/admin/ui/src/components/InstanceTable.vue
new file mode 100644
index 0000000..9947e92
--- /dev/null
+++ b/admin/ui/src/components/InstanceTable.vue
@@ -0,0 +1,114 @@
+
+
+
+
+
+ | Status |
+ Name |
+ Address |
+ Version |
+ Last Seen |
+ Actions |
+
+
+
+
+ |
+
+ |
+ {{ inst.name }} |
+ {{ inst.address }} |
+ {{ inst.version || '—' }} |
+ {{ formatTime(inst.last_seen_at) || '—' }} |
+
+
+ Edit
+
+
+ Remove
+
+ |
+
+
+
+
+
+
+
+
+
diff --git a/admin/ui/src/components/StatusIndicator.vue b/admin/ui/src/components/StatusIndicator.vue
index 07c8af9..4557afb 100644
--- a/admin/ui/src/components/StatusIndicator.vue
+++ b/admin/ui/src/components/StatusIndicator.vue
@@ -10,24 +10,26 @@
diff --git a/admin/ui/src/main.js b/admin/ui/src/main.js
index e9b5ead..776b771 100644
--- a/admin/ui/src/main.js
+++ b/admin/ui/src/main.js
@@ -1,11 +1,11 @@
-import { createApp } from "vue";
-import { createPinia } from "pinia";
-import App from "./App.vue";
-import router from "./router";
-import "./assets/variables.css";
-import "./assets/global.css";
+import { createApp } from 'vue';
+import { createPinia } from 'pinia';
+import App from './App.vue';
+import router from './router';
+import './assets/variables.css';
+import './assets/global.css';
const app = createApp(App);
app.use(createPinia());
app.use(router);
-app.mount("#app");
+app.mount('#app');
diff --git a/admin/ui/src/router/index.js b/admin/ui/src/router/index.js
index 0804c53..7be5fa7 100644
--- a/admin/ui/src/router/index.js
+++ b/admin/ui/src/router/index.js
@@ -1,22 +1,22 @@
-import { createRouter, createWebHistory } from "vue-router";
-import DashboardView from "../views/DashboardView.vue";
-import AuditLogView from "../views/AuditLogView.vue";
+import { createRouter, createWebHistory } from 'vue-router';
+import DashboardView from '../views/DashboardView.vue';
+import AuditLogView from '../views/AuditLogView.vue';
const routes = [
{
- path: "/",
- name: "dashboard",
+ path: '/',
+ name: 'dashboard',
component: DashboardView,
},
{
- path: "/audit-log",
- name: "audit-log",
+ path: '/audit-log',
+ name: 'audit-log',
component: AuditLogView,
},
{
- path: "/:pathMatch(.*)*",
- name: "not-found",
- redirect: "/",
+ path: '/:pathMatch(.*)*',
+ name: 'not-found',
+ redirect: '/',
},
];
diff --git a/admin/ui/src/stores/instances.js b/admin/ui/src/stores/instances.js
new file mode 100644
index 0000000..f2fc230
--- /dev/null
+++ b/admin/ui/src/stores/instances.js
@@ -0,0 +1,48 @@
+import { defineStore } from 'pinia';
+import { ref } from 'vue';
+import * as api from '../utils/api.js';
+
+export const useInstanceStore = defineStore('instances', () => {
+ const instances = ref([]);
+ const loading = ref(false);
+
+ async function fetchInstances() {
+ loading.value = true;
+ try {
+ instances.value = await api.get('/api/instances');
+ } finally {
+ loading.value = false;
+ }
+ }
+
+ async function createInstance(name, address) {
+ const inst = await api.post('/api/instances', { name, address });
+ await fetchInstances();
+ return inst;
+ }
+
+ async function updateInstance(id, name, address) {
+ const inst = await api.put(`/api/instances/${id}`, { name, address });
+ await fetchInstances();
+ return inst;
+ }
+
+ async function deleteInstance(id) {
+ await api.del(`/api/instances/${id}`);
+ await fetchInstances();
+ }
+
+ async function testConnection(address) {
+ return api.post('/api/instances/test', { address });
+ }
+
+ return {
+ instances,
+ loading,
+ fetchInstances,
+ createInstance,
+ updateInstance,
+ deleteInstance,
+ testConnection,
+ };
+});
diff --git a/admin/ui/src/stores/instances.test.js b/admin/ui/src/stores/instances.test.js
new file mode 100644
index 0000000..39b3b24
--- /dev/null
+++ b/admin/ui/src/stores/instances.test.js
@@ -0,0 +1,111 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { setActivePinia, createPinia } from 'pinia';
+import { useInstanceStore } from './instances.js';
+
+vi.mock('../utils/api.js', () => ({
+ get: vi.fn(),
+ post: vi.fn(),
+ put: vi.fn(),
+ del: vi.fn(),
+}));
+
+import * as api from '../utils/api.js';
+
+describe('useInstanceStore', () => {
+ let store;
+
+ beforeEach(() => {
+ vi.restoreAllMocks();
+ setActivePinia(createPinia());
+ store = useInstanceStore();
+ });
+
+ describe('fetchInstances', () => {
+ it('populates instances from API', async () => {
+ const data = [{ id: 1, name: 'proxy-1' }];
+ api.get.mockResolvedValue(data);
+ await store.fetchInstances();
+ expect(api.get).toHaveBeenCalledWith('/api/instances');
+ expect(store.instances).toEqual(data);
+ });
+
+ it('sets loading true during fetch and false after', async () => {
+ let resolve;
+ api.get.mockReturnValue(new Promise((r) => (resolve = r)));
+ const p = store.fetchInstances();
+ expect(store.loading).toBe(true);
+ resolve([]);
+ await p;
+ expect(store.loading).toBe(false);
+ });
+
+ it('sets loading false even on error', async () => {
+ api.get.mockRejectedValue(new Error('network error'));
+ await store.fetchInstances().catch(() => {});
+ expect(store.loading).toBe(false);
+ });
+ });
+
+ describe('createInstance', () => {
+ it('calls api.post and refreshes instances', async () => {
+ api.post.mockResolvedValue({
+ id: 1,
+ name: 'proxy-1',
+ address: '10.0.0.1:9090',
+ });
+ api.get.mockResolvedValue([{ id: 1 }]);
+ const result = await store.createInstance('proxy-1', '10.0.0.1:9090');
+ expect(api.post).toHaveBeenCalledWith('/api/instances', {
+ name: 'proxy-1',
+ address: '10.0.0.1:9090',
+ });
+ expect(result).toEqual({
+ id: 1,
+ name: 'proxy-1',
+ address: '10.0.0.1:9090',
+ });
+ expect(api.get).toHaveBeenCalledWith('/api/instances');
+ });
+
+ it('throws on API error', async () => {
+ api.post.mockRejectedValue(new Error('Address already registered'));
+ await expect(store.createInstance('x', '1:2')).rejects.toThrow(
+ 'Address already registered',
+ );
+ });
+ });
+
+ describe('updateInstance', () => {
+ it('calls api.put and refreshes instances', async () => {
+ api.put.mockResolvedValue({ id: 1, name: 'updated' });
+ api.get.mockResolvedValue([{ id: 1 }]);
+ const result = await store.updateInstance(1, 'updated', '10.0.0.1:9090');
+ expect(api.put).toHaveBeenCalledWith('/api/instances/1', {
+ name: 'updated',
+ address: '10.0.0.1:9090',
+ });
+ expect(result).toEqual({ id: 1, name: 'updated' });
+ });
+ });
+
+ describe('deleteInstance', () => {
+ it('calls api.del and refreshes instances', async () => {
+ api.del.mockResolvedValue(null);
+ api.get.mockResolvedValue([]);
+ await store.deleteInstance(1);
+ expect(api.del).toHaveBeenCalledWith('/api/instances/1');
+ expect(api.get).toHaveBeenCalledWith('/api/instances');
+ });
+ });
+
+ describe('testConnection', () => {
+ it('calls api.post and returns result', async () => {
+ api.post.mockResolvedValue({ ok: true, version: '1.0.0' });
+ const result = await store.testConnection('10.0.0.1:9090');
+ expect(api.post).toHaveBeenCalledWith('/api/instances/test', {
+ address: '10.0.0.1:9090',
+ });
+ expect(result).toEqual({ ok: true, version: '1.0.0' });
+ });
+ });
+});
diff --git a/admin/ui/src/utils/api.js b/admin/ui/src/utils/api.js
new file mode 100644
index 0000000..1582b1b
--- /dev/null
+++ b/admin/ui/src/utils/api.js
@@ -0,0 +1,52 @@
+class ApiError extends Error {
+ constructor(message, status, code) {
+ super(message);
+ this.name = 'ApiError';
+ this.status = status;
+ this.code = code;
+ }
+}
+
+async function request(path, options = {}) {
+ const res = await fetch(path, {
+ ...options,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...options.headers,
+ },
+ });
+
+ if (!res.ok) {
+ let message = `Request failed (${res.status})`;
+ let code;
+ try {
+ const data = await res.json();
+ if (data.error?.message) message = data.error.message;
+ if (data.error?.code) code = data.error.code;
+ } catch {
+ // response body not JSON — keep generic message
+ }
+ throw new ApiError(message, res.status, code);
+ }
+
+ if (res.status === 204) return null;
+ return res.json();
+}
+
+export function get(path) {
+ return request(path);
+}
+
+export function post(path, body) {
+ return request(path, { method: 'POST', body: JSON.stringify(body) });
+}
+
+export function put(path, body) {
+ return request(path, { method: 'PUT', body: JSON.stringify(body) });
+}
+
+export function del(path) {
+ return request(path, { method: 'DELETE' });
+}
+
+export { ApiError };
diff --git a/admin/ui/src/utils/api.test.js b/admin/ui/src/utils/api.test.js
new file mode 100644
index 0000000..fa7c1e2
--- /dev/null
+++ b/admin/ui/src/utils/api.test.js
@@ -0,0 +1,123 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { get, post, put, del, ApiError } from './api.js';
+
+describe('api client', () => {
+ beforeEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ function mockFetch(status, body, { json = true } = {}) {
+ const res = {
+ ok: status >= 200 && status < 300,
+ status,
+ json: json
+ ? vi.fn().mockResolvedValue(body)
+ : vi.fn().mockRejectedValue(new Error('not json')),
+ };
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(res);
+ return res;
+ }
+
+ describe('get', () => {
+ it('returns parsed JSON on success', async () => {
+ mockFetch(200, [{ id: 1 }]);
+ const result = await get('/api/instances');
+ expect(result).toEqual([{ id: 1 }]);
+ expect(globalThis.fetch).toHaveBeenCalledWith('/api/instances', {
+ headers: { 'Content-Type': 'application/json' },
+ });
+ });
+
+ it('throws ApiError with server message on failure', async () => {
+ mockFetch(404, {
+ error: {
+ code: 'INSTANCE_NOT_FOUND',
+ message: 'No instance with ID 42',
+ },
+ });
+ const err = await get('/api/instances/42').catch((e) => e);
+ expect(err).toBeInstanceOf(ApiError);
+ expect(err.message).toBe('No instance with ID 42');
+ expect(err.status).toBe(404);
+ expect(err.code).toBe('INSTANCE_NOT_FOUND');
+ });
+
+ it('throws ApiError with generic message when response is not JSON', async () => {
+ mockFetch(500, null, { json: false });
+ const err = await get('/api/instances').catch((e) => e);
+ expect(err).toBeInstanceOf(ApiError);
+ expect(err.message).toBe('Request failed (500)');
+ expect(err.status).toBe(500);
+ });
+ });
+
+ describe('post', () => {
+ it('sends JSON body and returns parsed response', async () => {
+ mockFetch(201, { id: 1, name: 'proxy-1' });
+ const result = await post('/api/instances', {
+ name: 'proxy-1',
+ address: '10.0.0.1:9090',
+ });
+ expect(result).toEqual({ id: 1, name: 'proxy-1' });
+
+ const [url, opts] = globalThis.fetch.mock.calls[0];
+ expect(url).toBe('/api/instances');
+ expect(opts.method).toBe('POST');
+ expect(JSON.parse(opts.body)).toEqual({
+ name: 'proxy-1',
+ address: '10.0.0.1:9090',
+ });
+ });
+
+ it('throws ApiError with server message on conflict', async () => {
+ mockFetch(409, {
+ error: {
+ code: 'DUPLICATE_ADDRESS',
+ message: 'Address already registered',
+ },
+ });
+ const err = await post('/api/instances', {
+ name: 'x',
+ address: '1:2',
+ }).catch((e) => e);
+ expect(err).toBeInstanceOf(ApiError);
+ expect(err.message).toBe('Address already registered');
+ expect(err.code).toBe('DUPLICATE_ADDRESS');
+ });
+ });
+
+ describe('put', () => {
+ it('sends JSON body with PUT method', async () => {
+ mockFetch(200, { id: 1, name: 'updated' });
+ const result = await put('/api/instances/1', { name: 'updated' });
+ expect(result).toEqual({ id: 1, name: 'updated' });
+
+ const [, opts] = globalThis.fetch.mock.calls[0];
+ expect(opts.method).toBe('PUT');
+ });
+ });
+
+ describe('del', () => {
+ it('returns null for 204 No Content', async () => {
+ const res = {
+ ok: true,
+ status: 204,
+ json: vi.fn(),
+ };
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(res);
+ const result = await del('/api/instances/1');
+ expect(result).toBeNull();
+ expect(res.json).not.toHaveBeenCalled();
+ });
+
+ it('sends DELETE method', async () => {
+ const res = { ok: true, status: 204, json: vi.fn() };
+ vi.spyOn(globalThis, 'fetch').mockResolvedValue(res);
+ await del('/api/instances/1');
+
+ const [url, opts] = globalThis.fetch.mock.calls[0];
+ expect(url).toBe('/api/instances/1');
+ expect(opts.method).toBe('DELETE');
+ });
+ });
+});
diff --git a/admin/ui/src/utils/instance.js b/admin/ui/src/utils/instance.js
new file mode 100644
index 0000000..260eff2
--- /dev/null
+++ b/admin/ui/src/utils/instance.js
@@ -0,0 +1,34 @@
+export const STALE_THRESHOLD_MS = 2 * 60 * 1000; // 2 minutes
+
+export function isInstanceStale(instance) {
+ if (instance.status !== 'healthy' || !instance.last_seen_at) return false;
+ return (
+ Date.now() - new Date(instance.last_seen_at).getTime() > STALE_THRESHOLD_MS
+ );
+}
+
+export function formatTime(ts) {
+ if (!ts) return '';
+ const d = new Date(ts);
+ const secs = Math.floor((Date.now() - d.getTime()) / 1000);
+ if (secs < 60) return 'just now';
+ if (secs < 3600) return `${Math.floor(secs / 60)}m ago`;
+ if (secs < 86400) return `${Math.floor(secs / 3600)}h ago`;
+ return d.toLocaleDateString();
+}
+
+const STATUS_LABELS = {
+ healthy: 'Healthy',
+ unreachable: 'Unreachable',
+ unknown: 'Unknown',
+ stale: 'Stale',
+};
+
+export function getStatusLabel(status, isStale) {
+ if (isStale) return STATUS_LABELS.stale;
+ return STATUS_LABELS[status] || STATUS_LABELS.unknown;
+}
+
+export function filterStaleInstances(instances) {
+ return instances.filter(isInstanceStale);
+}
diff --git a/admin/ui/src/utils/instance.test.js b/admin/ui/src/utils/instance.test.js
new file mode 100644
index 0000000..a811580
--- /dev/null
+++ b/admin/ui/src/utils/instance.test.js
@@ -0,0 +1,152 @@
+import { describe, it, expect, vi, afterEach } from 'vitest';
+import {
+ STALE_THRESHOLD_MS,
+ isInstanceStale,
+ formatTime,
+ getStatusLabel,
+ filterStaleInstances,
+} from './instance.js';
+
+describe('STALE_THRESHOLD_MS', () => {
+ it('is 2 minutes in milliseconds', () => {
+ expect(STALE_THRESHOLD_MS).toBe(120_000);
+ });
+});
+
+describe('isInstanceStale', () => {
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it('returns false for non-healthy instance', () => {
+ expect(
+ isInstanceStale({
+ status: 'unreachable',
+ last_seen_at: '2020-01-01T00:00:00Z',
+ }),
+ ).toBe(false);
+ expect(
+ isInstanceStale({
+ status: 'unknown',
+ last_seen_at: '2020-01-01T00:00:00Z',
+ }),
+ ).toBe(false);
+ });
+
+ it('returns false when last_seen_at is null', () => {
+ expect(isInstanceStale({ status: 'healthy', last_seen_at: null })).toBe(
+ false,
+ );
+ });
+
+ it('returns false when last seen recently', () => {
+ const recent = new Date(Date.now() - 30_000).toISOString(); // 30s ago
+ expect(isInstanceStale({ status: 'healthy', last_seen_at: recent })).toBe(
+ false,
+ );
+ });
+
+ it('returns true when last seen over 2 minutes ago', () => {
+ const old = new Date(Date.now() - STALE_THRESHOLD_MS - 1000).toISOString();
+ expect(isInstanceStale({ status: 'healthy', last_seen_at: old })).toBe(
+ true,
+ );
+ });
+
+ it('returns false at exactly the threshold boundary', () => {
+ vi.spyOn(Date, 'now').mockReturnValue(1000000);
+ const atBoundary = new Date(1000000 - STALE_THRESHOLD_MS).toISOString();
+ expect(
+ isInstanceStale({ status: 'healthy', last_seen_at: atBoundary }),
+ ).toBe(false);
+ });
+});
+
+describe('formatTime', () => {
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it('returns empty string for null/undefined', () => {
+ expect(formatTime(null)).toBe('');
+ expect(formatTime(undefined)).toBe('');
+ });
+
+ it('returns "just now" for timestamps under 60 seconds ago', () => {
+ const ts = new Date(Date.now() - 5000).toISOString();
+ expect(formatTime(ts)).toBe('just now');
+ });
+
+ it('returns minutes ago for timestamps under 1 hour', () => {
+ const ts = new Date(Date.now() - 5 * 60 * 1000).toISOString();
+ expect(formatTime(ts)).toBe('5m ago');
+ });
+
+ it('returns hours ago for timestamps under 24 hours', () => {
+ const ts = new Date(Date.now() - 3 * 3600 * 1000).toISOString();
+ expect(formatTime(ts)).toBe('3h ago');
+ });
+
+ it('returns locale date string for timestamps over 24 hours', () => {
+ const d = new Date(Date.now() - 48 * 3600 * 1000);
+ expect(formatTime(d.toISOString())).toBe(d.toLocaleDateString());
+ });
+
+ it('floors minutes correctly', () => {
+ // 90 seconds = 1.5 minutes → should show "1m ago"
+ const ts = new Date(Date.now() - 90_000).toISOString();
+ expect(formatTime(ts)).toBe('1m ago');
+ });
+
+ it('floors hours correctly', () => {
+ // 5400 seconds = 1.5 hours → should show "1h ago"
+ const ts = new Date(Date.now() - 5400 * 1000).toISOString();
+ expect(formatTime(ts)).toBe('1h ago');
+ });
+});
+
+describe('getStatusLabel', () => {
+ it('returns correct label for each status', () => {
+ expect(getStatusLabel('healthy', false)).toBe('Healthy');
+ expect(getStatusLabel('unreachable', false)).toBe('Unreachable');
+ expect(getStatusLabel('unknown', false)).toBe('Unknown');
+ });
+
+ it('returns "Stale" when isStale is true regardless of status', () => {
+ expect(getStatusLabel('healthy', true)).toBe('Stale');
+ expect(getStatusLabel('unreachable', true)).toBe('Stale');
+ expect(getStatusLabel('unknown', true)).toBe('Stale');
+ });
+
+ it('falls back to "Unknown" for unrecognized status', () => {
+ expect(getStatusLabel('bogus', false)).toBe('Unknown');
+ expect(getStatusLabel(undefined, false)).toBe('Unknown');
+ });
+});
+
+describe('filterStaleInstances', () => {
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it('returns empty array when no instances are stale', () => {
+ const recent = new Date(Date.now() - 30_000).toISOString();
+ const instances = [
+ { status: 'healthy', last_seen_at: recent },
+ { status: 'unreachable', last_seen_at: '2020-01-01T00:00:00Z' },
+ ];
+ expect(filterStaleInstances(instances)).toEqual([]);
+ });
+
+ it('returns only stale instances', () => {
+ const recent = new Date(Date.now() - 30_000).toISOString();
+ const old = new Date(Date.now() - STALE_THRESHOLD_MS - 1000).toISOString();
+ const stale = { status: 'healthy', last_seen_at: old };
+ const fresh = { status: 'healthy', last_seen_at: recent };
+ expect(filterStaleInstances([stale, fresh])).toEqual([stale]);
+ });
+
+ it('returns empty array for empty input', () => {
+ expect(filterStaleInstances([])).toEqual([]);
+ });
+});
diff --git a/admin/ui/src/utils/test-utils.js b/admin/ui/src/utils/test-utils.js
new file mode 100644
index 0000000..ead8075
--- /dev/null
+++ b/admin/ui/src/utils/test-utils.js
@@ -0,0 +1,13 @@
+import { createApp } from 'vue';
+
+export function withSetup(composable) {
+ let result;
+ const app = createApp({
+ setup() {
+ result = composable();
+ return () => {};
+ },
+ });
+ app.mount(document.createElement('div'));
+ return { result, app };
+}
diff --git a/admin/ui/src/utils/validation.js b/admin/ui/src/utils/validation.js
new file mode 100644
index 0000000..d2402e1
--- /dev/null
+++ b/admin/ui/src/utils/validation.js
@@ -0,0 +1,6 @@
+export function validateInstanceForm(name, address) {
+ return {
+ name: name.trim() ? '' : 'Name is required',
+ address: address.trim() ? '' : 'Address is required',
+ };
+}
diff --git a/admin/ui/src/utils/validation.test.js b/admin/ui/src/utils/validation.test.js
new file mode 100644
index 0000000..4bfa41f
--- /dev/null
+++ b/admin/ui/src/utils/validation.test.js
@@ -0,0 +1,40 @@
+import { describe, it, expect } from 'vitest';
+import { validateInstanceForm } from './validation.js';
+
+describe('validateInstanceForm', () => {
+ it('returns no errors for valid inputs', () => {
+ const errors = validateInstanceForm('proxy-1', '10.0.0.1:9090');
+ expect(errors.name).toBe('');
+ expect(errors.address).toBe('');
+ });
+
+ it('returns name error when name is empty', () => {
+ const errors = validateInstanceForm('', '10.0.0.1:9090');
+ expect(errors.name).toBe('Name is required');
+ expect(errors.address).toBe('');
+ });
+
+ it('returns address error when address is empty', () => {
+ const errors = validateInstanceForm('proxy-1', '');
+ expect(errors.address).toBe('Address is required');
+ expect(errors.name).toBe('');
+ });
+
+ it('returns both errors when both are empty', () => {
+ const errors = validateInstanceForm('', '');
+ expect(errors.name).toBe('Name is required');
+ expect(errors.address).toBe('Address is required');
+ });
+
+ it('treats whitespace-only as empty', () => {
+ const errors = validateInstanceForm(' ', ' \t ');
+ expect(errors.name).toBe('Name is required');
+ expect(errors.address).toBe('Address is required');
+ });
+
+ it('accepts values with leading/trailing whitespace', () => {
+ const errors = validateInstanceForm(' proxy-1 ', ' 10.0.0.1:9090 ');
+ expect(errors.name).toBe('');
+ expect(errors.address).toBe('');
+ });
+});
diff --git a/admin/ui/src/views/AuditLogView.vue b/admin/ui/src/views/AuditLogView.vue
index b392f56..923247e 100644
--- a/admin/ui/src/views/AuditLogView.vue
+++ b/admin/ui/src/views/AuditLogView.vue
@@ -35,8 +35,8 @@