UX fixes: fix Map tab crash, remove Edit/Delete buttons, update Help dialog and menu labels

This commit is contained in:
2026-05-30 20:56:28 -04:00
parent 9ae0f271ea
commit 474a618f79
6 changed files with 87 additions and 173 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env bash
# 🔄 Rotating Backup — keep N copies, replace oldest
#
# Usage:
# ./rotate-backup.sh [--keep N] [--dir DIR] <path/to/database.db>
#
# Examples:
# ./rotate-backup.sh /home/oplabs/projects/canteen-asset-tracker/assets.db
# ./rotate-backup.sh --keep 5 --dir /backups/dbs /var/lib/app/data.db
#
# Default: keep 3 backups in /home/oplabs/backups/canteen-db/
set -euo pipefail
KEEP=3
BACKUP_DIR="/home/oplabs/backups/canteen-db"
PREFIX="assets.db"
# ── Parse args ────────────────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
case "$1" in
--keep) shift; KEEP="$1"; shift ;;
--dir) shift; BACKUP_DIR="$1"; shift ;;
--prefix) shift; PREFIX="$1"; shift ;;
-h|--help)
echo "Usage: $(basename "$0") [--keep N] [--dir DIR] [--prefix NAME] <database-path>"
echo ""
echo "Backs up a SQLite database with rotation (keep N, remove oldest)."
echo ""
echo " --keep N Number of backups to retain (default: 3)"
echo " --dir DIR Target backup directory (default: /home/oplabs/backups/canteen-db)"
echo " --prefix NAME Backup file name prefix (default: assets.db)"
exit 0
;;
-*)
echo "Unknown option: $1" >&2
exit 1
;;
*)
DB_PATH="$1"
shift
;;
esac
done
if [[ -z "${DB_PATH:-}" ]]; then
echo "❌ Usage: $(basename "$0") [--keep N] <database-path>" >&2
exit 1
fi
if [[ ! -f "$DB_PATH" ]]; then
echo "❌ Database not found: $DB_PATH" >&2
exit 1
fi
# ── Create backup ─────────────────────────────────────────────────────────────
mkdir -p "$BACKUP_DIR"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/${PREFIX}-${TIMESTAMP}"
# Use sqlite3 .backup for safe online backup (consistent snapshot even if app is running)
if command -v sqlite3 &>/dev/null; then
sqlite3 "$DB_PATH" ".backup '$BACKUP_FILE'"
echo "$(basename "$DB_PATH")$BACKUP_FILE (sqlite3 snapshot)"
else
cp "$DB_PATH" "$BACKUP_FILE"
echo "$(basename "$DB_PATH")$BACKUP_FILE (file copy)"
fi
# ── Rotate: keep only N most recent ───────────────────────────────────────────
KEEP=$((KEEP + 0)) # ensure numeric
REMOVE_COUNT=$((KEEP))
REMOVE_FROM=$((REMOVE_COUNT + 1))
OLD_COUNT=$(ls -t "${BACKUP_DIR}/${PREFIX}-"* 2>/dev/null | wc -l)
ls -t "${BACKUP_DIR}/${PREFIX}-"* 2>/dev/null | tail -n +${REMOVE_FROM} | xargs rm -f 2>/dev/null || true
REMOVED=$((OLD_COUNT > KEEP ? OLD_COUNT - KEEP : 0))
echo " 📦 ${KEEP} kept · ${REMOVED} removed · ${BACKUP_DIR}"
+2 -2
View File
@@ -42,8 +42,8 @@ if [ -f "$DB_PATH" ]; then
mkdir -p "$BACKUP_DIR"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
cp "$DB_PATH" "${BACKUP_DIR}/assets.db-${TIMESTAMP}"
# Keep last 30 backups
ls -t "${BACKUP_DIR}/assets.db-"* 2>/dev/null | tail -n +31 | xargs rm -f 2>/dev/null || true
# Keep last 3 backups (rotate: oldest gets replaced)
ls -t "${BACKUP_DIR}/assets.db-"* 2>/dev/null | tail -n +4 | xargs rm -f 2>/dev/null || true
echo " Backup: ${BACKUP_DIR}/assets.db-${TIMESTAMP}"
fi
+7 -171
View File
@@ -1061,7 +1061,7 @@
</div>
<nav class="drawer-nav">
<button class="dn-item active" data-tab="tabAddAsset" onclick="navFromDrawer('tabAddAsset')">
<span class="dn-icon">📷</span> Add / Find Asset
<span class="dn-icon">📷</span> Find Asset
</button>
<button class="dn-item" data-tab="tabAssets" onclick="navFromDrawer('tabAssets')">
<span class="dn-icon">📦</span> Assets
@@ -1330,40 +1330,7 @@
<button class="btn btn-outline btn-sm" id="detailNavigateBtn" onclick="navigateToAsset()" style="flex:1;min-width:100px;display:none;">🧭 Navigate</button>
</div>
<div style="display:flex;gap:8px;margin-bottom:10px;">
<button class="btn btn-outline btn-sm" onclick="editAsset()" style="flex:1;">✏️ Edit</button>
<button class="btn btn-danger btn-sm" onclick="deleteAsset()" style="flex:1;">🗑️ Delete</button>
</div>
</div>
<!-- ── Edit View ────────────────────────────────────────────────────── -->
<div id="assetsEditView" style="display:none;">
<div class="detail-header">
<button class="back-btn" onclick="showAssetList()" title="Go back to asset list"></button>
<div style="font-size:18px;font-weight:700;">Edit Asset</div>
</div>
<div class="card" id="editFormCard">
<input id="editBarcode" type="text" class="input-field" placeholder="Machine ID *">
<input id="editName" type="text" class="input-field" placeholder="Asset name *">
<textarea id="editDesc" class="input-field" placeholder="Telemetry ID" rows="2"></textarea>
<div class="form-row" style="margin-bottom:8px;">
<select id="editCategory" class="input-field">
<option value="">Category</option>
<option>Furniture</option>
<option>Appliances</option>
<option>Utensils &amp; Serveware</option>
<option>Equipment</option>
<option>Other</option>
</select>
<select id="editStatus" class="input-field">
<option value="active">Active</option>
<option value="maintenance">Maintenance</option>
<option value="retired">Retired</option>
</select>
</div>
<select id="editAssignedTo" class="input-field">
<option value="">Assigned To (none)</option>
</select>
<button class="btn btn-primary" onclick="submitEditAsset()">Save Changes</button>
<!-- Edit/Delete removed — main app is lookup-only; use admin server -->
</div>
</div>
@@ -1685,18 +1652,12 @@
</ul>
<p style="margin-bottom:10px;"><strong>Navigation</strong></p>
<ul style="margin:0 0 12px 20px;padding:0;list-style:disc;">
<li>Bottom tabs switch between Assets, Add, Route, and Map views</li>
<li>Bottom tabs switch between Find Asset, Assets, Map, Nav, and Route views</li>
<li>Use the ☰ menu to access settings, profile, and admin panel</li>
</ul>
<p style="margin-bottom:10px;"><strong>Offline Mode</strong></p>
<p style="margin-bottom:10px;"><strong>Scanning</strong></p>
<ul style="margin:0 0 12px 20px;padding:0;list-style:disc;">
<li>When disconnected, changes are queued and sync automatically</li>
<li>Click the ⬇️ badge to process the queue immediately</li>
<li>Barcode scanning and manual entry work offline</li>
</ul>
<p><strong>Scanning</strong></p>
<ul style="margin:0 0 0 20px;padding:0;list-style:disc;">
<li>Use the Add tab to scan barcodes or read ConnectID stickers via camera</li>
<li>Use the Find Asset tab to scan barcodes or read ConnectID stickers via camera</li>
<li>Stickers in <code>XXXXX-XXXXXX</code> format are read automatically via OCR</li>
</ul>
</div>
@@ -3821,7 +3782,7 @@
// ── View switching ────────────────────────────────────────────────────
function hideAllAssetViews() {
['assetsListView','assetsDetailView','assetsEditView'].forEach(id => {
['assetsListView','assetsDetailView'].forEach(id => {
const el = document.getElementById(id);
if (el) el.style.display = 'none';
});
@@ -3838,11 +3799,6 @@
document.getElementById('assetsDetailView').style.display = 'block';
}
function showEditView() {
hideAllAssetViews();
document.getElementById('assetsEditView').style.display = 'block';
}
// ── Detail view ───────────────────────────────────────────────────────
function nvl(v, fallback) { return (v !== null && v !== undefined && v !== '') ? v : fallback; }
@@ -4500,131 +4456,11 @@
}
}
// ── Edit ──────────────────────────────────────────────────────────────
async function editAsset() {
if (!AppState.currentAssetId) return;
try {
const a = await api('/api/assets/' + AppState.currentAssetId);
showEditView();
document.getElementById('editFormCard').innerHTML = renderEditForm(a);
// Assigned To user dropdown (users managed via admin app)
setTimeout(function() {
var sel = document.getElementById('editAssignedTo');
if (sel && a.assigned_to) sel.value = a.assigned_to;
}, 0);
} catch (e) {
showToast(e.message, true);
}
}
function renderEditForm(a) {
return `
<div class="card-title">Edit Asset #${a.id}</div>
<input id="editMachineId" type="text" class="input-field" placeholder="Machine ID *" value="${esc(a.machine_id)}">
<input id="editName" type="text" class="input-field" placeholder="Asset name *" value="${esc(a.name)}">
<input id="editSerialNumber" type="text" class="input-field" placeholder="Serial number" value="${esc(a.serial_number || '')}">
<textarea id="editDesc" class="input-field" placeholder="Telemetry ID" rows="2">${esc(a.description || '')}</textarea>
<div class="form-row">
<input id="editCategory" type="text" class="input-field" placeholder="Category" value="${esc(a.category || '')}" list="catList">
<input id="editMake" type="text" class="input-field" placeholder="Make" value="${esc(a.make || '')}">
</div>
<div class="form-row">
<input id="editModel" type="text" class="input-field" placeholder="Model" value="${esc(a.model || '')}">
<select id="editStatus" class="input-field">
<option value="active" ${a.status === 'active' ? 'selected' : ''}>Active</option>
<option value="maintenance" ${a.status === 'maintenance' ? 'selected' : ''}>Maintenance</option>
</select>
</div>
<select id="editAssignedTo" class="input-field">
<option value="">Assigned To (none)</option>
</select>
<div class="card-title" style="margin-top:8px;">📍 Directions</div>
<input id="editAddress" type="text" class="input-field" placeholder="Address" value="${esc(a.address || '')}">
<div class="form-row">
<input id="editBuildingName" type="text" class="input-field" placeholder="Building name" value="${esc(a.building_name || '')}">
<input id="editBuildingNumber" type="text" class="input-field" placeholder="Building #" value="${esc(a.building_number || '')}">
</div>
<div class="form-row">
<input id="editFloor" type="text" class="input-field" placeholder="Floor" value="${esc(a.floor || '')}">
<input id="editRoom" type="text" class="input-field" placeholder="Room" value="${esc(a.room || '')}">
</div>
<input id="editWalkingDirections" type="text" class="input-field" placeholder="Walking directions" value="${esc(a.walking_directions || '')}">
<input id="editMapLink" type="text" class="input-field" placeholder="Map link (URL)" value="${esc(a.map_link || '')}">
<div class="form-row">
<input id="editParking" type="text" class="input-field" placeholder="Parking location" value="${esc(a.parking_location || '')}">
<button class="btn btn-outline btn-sm" onclick="fillGpsParking('editParking')" style="flex-shrink:0;">📍 GPS</button>
</div>
<datalist id="catList">
<option value="Coffee"><option value="Cold Beverage"><option value="Snacks">
<option value="Cold Food"><option value="Market"><option value="Smart Cooler">
</datalist>
<button class="btn btn-primary" onclick="submitEditAsset()">Save Changes</button>
`;
}
async function submitEditAsset() {
if (!AppState.currentAssetId) return;
const machine_id = document.getElementById('editMachineId').value.trim();
const name = document.getElementById('editName').value.trim();
if (!machine_id || !name) {
showToast('Machine ID and name are required', true);
return;
}
const payload = {
machine_id, name,
serial_number: document.getElementById('editSerialNumber').value.trim() || null,
description: document.getElementById('editDesc').value.trim() || null,
category: document.getElementById('editCategory').value.trim() || null,
make: document.getElementById('editMake').value.trim() || null,
model: document.getElementById('editModel').value.trim() || null,
status: document.getElementById('editStatus').value,
address: document.getElementById('editAddress').value.trim() || null,
building_name: document.getElementById('editBuildingName').value.trim() || null,
building_number: document.getElementById('editBuildingNumber').value.trim() || null,
floor: document.getElementById('editFloor').value.trim() || null,
room: document.getElementById('editRoom').value.trim() || null,
walking_directions: document.getElementById('editWalkingDirections').value.trim() || null,
map_link: document.getElementById('editMapLink').value.trim() || null,
parking_location: document.getElementById('editParking').value.trim() || null,
assigned_to: document.getElementById('editAssignedTo').value ? parseInt(document.getElementById('editAssignedTo').value) : null,
};
try {
await api('/api/assets/' + AppState.currentAssetId, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
showToast('Asset updated!');
viewAsset(AppState.currentAssetId);
} catch (e) {
showToast(e.message, true);
}
}
// ── Delete ────────────────────────────────────────────────────────────
async function deleteAsset() {
if (!AppState.currentAssetId) return;
const confirmed = await showModal(
'Delete Asset',
'Delete this asset and all its check-ins? This cannot be undone.',
'Delete'
);
if (!confirmed) return;
try {
await api('/api/assets/' + AppState.currentAssetId, { method: 'DELETE' });
showToast('Asset deleted');
showAssetList();
} catch (e) {
showToast(e.message, true);
}
}
// ── Map filter state ──────────────────────────────────────────────────
let mapFilters = { category: null, status: null, make: null, disney_park: null, disney_filter: null, q: '',
customer_id: null, location_id: null, assigned_to: null, branch: null };
let mapFilterDropdownsLoaded = false;
let _mapFilterTimer;
function toggleMapFilterPanel() {
const panel = document.getElementById('mapFilterPanel');