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:
54
server/dashboard/src/lib/schema.ts
Normal file
54
server/dashboard/src/lib/schema.ts
Normal 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'];
|
||||
16
server/dashboard/src/lib/server/db.ts
Normal file
16
server/dashboard/src/lib/server/db.ts
Normal 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' });
|
||||
39
server/dashboard/src/lib/utils.ts
Normal file
39
server/dashboard/src/lib/utils.ts
Normal 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'
|
||||
};
|
||||
Reference in New Issue
Block a user