226 lines
9.2 KiB
Plaintext
226 lines
9.2 KiB
Plaintext
---
|
|
export const prerender = false;
|
|
import Layout from '../layouts/Main.astro';
|
|
import NavItems from '../components/NavItems.astro';
|
|
import NavBar from '../components/NavBar.astro';
|
|
import Footer from '../components/Footer.astro';
|
|
---
|
|
<Layout title="Admin Panel">
|
|
<NavBar slot="navbar">
|
|
<NavItems slot="navItems" />
|
|
</NavBar>
|
|
<div slot="page">
|
|
<div class="container my-4">
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<h1>Admin Panel</h1>
|
|
|
|
<div class="accordion" id="adminAccordion">
|
|
<div class="accordion-item">
|
|
<h2 class="accordion-header" id="reindexHeading">
|
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
|
data-bs-target="#reindexCollapse" aria-expanded="false" aria-controls="reindexCollapse">
|
|
Reindex
|
|
</button>
|
|
</h2>
|
|
<div id="reindexCollapse" class="accordion-collapse collapse" aria-labelledby="reindexHeading"
|
|
data-bs-parent="#adminAccordion">
|
|
<div class="accordion-body">
|
|
<form id="reindexForm">
|
|
<div class="mb-3">
|
|
<div class="form-text mb-2">Select collections to reindex:</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="reindexCards" name="cards" checked />
|
|
<label class="form-check-label" for="reindexCards">Cards</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="reindexSkus" name="skus" checked />
|
|
<label class="form-check-label" for="reindexSkus">SKUs</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="reindexInventory" name="inventory" checked />
|
|
<label class="form-check-label" for="reindexInventory">Inventory</label>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3 form-check">
|
|
<input class="form-check-input" type="checkbox" id="reindexRecreate" name="recreate" />
|
|
<label class="form-check-label" for="reindexRecreate">
|
|
Recreate index (drops and recreates collections; otherwise updates in place)
|
|
</label>
|
|
</div>
|
|
<button type="submit" class="btn btn-purple" id="reindexRun">Run Reindex</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="accordion-item">
|
|
<h2 class="accordion-header" id="tcgImportHeading">
|
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
|
data-bs-target="#tcgImportCollapse" aria-expanded="false" aria-controls="tcgImportCollapse">
|
|
TCG Player Import
|
|
</button>
|
|
</h2>
|
|
<div id="tcgImportCollapse" class="accordion-collapse collapse" aria-labelledby="tcgImportHeading"
|
|
data-bs-parent="#adminAccordion">
|
|
<div class="accordion-body">
|
|
<form id="tcgImportForm">
|
|
<div class="mb-3">
|
|
<label for="tcgImportSetName" class="form-label">Set Name</label>
|
|
<input type="text" class="form-control" id="tcgImportSetName" name="setName"
|
|
placeholder="e.g. Surging Sparks" autocomplete="off" required />
|
|
<div class="form-text">Matches any set whose name contains this text (case-insensitive).</div>
|
|
</div>
|
|
<button type="submit" class="btn btn-purple" id="tcgImportRun">Run Import</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Reusable scrollable progress modal. Open via window.AdminProgress.open(title). -->
|
|
<div class="modal fade" id="adminProgressModal" tabindex="-1" aria-labelledby="adminProgressLabel" aria-hidden="true"
|
|
data-bs-backdrop="static" data-bs-keyboard="false">
|
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="adminProgressLabel">Progress</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"
|
|
id="adminProgressClose" disabled></button>
|
|
</div>
|
|
<div class="modal-body p-0">
|
|
<pre id="adminProgressLog"
|
|
class="m-0 p-3 small"
|
|
style="max-height: 60vh; overflow-y: auto; white-space: pre-wrap; word-break: break-word;"></pre>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<span class="me-auto small text-secondary" id="adminProgressStatus">Idle</span>
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"
|
|
id="adminProgressDismiss" disabled>Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Footer slot="footer" />
|
|
</Layout>
|
|
|
|
<script>
|
|
// Reusable progress modal. Other admin features can call window.AdminProgress.
|
|
type ProgressHandle = {
|
|
append: (line: string) => void;
|
|
setStatus: (text: string) => void;
|
|
done: (text?: string) => void;
|
|
};
|
|
|
|
declare global {
|
|
interface Window {
|
|
AdminProgress: {
|
|
open: (title: string) => Promise<ProgressHandle>;
|
|
};
|
|
}
|
|
}
|
|
|
|
const getBootstrap = (): any => (window as any).bootstrap;
|
|
|
|
const openProgress = async (title: string): Promise<ProgressHandle> => {
|
|
const modalEl = document.getElementById('adminProgressModal')!;
|
|
const labelEl = document.getElementById('adminProgressLabel')!;
|
|
const logEl = document.getElementById('adminProgressLog')!;
|
|
const statusEl = document.getElementById('adminProgressStatus')!;
|
|
const closeBtn = document.getElementById('adminProgressClose') as HTMLButtonElement;
|
|
const dismissBtn = document.getElementById('adminProgressDismiss') as HTMLButtonElement;
|
|
|
|
labelEl.textContent = title;
|
|
logEl.textContent = '';
|
|
statusEl.textContent = 'Running...';
|
|
closeBtn.disabled = true;
|
|
dismissBtn.disabled = true;
|
|
|
|
const modal = getBootstrap().Modal.getOrCreateInstance(modalEl);
|
|
modal.show();
|
|
|
|
return {
|
|
append: (line: string) => {
|
|
logEl.textContent += (logEl.textContent ? '\n' : '') + line;
|
|
logEl.scrollTop = logEl.scrollHeight;
|
|
},
|
|
setStatus: (text: string) => { statusEl.textContent = text; },
|
|
done: (text = 'Done') => {
|
|
statusEl.textContent = text;
|
|
closeBtn.disabled = false;
|
|
dismissBtn.disabled = false;
|
|
},
|
|
};
|
|
};
|
|
|
|
window.AdminProgress = { open: openProgress };
|
|
|
|
// Stream a POST JSON request line-by-line into a progress modal.
|
|
const streamToProgress = async (url: string, body: unknown, title: string, runBtn: HTMLButtonElement) => {
|
|
runBtn.disabled = true;
|
|
const progress = await window.AdminProgress.open(title);
|
|
try {
|
|
const resp = await fetch(url, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
if (!resp.ok || !resp.body) {
|
|
progress.append(`Request failed: ${resp.status} ${resp.statusText}`);
|
|
progress.done('Failed');
|
|
return;
|
|
}
|
|
const reader = resp.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
let buf = '';
|
|
while (true) {
|
|
const { value, done } = await reader.read();
|
|
if (done) break;
|
|
buf += decoder.decode(value, { stream: true });
|
|
const lines = buf.split('\n');
|
|
buf = lines.pop() ?? '';
|
|
for (const line of lines) progress.append(line);
|
|
}
|
|
if (buf) progress.append(buf);
|
|
progress.done('Done');
|
|
} catch (err: any) {
|
|
progress.append(`Error: ${err?.message || String(err)}`);
|
|
progress.done('Failed');
|
|
} finally {
|
|
runBtn.disabled = false;
|
|
}
|
|
};
|
|
|
|
// Reindex form wiring
|
|
const reindexForm = document.getElementById('reindexForm') as HTMLFormElement | null;
|
|
if (reindexForm) {
|
|
reindexForm.addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
const body = {
|
|
cards: (document.getElementById('reindexCards') as HTMLInputElement).checked,
|
|
skus: (document.getElementById('reindexSkus') as HTMLInputElement).checked,
|
|
inventory: (document.getElementById('reindexInventory') as HTMLInputElement).checked,
|
|
recreate: (document.getElementById('reindexRecreate') as HTMLInputElement).checked,
|
|
};
|
|
streamToProgress('/api/reindex', body, 'Reindex',
|
|
document.getElementById('reindexRun') as HTMLButtonElement);
|
|
});
|
|
}
|
|
|
|
// TCG Player import form wiring
|
|
const tcgImportForm = document.getElementById('tcgImportForm') as HTMLFormElement | null;
|
|
if (tcgImportForm) {
|
|
tcgImportForm.addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
const setName = (document.getElementById('tcgImportSetName') as HTMLInputElement).value.trim();
|
|
streamToProgress('/api/preload-tcgplayer', { setName }, `TCG Player Import: ${setName || '(none)'}`,
|
|
document.getElementById('tcgImportRun') as HTMLButtonElement);
|
|
});
|
|
}
|
|
</script>
|