feat: revised category icons + unknown type detection

- Cleaned CATEGORY_MAP: removed dead entries (Furniture, GF Bev,
  Snack/bev, Snack/food), fixed duplicate 'Food' key, proper
  distinct icons for all 6 real categories
- Added categoryInfo() helper: 🥤/🍿/🍔/🔧/🔌/📦 for known cats,
   (red) for unmapped categories, 📦 for null
- Updated all render sites (list, map pins, map list, detail view,
  scan result, OCR result) to use categoryInfo()
- Added category icons to detail view and scan/OCR result headers
- Synced categories DB table: added Equipment/Appliances with icons,
  removed unused Unknown
- Updated seed data in both server.py and admin_server.py to match
  real asset categories
- Added category_health to /api/stats with unmapped category detection
- Added Category Health card to admin dashboard (shown only when
  unmapped categories exist)
- Updated CSS backgrounds for all real categories + .cat-unmapped
This commit is contained in:
2026-05-31 00:28:03 -04:00
parent 5c2e15e28b
commit f1a3bb9f00
2 changed files with 32 additions and 26 deletions
+3 -2
View File
@@ -342,8 +342,9 @@ def _seed_if_empty(conn: sqlite3.Connection, table: str, columns: tuple, rows: l
def _seed_data(conn: sqlite3.Connection):
"""Insert default seed data for lookup tables."""
_seed_if_empty(conn, "categories", ("name", "icon"), [
("Furniture", "🪑"), ("Appliances", "🔌"),
("Utensils & Serveware", "🍽️"), ("Equipment", "⚙️"), ("Other", "📦"),
("Bev", "🥤"), ("Snack", "🍿"),
("Food", "🍔"), ("Equipment", "🔧"),
("Appliances", "🔌"), ("Other", "📦"),
])
_seed_if_empty(conn, "key_names", ("name",), [
("MK500",), ("Green Dot",), ("Red Key",), ("Blue Key",),
+29 -24
View File
@@ -423,11 +423,13 @@
width: 64px; padding: 0 4px;
}
.asset-item .ai-icon .cat-icon-img { width: 32px; height: 32px; object-fit: contain; }
.asset-item .ai-icon.cat-Furniture { background: #1a2e2a; }
.asset-item .ai-icon.cat-Bev { background: #0a3a65; }
.asset-item .ai-icon.cat-Snack { background: #3a2a10; }
.asset-item .ai-icon.cat-Food { background: #3a1a0a; }
.asset-item .ai-icon.cat-Equipment { background: #1a1a2e; }
.asset-item .ai-icon.cat-Appliances { background: #1a2a3e; }
.asset-item .ai-icon.cat-Utensils---Serveware { background: #2a2510; }
.asset-item .ai-icon.cat-Equipment { background: #2a1a2e; }
.asset-item .ai-icon.cat-Other { background: #1a1b26; }
.asset-item .ai-icon.cat-unmapped { background: #2a0a0a; }
.asset-item .ai-info { flex: 1; min-width: 0; }
.asset-item .ai-name { font-weight: 600; font-size: 15px; line-height: 1.35; white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.asset-item .ai-company { font-weight: 600; font-size: 14px; line-height: 1.3; }
@@ -2557,7 +2559,7 @@
const el = document.getElementById('scanResult');
el.style.display = 'block';
el.innerHTML = `
<div class="sr-name">${esc(asset.name)}</div>
<div class="sr-name"><span style="margin-right:6px">${categoryInfo(asset.category).icon}</span>${esc(asset.name)}</div>
<div class="sr-meta">
${esc(asset.machine_id)} · ${esc(catLabel(asset.category))} ·
@@ -2853,9 +2855,10 @@
const asset = await api('/api/assets/search?machine_id=' + encodeURIComponent(machineId));
// Asset found — update OCR result with asset info
const el = document.getElementById('ocrResult');
const oci = categoryInfo(asset.category);
el.innerHTML = `
<div class="or-id">${esc(asset.machine_id)} ✅</div>
<div class="sr-name" style="font-size:16px;font-weight:700;margin:4px 0;">${esc(asset.name)}</div>
<div class="sr-name" style="font-size:16px;font-weight:700;margin:4px 0;"><span style="margin-right:6px">${oci.icon}</span>${esc(asset.name)}</div>
<div class="or-meta">${esc(catLabel(asset.category))} · ${esc(asset.status)}</div>`;
setOcrStatus('Asset found!', 'success');
@@ -3141,17 +3144,21 @@
const CATEGORY_MAP = {
'Bev': { icon: '🥤', color: '#0a7ab5' },
'Snack': { icon: '🍿', color: '#c97d0e' },
'Food': { icon: '🍔', color: '#b15a0a' },
'Other': { icon: '📦', color: '#6b7280' },
'Snack/bev': { icon: '🥤🍿', color: '#9b59b6', label: 'Snack & Bev', wide: true },
'Snack/food': { icon: '🍔🍿', color: '#9b59b6', label: 'Snack & Food', wide: true },
'Food': { icon: '🍔', color: '#d35400' },
'Food': { icon: '🍔', color: '#e67e22' },
'Equipment': { icon: '🔧', color: '#2c3e50' },
'Furniture': { icon: '🪑', color: '#8e44ad' },
'Appliances': { icon: '🔌', color: '#3b82f6' },
'Unknown': { icon: '', color: '#6b7280' },
'GF Bev': { icon: '🥤', color: '#0a7ab5' },
'Other': { icon: '📦', color: '#6b7280' },
};
const CATEGORY_UNMAPPED_ICON = '❓';
const CATEGORY_UNMAPPED_COLOR = '#b91c1c';
const CATEGORY_FALLBACK_ICON = '📦'; // null/missing category
function categoryInfo(cat) {
if (!cat) return { icon: CATEGORY_FALLBACK_ICON, color: '#6b7280', mapped: false };
const e = CATEGORY_MAP[cat];
if (e) return { icon: e.icon, color: e.color, mapped: true, wide: e.wide };
return { icon: CATEGORY_UNMAPPED_ICON, color: CATEGORY_UNMAPPED_COLOR, mapped: false };
}
const PARK_ICONS = {'magic-kingdom':'🏰','epcot':'🌍','hollywood-studios':'🎬','animal-kingdom':'🌿','disney-springs':'🛍️','resort':'🏨','office':'🏢','other':'📍'};
const PARK_NAMES = {'magic-kingdom':'Magic Kingdom','epcot':'Epcot','hollywood-studios':'Hollywood Studios','animal-kingdom':'Animal Kingdom','disney-springs':'Disney Springs','resort':'Resort','office':'Office','other':'Other'};
@@ -3694,13 +3701,10 @@
return;
}
el.innerHTML = assets.filter(a => a.status !== 'retired').map(a => {
const catEntry = CATEGORY_MAP[a.category];
const icon = catEntry ? catEntry.icon : (a.category ? a.category.charAt(0).toUpperCase() : '📦');
const catColor = catEntry ? catEntry.color : '#1a1b26';
const iconWide = catEntry && catEntry.wide ? ' ai-icon-wide' : '';
const ci = categoryInfo(a.category);
return `
<div class="asset-item" onclick="viewAsset(${a.id})">
<div class="ai-icon${iconWide}" style="background:${catColor}22;color:${catColor}">${icon}</div>
<div class="ai-icon${ci.wide ? ' ai-icon-wide' : ''}" style="background:${ci.color}22;color:${ci.color}">${ci.icon}</div>
<div class="ai-info">
${renderAssetCardContent(a)}
<div class="ai-meta">
@@ -3813,7 +3817,8 @@
try {
const a = await api('/api/assets/' + id);
document.getElementById('detailName').textContent = a.name;
const ci = categoryInfo(a.category);
document.getElementById('detailName').innerHTML = `<span style="margin-right:8px">${ci.icon}</span>${esc(a.name)}`;
document.getElementById('detailMeta').innerHTML =
`${esc(a.machine_id)}${a.disney_park ? ' · <span class="disney-tag">' + parkIcon(a.disney_park) + ' ' + parkName(a.disney_park) + '</span>' : ''}`;
@@ -4712,13 +4717,13 @@
// Render pins for assets with GPS
matched.forEach(function(a) {
if (a.latitude != null && a.longitude != null) {
var catEntry = CATEGORY_MAP[a.category];
var icon = catEntry ? catEntry.icon : (a.category ? a.category.charAt(0).toUpperCase() : '📦');
var ci = categoryInfo(a.category);
var icon = ci.icon;
if (icon.indexOf('<img') !== -1) icon = '🍽';
var popupHtml = '<b>' + esc(a.name) + '</b><br>' + esc(a.machine_id || '') +
'<br><button class="btn btn-outline btn-sm" style="margin-top:6px;" ' +
'onclick="switchTab(\'tabAssets\');viewAsset(' + a.id + ');">View Details</button>';
var catColor = catEntry ? catEntry.color : '#6b7280';
var catColor = ci.color;
var pinIcon = L.divIcon({
className: 'cat-pin-icon',
html: '<div style="width:28px;height:28px;border-radius:50%;background:' + catColor +
@@ -4754,8 +4759,8 @@
var sorted = assets.slice().sort(function(a, b) { return (a.name || '').localeCompare(b.name || ''); });
var html = '';
sorted.forEach(function(a) {
var catEntry = CATEGORY_MAP[a.category];
var icon = catEntry ? catEntry.icon : '📦';
var ci = categoryInfo(a.category);
var icon = ci.icon;
if (icon.indexOf('<img') !== -1) icon = '🍽';
var hasGps = (a.latitude != null && a.longitude != null);
html += '<div class="map-asset-item" onclick="switchTab(\'tabAssets\');viewAsset(' + a.id + ');">' +