(add): Backup/Restore

This commit is contained in:
2026-07-16 22:38:01 -04:00
parent 76040c1f91
commit 1e6b7787b3
14 changed files with 343 additions and 12 deletions
@@ -36,9 +36,13 @@
type="number"
id="id_from"
class={iS.normal}
title="Alt + Q"
onclick={(e) => {
e.target.select();
}}
onchange={() => {
if (functions.pagerFromUpdate) functions.pagerFromUpdate();
}}
bind:value={pager.idFrom}
/>
<div>-</div>
@@ -46,6 +50,7 @@
type="number"
id="id_to"
class={iS.normal}
title="Alt + W"
onclick={(e) => {
e.target.select();
}}
@@ -62,6 +67,7 @@
{#if functions.prevPage}
<button
class={bS[prefix.color]}
title="Alt + B"
onclick={() => {
functions.prevPage();
}}>Prev Page</button
@@ -70,6 +76,7 @@
{#if functions.nextPage}
<button
class={bS[prefix.color]}
title="Alt + N"
onclick={() => {
functions.nextPage();
}}>Next Page</button
+1 -1
View File
@@ -43,7 +43,7 @@ export const iS = {
};
export const rBS = {
white: 'even:bg-gray-100',
white: 'even:bg-gray-200',
blue: 'even:bg-blue-100',
yellow: 'even:bg-yellow-100',
green: 'even:bg-green-100',
@@ -0,0 +1,58 @@
import { db } from '$lib/server/db';
import { baskets, prefixes, tickets } from '$lib/server/db/schema';
import { json } from '@sveltejs/kit';
import { sql } from 'drizzle-orm';
const chunk_size = 300;
export const GET = async () => {
const data = {};
data.prefixes = await db.select().from(prefixes);
data.baskets = await db.select().from(baskets);
data.tickets = await db.select().from(tickets);
return json(data);
};
export const POST = async ({ request }) => {
const reqData = await request.json();
for (let i = 0; i < reqData.prefixes.length; i += chunk_size) {
const chunk = reqData.prefixes.slice(i, i + chunk_size);
await db
.insert(prefixes)
.values(chunk)
.onConflictDoUpdate({
target: prefixes.prefix,
set: { color: sql`EXCLUDED.color`, weight: sql`EXCLUDED.weight` }
});
}
for (let i = 0; i < reqData.baskets.length; i += chunk_size) {
const chunk = reqData.baskets.slice(i, i + chunk_size);
await db
.insert(baskets)
.values(chunk)
.onConflictDoUpdate({
target: [baskets.prefix, baskets.b_id],
set: {
description: sql`EXCLUDED.description`,
donors: sql`EXCLUDED.donors`,
winning_ticket: sql`EXCLUDED.winning_ticket`
}
});
}
for (let i = 0; i < reqData.tickets.length; i += chunk_size) {
const chunk = reqData.tickets.slice(i, i + chunk_size);
await db
.insert(tickets)
.values(chunk)
.onConflictDoNothing({
target: [tickets.prefix, tickets.t_id],
set: {
first_name: sql`EXCLUDED.first_name`,
last_name: sql`EXCLUDED.last_name`,
phone_number: sql`EXCLUDED.phone_number`,
pref: sql`EXCLUDED.pref`
}
});
}
return json({ details: 'Data loaded successfully.' });
};
@@ -0,0 +1,32 @@
import { db } from '$lib/server/db/index.js';
import { baskets, prefixes, tickets } from '$lib/server/db/schema';
import { getPath, getSettings } from '$lib/server/settings/index.js';
import { error } from '@sveltejs/kit';
export const HEAD = async ({ params }) => {
const target = params.target;
const pushData = {};
if (target === 'prefixes') {
pushData.prefixes = await db.select().from(prefixes);
} else if (target === 'tickets') {
pushData.tickets = await db.select().from(tickets);
} else if (target === 'baskets') {
pushData.baskets = await db.select().from(baskets);
} else {
throw error(400, 'Can only push prefixes, tickets, or baskets.');
}
const s = getSettings();
if (s.remote_server) {
const connStr = getPath(s);
const res = await fetch(`${connStr}/api/backuprestore`, {
method: 'POST',
headers: { 'TAM-KEY': s.remote_key, 'Content-Type': 'application/json' },
body: JSON.stringify(pushData)
});
if (!res.ok) throw error(res.status, res.statusText);
return new Response(null, { status: 200, statusText: 'Data pushed successfully.' });
} else {
throw error(500, 'Server not set.');
};
};
@@ -0,0 +1,36 @@
import { getPath, getSettings } from '$lib/server/settings';
import { error, json } from '@sveltejs/kit';
export const GET = async () => {
const s = getSettings();
if (s.remote_server) {
const connStr = getPath(s);
try {
const res = await fetch(`${connStr}/api/backuprestore`, {
headers: { TAM_KEY: s.remote_key }
});
if (!res.ok) throw error(res.status);
const data = await res.json();
return json(data);
} catch {
return json({});
}
} else {
return json({});
}
};
export const POST = async ({ request }) => {
const resData = await request.json();
const s = getSettings();
if (s.remote_server) {
const connStr = getPath(s);
const res = await fetch(`${connStr}/api/backuprestore`, {
method: 'POST',
headers: { 'TAM-KEY': s.remote_key, 'Content-Type': 'application/json' },
body: JSON.stringify(resData)
});
if (!res.ok) throw error(res.status);
return json([]);
}
};
@@ -21,7 +21,7 @@
curIdx = idx;
const elemIdx = document.getElementById(`${idx}_first`);
if (elemIdx) {
elemIdx.focus();
elemIdx.select();
}
};
@@ -44,6 +44,13 @@
items = [...resData];
setTimeout(() => focusIdx(0));
},
pagerFromUpdate(){
if (itemsLength === 0) {
pager.idTo = pager.idFrom;
} else {
pager.idTo = pager.idFrom + (itemsLength - 1);
}
},
save: async () => {
if (itemsBuffer.length > 0) {
const res = await fetch('/api/baskets', {
@@ -21,7 +21,7 @@
curIdx = idx;
const elemIdx = document.getElementById(`${idx}_first`);
if (elemIdx) {
elemIdx.focus();
elemIdx.select();
}
};
@@ -44,6 +44,13 @@
items = [...resData];
setTimeout(() => focusIdx(0));
},
pagerFromUpdate(){
if (itemsLength === 0) {
pager.idTo = pager.idFrom;
} else {
pager.idTo = pager.idFrom + (itemsLength - 1);
}
},
save: async () => {
if (itemsBuffer.length > 0) {
const res = await fetch('/api/drawing', {
+1
View File
@@ -29,6 +29,7 @@
<a href={resolve('/settings/auth-keys')} class={bS.gray}>Auth Keys</a>
{/if}
<a href={resolve('/settings/prefixes')} class={bS.gray}>Prefixes</a>
<a href={resolve('/settings/backuprestore')} class={bS.gray}>Backup/Restore</a>
</HeaderBar>
<h1 class="text-xl font-bold">{pageTitle}</h1>
<div class="flex flex-col gap-1 w-full py-1">
@@ -0,0 +1,6 @@
import { getSettings } from "$lib/server/settings/index.js"
export const load = async () => {
const s = getSettings();
return { remoteServer: s.remote_server };
}
@@ -0,0 +1,131 @@
<script>
import { resolve } from '$app/paths';
import HeaderBar from '$lib/client/components/HeaderBar.svelte';
import { bS, iS } from '$lib/client/styles';
const pageTitle = 'Backup and Restore | TAM';
let { data } = $props();
let remoteServer = $derived(data.remoteServer);
let uploadFile = $state();
let contents = $state('');
let results = $state('');
function setResult(newResult) {
results = newResult;
setTimeout(() => (results = ''), 10000);
}
async function downloadBackupFile(target) {
let fetch_url;
if (target === 'local') {
fetch_url = '/api/backuprestore/local';
} else if (target === 'remote') {
fetch_url = '/api/backuprestore/remote';
}
const now = new Date();
const date = {
year: now.getFullYear(),
month: String(now.getMonth() + 1).padStart(2, '0'),
day: String(now.getDate()).padStart(2, '0'),
hour: String(now.getHours()).padStart(2, '0'),
minutes: String(now.getMinutes()).padStart(2, '0')
};
const res = await fetch(fetch_url);
if (res.ok) {
const data = await res.json();
const jsonString = JSON.stringify(data, null, 2);
const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `TAM_${date.year}-${date.month}-${date.day} ${date.hour}:${date.minutes}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
}
async function fileUpload(target) {
const reader = new FileReader();
reader.readAsText(uploadFile[0], 'utf-8');
reader.onload = function (e) {
contents = String(e.target.result);
};
console.log(contents);
setTimeout(async () => {
const res = await fetch(`/api/backuprestore/${target}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: contents
});
if (res.ok) setResult('File uploaded successfully. Check to see if your data exists.');
else setResult('Error uploading file.');
}, 100);
}
async function pushData(target) {
const res = await fetch(`/api/backuprestore/push/${target}`, { method: 'HEAD' });
const targetStr = target.charAt(0).toUpperCase() + target.slice(1);
if (res.ok) {
setResult(`${targetStr} pushed successfully.`);
} else {
setResult(`Error pushing ${targetStr}. Error [${res.status}] ${res.statusText}.`);
}
}
</script>
<svelte:head>
<title>{pageTitle}</title>
</svelte:head>
<div id="app_container" class="p-1">
<HeaderBar>
<a href={resolve('/settings')} class={bS.gray}>Back to Settings</a>
<div>Other Settings</div>
<a href={resolve('/settings/prefixes')} class={bS.gray}>Prefixes</a>
{#if remoteServer}
<a href={resolve('/settings/auth-keys')} class={bS.gray}>Auth Keys</a>
{/if}
</HeaderBar>
<h1 class="text-xl font-bold">{pageTitle}</h1>
<div class="my-1 p-1 border border-black rounded">
<h2 class="text-lg font-bold">Backup File Downloads</h2>
<div class="flex flex-row gap-1">
<button class={bS.gray} onclick={downloadBackupFile('local')}>Local Data</button>
{#if remoteServer}
<button class={bS.gray} onclick={downloadBackupFile('remote')}>Remote Data</button>
{/if}
</div>
</div>
<div class="my-1 p-1 border border-black rounded">
<h2 class="text-lg font-bold">Backup File Upload</h2>
<div class="flex flex-row gap-1 items-center">
<input
type="file"
accept=".json"
class="{iS.normal} rounded file:bg-gray-300 file:border file:border-black file:px-2 file:py-1 file:rounded"
bind:files={uploadFile}
/>
<button class={bS.gray} onclick={() => fileUpload('local')}>Upload to Local</button>
{#if remoteServer}
<button class={bS.gray} onclick={() => fileUpload('remote')}>Upload to Remote</button>
{/if}
</div>
</div>
{#if remoteServer}
<div class="my-1 p-1 border border-black rounded">
<h2 class="text-lg font-bold">Push Data to Server</h2>
<div class="flex flex-row gap-1">
<button class={bS.gray} onclick={() => pushData('prefixes')}>Push Prefixes</button>
<button class={bS.gray} onclick={() => pushData('tickets')}>Push Tickets</button>
<button class={bS.gray} onclick={() => pushData('baskets')}>Push Baskets</button>
</div>
</div>
{/if}
<div class="my-1 p-1 border border-black rounded">
<span class="font-bold">Status: </span><span>{results}</span>
</div>
</div>
@@ -44,6 +44,13 @@
items = [...resData];
setTimeout(() => focusIdx(0));
},
pagerFromUpdate(){
if (itemsLength === 0) {
pager.idTo = pager.idFrom;
} else {
pager.idTo = pager.idFrom + (itemsLength - 1);
}
},
save: async () => {
if (itemsBuffer.length > 0) {
const res = await fetch('/api/tickets', {