ux: format activity feed JSON, remove default creds hint, add login loading state
This commit is contained in:
+59
-3
@@ -136,6 +136,7 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
max-width: calc(100vw - var(--sidebar-w));
|
max-width: calc(100vw - var(--sidebar-w));
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
.page-title {
|
.page-title {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
@@ -716,6 +717,21 @@
|
|||||||
.cs-repl-status.approved { background: rgba(72,199,142,0.15); color: var(--green); }
|
.cs-repl-status.approved { background: rgba(72,199,142,0.15); color: var(--green); }
|
||||||
.cs-repl-status.rejected { background: rgba(240,61,62,0.15); color: var(--red); }
|
.cs-repl-status.rejected { background: rgba(240,61,62,0.15); color: var(--red); }
|
||||||
.cs-repl-resolve-all { margin-top: 8px; }
|
.cs-repl-resolve-all { margin-top: 8px; }
|
||||||
|
|
||||||
|
/* ── Route Planner iframe ── */
|
||||||
|
#pageRoute {
|
||||||
|
position: absolute;
|
||||||
|
top: 0; left: 0; right: 0; bottom: 0;
|
||||||
|
margin-top: 0; padding: 0;
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
}
|
||||||
|
#pageRoute.hidden { display: none !important; }
|
||||||
|
#routeFrame {
|
||||||
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
border: none;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||||
<script src="https://cdn.jsdelivr.net/npm/exifr@7/dist/lite.umd.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/exifr@7/dist/lite.umd.js"></script>
|
||||||
@@ -731,7 +747,6 @@
|
|||||||
<input id="loginUser" class="input-field" type="text" placeholder="Username" autocomplete="username">
|
<input id="loginUser" class="input-field" type="text" placeholder="Username" autocomplete="username">
|
||||||
<input id="loginPass" class="input-field" type="password" placeholder="Password" autocomplete="current-password">
|
<input id="loginPass" class="input-field" type="password" placeholder="Password" autocomplete="current-password">
|
||||||
<button id="loginBtn" class="btn btn-primary btn-block" onclick="doLogin()">Sign In</button>
|
<button id="loginBtn" class="btn btn-primary btn-block" onclick="doLogin()">Sign In</button>
|
||||||
<div style="text-align:center;font-size:12px;color:var(--text3);margin-top:10px;">Default: admin / changeme</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -779,6 +794,9 @@
|
|||||||
<button class="nav-item" data-page="exifscanner" onclick="switchPage('exifscanner')">
|
<button class="nav-item" data-page="exifscanner" onclick="switchPage('exifscanner')">
|
||||||
<span class="ni-icon">📸</span><span>EXIF Scanner</span>
|
<span class="ni-icon">📸</span><span>EXIF Scanner</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="nav-item" data-page="route" onclick="switchPage('route')">
|
||||||
|
<span class="ni-icon">🗺️</span><span>Route</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-footer">
|
<div class="sidebar-footer">
|
||||||
<button id="logoutBtn" class="logout-btn" onclick="doLogout()">🚪 Sign out</button>
|
<button id="logoutBtn" class="logout-btn" onclick="doLogout()">🚪 Sign out</button>
|
||||||
@@ -1116,6 +1134,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="exifPrevResults"></div>
|
<div id="exifPrevResults"></div>
|
||||||
</div>
|
</div>
|
||||||
|
</div> <!-- /pageExifscanner -->
|
||||||
|
|
||||||
|
<!-- ── ROUTE PLANNER (iframe) ── -->
|
||||||
|
<div id="pageRoute" class="page hidden">
|
||||||
|
<iframe id="routeFrame" src="https://route.ourpad.casa" frameborder="0"></iframe>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -1201,7 +1224,7 @@ function showToast(msg, isError = false) {
|
|||||||
t.textContent = msg;
|
t.textContent = msg;
|
||||||
t.className = 'toast' + (isError ? ' error' : '');
|
t.className = 'toast' + (isError ? ' error' : '');
|
||||||
t.classList.add('show');
|
t.classList.add('show');
|
||||||
setTimeout(() => t.classList.remove('show'), 3000);
|
setTimeout(() => t.classList.remove('show'), 4000);
|
||||||
}
|
}
|
||||||
|
|
||||||
let modalResolve = null;
|
let modalResolve = null;
|
||||||
@@ -1245,6 +1268,7 @@ function switchPage(page) {
|
|||||||
else if (page === 'activity') loadActivity();
|
else if (page === 'activity') loadActivity();
|
||||||
else if (page === 'cantaloupe') loadCantaloupeSync();
|
else if (page === 'cantaloupe') loadCantaloupeSync();
|
||||||
else if (page === 'exifscanner') loadExifScanner();
|
else if (page === 'exifscanner') loadExifScanner();
|
||||||
|
else if (page === 'route') initRoutePage();
|
||||||
}
|
}
|
||||||
function closeDetail() {
|
function closeDetail() {
|
||||||
if (AppState._prevPage) switchPage(AppState._prevPage);
|
if (AppState._prevPage) switchPage(AppState._prevPage);
|
||||||
@@ -1257,10 +1281,13 @@ function closeDetail() {
|
|||||||
async function doLogin() {
|
async function doLogin() {
|
||||||
const username = document.getElementById('loginUser').value.trim();
|
const username = document.getElementById('loginUser').value.trim();
|
||||||
const password = document.getElementById('loginPass').value;
|
const password = document.getElementById('loginPass').value;
|
||||||
|
const loginBtn = document.getElementById('loginBtn');
|
||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
showLoginError('Please enter username and password');
|
showLoginError('Please enter username and password');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
loginBtn.disabled = true;
|
||||||
|
loginBtn.textContent = 'Signing in...';
|
||||||
try {
|
try {
|
||||||
const data = await api('/api/auth/login', {
|
const data = await api('/api/auth/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -1276,6 +1303,9 @@ async function doLogin() {
|
|||||||
switchPage('dashboard');
|
switchPage('dashboard');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showLoginError(e.message);
|
showLoginError(e.message);
|
||||||
|
} finally {
|
||||||
|
loginBtn.disabled = false;
|
||||||
|
loginBtn.textContent = 'Sign In';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1400,10 +1430,23 @@ function renderDashActivity(items) {
|
|||||||
}
|
}
|
||||||
el.innerHTML = items.map(a => {
|
el.innerHTML = items.map(a => {
|
||||||
const icon = getActIcon(a.action);
|
const icon = getActIcon(a.action);
|
||||||
|
let details = a.details || a.action || '';
|
||||||
|
// Try to pretty-print JSON details
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(details);
|
||||||
|
if (typeof parsed === 'object' && parsed !== null) {
|
||||||
|
const parts = [];
|
||||||
|
if (parsed.assets_updated !== undefined) parts.push(`${parsed.assets_updated} asset(s) updated`);
|
||||||
|
if (parsed.fields_applied !== undefined) parts.push(`${parsed.fields_applied} field(s) applied`);
|
||||||
|
if (parsed.assets_created !== undefined) parts.push(`${parsed.assets_created} created`);
|
||||||
|
if (parts.length > 0) details = parts.join(', ');
|
||||||
|
else details = JSON.stringify(parsed).replace(/[{}"]/g, '').replace(/,/g, ', ');
|
||||||
|
}
|
||||||
|
} catch(e) { /* not JSON, show as-is */ }
|
||||||
return `<div class="activity-item">
|
return `<div class="activity-item">
|
||||||
<div class="act-icon ${icon}">${getActEmoji(a.action)}</div>
|
<div class="act-icon ${icon}">${getActEmoji(a.action)}</div>
|
||||||
<div class="act-info">
|
<div class="act-info">
|
||||||
<div class="act-text">${esc(a.details || a.action || '')} <span class="act-user">${esc(a.user_name || '')}</span></div>
|
<div class="act-text">${esc(details)} <span class="act-user">${esc(a.user_name || '')}</span></div>
|
||||||
<div class="act-time">${timeAgo(a.created_at)}</div>
|
<div class="act-time">${timeAgo(a.created_at)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -4022,6 +4065,19 @@ function openExifLightbox(src) {
|
|||||||
function closeExifLightbox() {
|
function closeExifLightbox() {
|
||||||
if (exifLightboxEl) exifLightboxEl.style.display = 'none';
|
if (exifLightboxEl) exifLightboxEl.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
// ROUTE PLANNER
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
function initRoutePage() {
|
||||||
|
const iframe = document.getElementById('routeFrame');
|
||||||
|
if (!iframe) return;
|
||||||
|
// Force-reload on each visit so the route data is always fresh
|
||||||
|
if (iframe.dataset.loaded) {
|
||||||
|
iframe.src = iframe.src;
|
||||||
|
}
|
||||||
|
iframe.dataset.loaded = '1';
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user