feat: initialize dashboard with bug reporting functionality

- Add HTML structure for the dashboard application.
- Create database schema for bug reports and associated files.
- Implement database connection using Drizzle ORM with MySQL.
- Add utility functions for class names, byte formatting, and date formatting.
- Create error handling page for the application.
- Implement layout and main page structure with navigation and report listing.
- Add server-side logic for loading reports with pagination and filtering.
- Create report detail page with metadata, description, and file attachments.
- Implement API endpoints for downloading reports and files, refreshing report counts, and managing report statuses.
- Set up SvelteKit configuration and TypeScript support.
- Configure Vite for SvelteKit and Tailwind CSS integration.
- Update Docker Compose configuration for the dashboard service.
- Create systemd service for managing the dashboard server.
This commit is contained in:
Flavio Fois
2026-02-14 23:01:08 +01:00
parent c2052595cb
commit 492db8fcf8
28 changed files with 25 additions and 1 deletions

View File

@@ -0,0 +1,54 @@
import {
mysqlTable,
int,
varchar,
text,
json,
mysqlEnum,
timestamp,
customType
} from 'drizzle-orm/mysql-core';
const longblob = customType<{ data: Buffer }>({
dataType() {
return 'longblob';
}
});
export const bugReports = mysqlTable('bug_reports', {
id: int('id').autoincrement().primaryKey(),
name: varchar('name', { length: 255 }).notNull(),
email: varchar('email', { length: 255 }).notNull(),
description: text('description').notNull(),
hwid: varchar('hwid', { length: 255 }).notNull().default(''),
hostname: varchar('hostname', { length: 255 }).notNull().default(''),
os_user: varchar('os_user', { length: 255 }).notNull().default(''),
submitter_ip: varchar('submitter_ip', { length: 45 }).notNull().default(''),
system_info: json('system_info'),
status: mysqlEnum('status', ['new', 'in_review', 'resolved', 'closed']).notNull().default('new'),
created_at: timestamp('created_at').notNull().defaultNow(),
updated_at: timestamp('updated_at').notNull().defaultNow().onUpdateNow()
});
export const bugReportFiles = mysqlTable('bug_report_files', {
id: int('id').autoincrement().primaryKey(),
report_id: int('report_id')
.notNull()
.references(() => bugReports.id, { onDelete: 'cascade' }),
file_role: mysqlEnum('file_role', [
'screenshot',
'mail_file',
'localstorage',
'config',
'system_info'
]).notNull(),
filename: varchar('filename', { length: 255 }).notNull(),
mime_type: varchar('mime_type', { length: 127 }).notNull().default('application/octet-stream'),
file_size: int('file_size').notNull().default(0),
data: longblob('data').notNull(),
created_at: timestamp('created_at').notNull().defaultNow()
});
export type BugReport = typeof bugReports.$inferSelect;
export type BugReportFile = typeof bugReportFiles.$inferSelect;
export type BugReportStatus = BugReport['status'];

View File

@@ -0,0 +1,16 @@
import { drizzle } from 'drizzle-orm/mysql2';
import mysql from 'mysql2/promise';
import * as schema from '$lib/schema';
import { env } from '$env/dynamic/private';
const pool = mysql.createPool({
host: env.MYSQL_HOST || 'localhost',
port: Number(env.MYSQL_PORT) || 3306,
user: env.MYSQL_USER || 'emly',
password: env.MYSQL_PASSWORD,
database: env.MYSQL_DATABASE || 'emly_bugreports',
connectionLimit: 10,
idleTimeout: 60000
});
export const db = drizzle(pool, { schema, mode: 'default' });

View File

@@ -0,0 +1,39 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function formatBytes(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
}
export function formatDate(date: Date | string): string {
const d = typeof date === 'string' ? new Date(date) : date;
return d.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
export const statusColors: Record<string, string> = {
new: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
in_review: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30',
resolved: 'bg-green-500/20 text-green-400 border-green-500/30',
closed: 'bg-zinc-500/20 text-zinc-400 border-zinc-500/30'
};
export const statusLabels: Record<string, string> = {
new: 'New',
in_review: 'In Review',
resolved: 'Resolved',
closed: 'Closed'
};