diff --git a/src/app/[password]/admin/danger/page.tsx b/src/app/[password]/admin/danger/page.tsx
new file mode 100644
index 0000000..2a95b63
--- /dev/null
+++ b/src/app/[password]/admin/danger/page.tsx
@@ -0,0 +1,101 @@
+"use client";
+
+import { downloadDatabase, resetDatabase } from "@/data";
+import { ReactNode, useCallback } from "react";
+import { Slide, toast, ToastContainer } from "react-toastify";
+
+export default function Danger() {
+ const handleDownload = useCallback(async () => {
+ try {
+ const result = await downloadDatabase();
+
+ if (!result.success || !result.data) {
+ toast.error(`Download failed: ${result.error || "Unknown error"}`);
+ return;
+ }
+
+ const byteCharacters = atob(result.data);
+ const byteNumbers = new Array(byteCharacters.length);
+ for (let i = 0; i < byteCharacters.length; i++) {
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
+ }
+ const byteArray = new Uint8Array(byteNumbers);
+ const blob = new Blob([byteArray], { type: result.mimeType });
+
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = result.fileName;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ toast.error(`Download failed: ${message}`);
+ console.error("Download error:", error);
+ }
+ }, []);
+
+ const handleReset = useCallback(() => {
+ toast.warning(
+ "WARNING: This will permanently delete ALL database content. " +
+ "Are you ABSOLUTELY sure?",
+ {
+ autoClose: false,
+ closeButton: ToastButtons({
+ onOk: async () => {
+ const result = await resetDatabase();
+ if (!result.success) {
+ toast.error(result.message);
+ console.error("Reset error:", result);
+ return;
+ }
+ toast.success(result.message, { closeButton: false });
+ },
+ }),
+ }
+ );
+ }, []);
+
+ return (
+
+
+ Danger Zone
+
+ IT Admin Only
+
+
+
+
+
+ );
+}
+
+function ToastButtons({
+ onOk,
+}: {
+ onOk: () => void;
+}): ({ closeToast }: { closeToast: () => void }) => ReactNode {
+ const ToastButtonsResult = ({ closeToast }: { closeToast: () => void }) => (
+ <>
+
+
+ >
+ );
+ return ToastButtonsResult;
+}
diff --git a/src/data.ts b/src/data.ts
index 93dfaad..6d3e00d 100644
--- a/src/data.ts
+++ b/src/data.ts
@@ -19,8 +19,11 @@ export interface Menu {
stocks: number;
}
+const DB_PATH = process.env["OVERCLICKED_DB_DIR"] + "/db.sqlite";
+const DB_DOWNLOAD_FILENAME = "overclicked_db_backup.sqlite";
+
function runOnDb
(func: (db: Database.Database) => T) {
- const db = Database(process.env["OVERCLICKED_DB_DIR"] + "/db.sqlite");
+ const db = Database(DB_PATH);
db.exec(fs.readFileSync("migration.sql").toString());
const res = db.transaction(func)(db);
db.close();
@@ -128,3 +131,54 @@ export const addMenuStocks = async (menu: number, stocks: number) =>
)
.run([stocks, stocks, menu])
);
+
+export const downloadDatabase = async () => {
+ try {
+ if (!fs.existsSync(DB_PATH)) {
+ throw new Error("Database file not found on server.");
+ }
+ const fileBuffer = fs.readFileSync(DB_PATH);
+ const base64Data = fileBuffer.toString("base64");
+
+ return {
+ success: true,
+ data: base64Data,
+ fileName: DB_DOWNLOAD_FILENAME,
+ mimeType: "application/x-sqlite3",
+ error: null,
+ };
+ } catch (e) {
+ console.error("Failed to download database file:", e);
+ return {
+ success: false,
+ data: null,
+ fileName: DB_DOWNLOAD_FILENAME,
+ mimeType: "application/x-sqlite3",
+ error: `Failed to download database: ${
+ e instanceof Error ? e.message : String(e)
+ }`,
+ };
+ }
+};
+
+export const resetDatabase = async () => {
+ try {
+ if (fs.existsSync(DB_PATH)) {
+ // Synchronously delete the file
+ fs.unlinkSync(DB_PATH);
+ return {
+ success: true,
+ message: "Database deleted! It will be recreated on next access",
+ };
+ }
+ return {
+ success: true,
+ message: "Database was already missing",
+ };
+ } catch (e) {
+ console.error("Failed to delete database file:", e);
+ throw new Error(
+ `Failed to reset database: ${e instanceof Error ? e.message : String(e)}`
+ );
+ }
+};
diff --git a/src/styles/pages/_danger.scss b/src/styles/pages/_danger.scss
new file mode 100644
index 0000000..1754339
--- /dev/null
+++ b/src/styles/pages/_danger.scss
@@ -0,0 +1,28 @@
+.danger-page {
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ max-width: 60ch;
+ margin: 4em auto;
+ gap: 1em;
+
+ width: 100%;
+
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ h1 {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ button {
+ width: 100%;
+ }
+
+ .Toastify__toast--warning {
+ flex-direction: column;
+ }
+}
diff --git a/src/styles/pages/_index.scss b/src/styles/pages/_index.scss
index 904bda6..0739bbc 100644
--- a/src/styles/pages/_index.scss
+++ b/src/styles/pages/_index.scss
@@ -2,4 +2,5 @@
@forward "preparation";
@forward "admin";
@forward "public";
-@forward "finished";
\ No newline at end of file
+@forward "finished";
+@forward "danger";
\ No newline at end of file
diff --git a/src/styles/pages/_public.scss b/src/styles/pages/_public.scss
index 6c5fec1..e681f14 100644
--- a/src/styles/pages/_public.scss
+++ b/src/styles/pages/_public.scss
@@ -8,7 +8,6 @@
width: 100%;
height: 100vh;
- background-color: #000;
color: #f8f7c8;
font-size: 2rem;