Add first-run setup wizard for import sources
All checks were successful
build-and-push / docker (push) Successful in 11s
All checks were successful
build-and-push / docker (push) Successful in 11s
On a fresh install (no integration settings yet), prompt once for the Thingiverse/MakerWorld/Printables tokens and FlareSolverr URL, with a "skip for now" option. /api/config exposes setupComplete (true if any setting is configured or setup was dismissed); POST /api/setup/complete records dismissal. DB password stays a deploy-time env var by necessity. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
5d69acd718
commit
5fb3939bcd
3 changed files with 103 additions and 0 deletions
|
|
@ -15,6 +15,7 @@ const uploadModal = document.getElementById('uploadModal');
|
|||
const projectModal = document.getElementById('projectModal');
|
||||
const importModal = document.getElementById('importModal');
|
||||
const settingsModal = document.getElementById('settingsModal');
|
||||
const setupModal = document.getElementById('setupModal');
|
||||
const editModal = document.getElementById('editModal');
|
||||
const fileDetailModal = document.getElementById('fileDetailModal');
|
||||
|
||||
|
|
@ -39,6 +40,11 @@ async function loadData() {
|
|||
renderModels();
|
||||
updateCounts();
|
||||
generateMissingThumbnails(); // fire-and-forget; updates cards as they finish
|
||||
|
||||
// First-run onboarding: prompt for import sources once.
|
||||
if (config.setupComplete === false && !setupModal.classList.contains('active')) {
|
||||
setupModal.classList.add('active');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error);
|
||||
}
|
||||
|
|
@ -170,9 +176,13 @@ function setupEventListeners() {
|
|||
btn.addEventListener('click', (e) => closeModal(e.target.closest('.modal')));
|
||||
});
|
||||
document.querySelectorAll('.modal').forEach((modal) => {
|
||||
if (modal.id === 'setupModal') return; // must be finished or skipped, not backdrop-dismissed
|
||||
modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(modal); });
|
||||
});
|
||||
|
||||
document.getElementById('setupForm').addEventListener('submit', handleFinishSetup);
|
||||
document.getElementById('skipSetup').addEventListener('click', handleSkipSetup);
|
||||
|
||||
document.getElementById('uploadForm').addEventListener('submit', handleUpload);
|
||||
document.getElementById('cancelUpload').addEventListener('click', () => closeModal(uploadModal));
|
||||
document.getElementById('projectForm').addEventListener('submit', handleCreateProject);
|
||||
|
|
@ -259,6 +269,42 @@ function openProjectModal() {
|
|||
projectModal.classList.add('active');
|
||||
}
|
||||
|
||||
async function markSetupComplete() {
|
||||
try {
|
||||
await fetch('/api/setup/complete', { method: 'POST' });
|
||||
config.setupComplete = true;
|
||||
} catch { /* non-fatal */ }
|
||||
}
|
||||
|
||||
async function handleFinishSetup(e) {
|
||||
e.preventDefault();
|
||||
const status = document.getElementById('setupStatus');
|
||||
const payload = {};
|
||||
document.querySelectorAll('#setupForm [data-setting]').forEach((input) => {
|
||||
if (input.value.trim()) payload[input.dataset.setting] = input.value.trim();
|
||||
});
|
||||
try {
|
||||
if (Object.keys(payload).length) {
|
||||
const res = await fetch('/api/settings', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!res.ok) throw new Error((await res.json()).error);
|
||||
}
|
||||
await markSetupComplete();
|
||||
closeModal(setupModal);
|
||||
await loadData();
|
||||
} catch (err) {
|
||||
status.textContent = 'Could not save: ' + err.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSkipSetup() {
|
||||
await markSetupComplete();
|
||||
closeModal(setupModal);
|
||||
}
|
||||
|
||||
async function openSettingsModal() {
|
||||
document.getElementById('settingsForm').reset();
|
||||
document.getElementById('settingsStatus').textContent = '';
|
||||
|
|
|
|||
|
|
@ -69,6 +69,50 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- First-run Setup Modal -->
|
||||
<div id="setupModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>Welcome to Printhive</h2>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="form-hint" style="margin-bottom:18px">
|
||||
Optionally connect import sources now — you can change these any time in Settings.
|
||||
All are optional; you can also just upload files directly.
|
||||
</p>
|
||||
<form id="setupForm">
|
||||
<div class="form-group">
|
||||
<label for="setupThingiverse">Thingiverse API token</label>
|
||||
<input type="password" id="setupThingiverse" data-setting="thingiverse_token" autocomplete="off"
|
||||
placeholder="App Token from thingiverse.com/developers">
|
||||
<p class="form-hint">Enables automatic Thingiverse imports.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="setupFlaresolverr">FlareSolverr URL</label>
|
||||
<input type="text" id="setupFlaresolverr" data-setting="flaresolverr_url" autocomplete="off"
|
||||
placeholder="http://192.168.1.124:8191">
|
||||
<p class="form-hint">Lets imports read Cloudflare-protected sites like MakerWorld.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="setupMakerworld">MakerWorld auth token</label>
|
||||
<input type="password" id="setupMakerworld" data-setting="makerworld_token" autocomplete="off"
|
||||
placeholder="Bearer token from your logged-in session">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="setupPrintables">Printables auth token</label>
|
||||
<input type="password" id="setupPrintables" data-setting="printables_token" autocomplete="off"
|
||||
placeholder="Optional — for account-gated files">
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" id="skipSetup">Skip for now</button>
|
||||
<button type="submit" class="btn btn-primary">Save & finish</button>
|
||||
</div>
|
||||
</form>
|
||||
<div id="setupStatus" class="import-status"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Modal -->
|
||||
<div id="settingsModal" class="modal">
|
||||
<div class="modal-content">
|
||||
|
|
|
|||
13
server.js
13
server.js
|
|
@ -98,9 +98,14 @@ app.get('/api/config', async (req, res, next) => {
|
|||
try {
|
||||
const thingiverse = await resolveSetting('thingiverse_token');
|
||||
const flare = await resolveSetting('flaresolverr_url');
|
||||
const makerworld = await resolveSetting('makerworld_token');
|
||||
const printables = await resolveSetting('printables_token');
|
||||
const alreadyConfigured = Boolean(thingiverse.value || flare.value || makerworld.value || printables.value);
|
||||
const setupComplete = alreadyConfigured || Boolean(await store.getSetting('setup_complete'));
|
||||
res.json({
|
||||
importers: { thingiverse: Boolean(thingiverse.value), flaresolverr: Boolean(flare.value) },
|
||||
formats: fileProcessor.MODEL_FORMATS,
|
||||
setupComplete,
|
||||
});
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
|
@ -126,6 +131,14 @@ app.get('/api/settings', async (req, res, next) => {
|
|||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
// Mark the first-run setup wizard as done (so it won't show again).
|
||||
app.post('/api/setup/complete', async (req, res, next) => {
|
||||
try {
|
||||
await store.setSetting('setup_complete', 'true');
|
||||
res.json({ ok: true });
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
app.put('/api/settings', async (req, res, next) => {
|
||||
try {
|
||||
for (const [key, value] of Object.entries(req.body || {})) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue