From 16ac3f0eea3b25f8a863f6b08b02479ab1648d02 Mon Sep 17 00:00:00 2001 From: Sidonie Bouthors Date: Thu, 13 Nov 2025 14:17:20 +0100 Subject: [PATCH 1/2] feat: add danger page to download and reset database --- src/app/[password]/admin/danger/page.tsx | 101 +++++++++++++++++++++++ src/data.ts | 56 ++++++++++++- src/styles/pages/_danger.scss | 28 +++++++ src/styles/pages/_index.scss | 3 +- 4 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 src/app/[password]/admin/danger/page.tsx create mode 100644 src/styles/pages/_danger.scss 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 From c3762935ee628b6318780b899fd796a319fd7660 Mon Sep 17 00:00:00 2001 From: Sidonie Bouthors Date: Thu, 13 Nov 2025 14:17:55 +0100 Subject: [PATCH 2/2] fix: optimize public background image --- .../admin/[register]/public/page.tsx | 23 +++++++++++-------- src/styles/pages/_public.scss | 1 - 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/app/[password]/admin/[register]/public/page.tsx b/src/app/[password]/admin/[register]/public/page.tsx index 5a3f253..644a502 100644 --- a/src/app/[password]/admin/[register]/public/page.tsx +++ b/src/app/[password]/admin/[register]/public/page.tsx @@ -4,6 +4,7 @@ import { getMenus, getOrdersToServe, Menu, Order } from "@/data"; import { delay } from "@/utils"; import { useParams } from "next/navigation"; import { useEffect, useState } from "react"; +import Image from "next/image"; import backgroundImage from "@public/background.webp"; export default function Public() { @@ -30,15 +31,19 @@ export default function Public() { }, []); return ( -
+
+ Background

Croques

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;