feat(frontend): implement basic frontend interface
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
import { reactive } from 'vue';
|
||||
|
||||
const TOKEN_KEY = 'nukumizu_token';
|
||||
const USER_KEY = 'nukumizu_user';
|
||||
|
||||
function readUser() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(USER_KEY)) || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const auth = reactive({
|
||||
token: localStorage.getItem(TOKEN_KEY) || '',
|
||||
user: readUser()
|
||||
});
|
||||
|
||||
export function setSession(token, user) {
|
||||
auth.token = token;
|
||||
auth.user = user;
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(user));
|
||||
}
|
||||
|
||||
export function clearSession() {
|
||||
auth.token = '';
|
||||
auth.user = null;
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
localStorage.removeItem(USER_KEY);
|
||||
}
|
||||
|
||||
export function getToken() {
|
||||
return auth.token;
|
||||
}
|
||||
|
||||
export function isLoggedIn() {
|
||||
return !!auth.token;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Severity levels as sent by the backend log websocket (see postLog package):
|
||||
// 0=DEBUG 1=INFO 2=WARN 3=ERROR 4=FATAL
|
||||
export const LOG_LEVELS = [
|
||||
{ key: 'DEBUG', value: 0 },
|
||||
{ key: 'INFO', value: 1 },
|
||||
{ key: 'WARN', value: 2 },
|
||||
{ key: 'ERROR', value: 3 },
|
||||
{ key: 'FATAL', value: 4 }
|
||||
];
|
||||
|
||||
export const levelOf = (value) => LOG_LEVELS.find((l) => l.value === value) || { key: 'LOG', value };
|
||||
|
||||
export function formatBytes(n, digits = 1) {
|
||||
if (n === null || n === undefined || Number.isNaN(n)) return '-';
|
||||
if (n < 1024) return `${Math.round(n)} B`;
|
||||
const units = ['KB', 'MB', 'GB', 'TB', 'PB'];
|
||||
let v = n;
|
||||
let u = -1;
|
||||
do {
|
||||
v /= 1024;
|
||||
u += 1;
|
||||
} while (v >= 1024 && u < units.length - 1);
|
||||
return `${v.toFixed(digits)} ${units[u]}`;
|
||||
}
|
||||
|
||||
export function formatPercent(used, total) {
|
||||
if (!total) return '—';
|
||||
return `${Math.min(100, Math.max(0, Math.round((used / total) * 100)))}%`;
|
||||
}
|
||||
|
||||
export function shortUUID(uuid = '') {
|
||||
if (!uuid) return '';
|
||||
if (uuid.length <= 13) return uuid;
|
||||
return `${uuid.slice(0, 8)}…${uuid.slice(-4)}`;
|
||||
}
|
||||
|
||||
// Online/offline counts + aggregate online ratio. Report arrays carry the
|
||||
// report; when absent the server has no live telemetry yet.
|
||||
export function sumUp(entries) {
|
||||
const total = entries.length;
|
||||
const online = entries.filter((e) => e.online).length;
|
||||
return { total, online, offline: total - online };
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import axios from 'axios';
|
||||
import { getToken, clearSession } from './auth.js';
|
||||
|
||||
const http = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 15000
|
||||
});
|
||||
|
||||
http.interceptors.request.use((config) => {
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
config.headers['X-Token'] = token;
|
||||
}
|
||||
// Backend expects a Unix timestamp in *seconds* with a ±30 min tolerance.
|
||||
config.headers['X-Timestamp'] = Math.floor(Date.now() / 1000);
|
||||
return config;
|
||||
});
|
||||
|
||||
http.interceptors.response.use(
|
||||
(response) => response.data,
|
||||
(error) => {
|
||||
const status = error.response ? error.response.status : 0;
|
||||
const payload = error.response && error.response.data;
|
||||
const message = (payload && payload.message) || error.message || 'Network error';
|
||||
|
||||
if (status === 401) {
|
||||
clearSession();
|
||||
window.dispatchEvent(new CustomEvent('auth:expired'));
|
||||
}
|
||||
|
||||
const err = new Error(message);
|
||||
err.status = status;
|
||||
return Promise.reject(err);
|
||||
}
|
||||
);
|
||||
|
||||
export default http;
|
||||
@@ -0,0 +1,32 @@
|
||||
const KEY = 'nukumizu_theme';
|
||||
|
||||
function systemPrefersDark() {
|
||||
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
|
||||
export function applyTheme(theme) {
|
||||
const dark = theme === 'dark' || (theme !== 'light' && systemPrefersDark());
|
||||
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
|
||||
return dark ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
export function currentTheme() {
|
||||
return document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
export function initTheme() {
|
||||
const saved = localStorage.getItem(KEY);
|
||||
applyTheme(saved || 'system');
|
||||
}
|
||||
|
||||
export function setTheme(theme) {
|
||||
localStorage.setItem(KEY, theme);
|
||||
return applyTheme(theme);
|
||||
}
|
||||
|
||||
export function toggleTheme() {
|
||||
const next = currentTheme() === 'dark' ? 'light' : 'dark';
|
||||
localStorage.setItem(KEY, next);
|
||||
applyTheme(next);
|
||||
return next;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { reactive } from 'vue';
|
||||
|
||||
let seq = 0;
|
||||
|
||||
export const toasts = reactive([]);
|
||||
|
||||
function dismiss(id) {
|
||||
const idx = toasts.findIndex((t) => t.id === id);
|
||||
if (idx !== -1) toasts[idx].leaving = true;
|
||||
setTimeout(() => {
|
||||
const i = toasts.findIndex((t) => t.id === id);
|
||||
if (i !== -1) toasts.splice(i, 1);
|
||||
}, 240);
|
||||
}
|
||||
|
||||
function push(type, message, timeout) {
|
||||
const id = ++seq;
|
||||
toasts.push({ id, type, message, leaving: false });
|
||||
if (timeout > 0) {
|
||||
setTimeout(() => dismiss(id), timeout);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
export const toast = {
|
||||
success(message, timeout = 3600) {
|
||||
return push('success', message, timeout);
|
||||
},
|
||||
error(message, timeout = 5200) {
|
||||
return push('error', message, timeout);
|
||||
},
|
||||
info(message, timeout = 3200) {
|
||||
return push('info', message, timeout);
|
||||
},
|
||||
warn(message, timeout = 4200) {
|
||||
return push('warn', message, timeout);
|
||||
},
|
||||
dismiss
|
||||
};
|
||||
Reference in New Issue
Block a user