feat(devices): add console-only display name (console_name)
Lets staff set a friendly name for a device independent of whatever name the end user gave it in the app. console_name is never shown to app users and never synced from/to device_name; every display label across search, equipment/helpdesk name resolution, device search, and the Manage tab's issue linker now falls back through console_name -> device_name -> serial rather than device_name alone. Also includes an incidental one-line fix in devices/service.py: the nested-struct deep-merge in update_device() was missing the newly added device_health_settings key. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -159,9 +159,10 @@ def list_devices(
|
|||||||
if search:
|
if search:
|
||||||
search_lower = search.lower()
|
search_lower = search.lower()
|
||||||
name_match = search_lower in (device.device_name or "").lower()
|
name_match = search_lower in (device.device_name or "").lower()
|
||||||
|
console_name_match = search_lower in (device.console_name or "").lower()
|
||||||
location_match = search_lower in (device.device_location or "").lower()
|
location_match = search_lower in (device.device_location or "").lower()
|
||||||
sn_match = search_lower in (device.serial_number or "").lower()
|
sn_match = search_lower in (device.serial_number or "").lower()
|
||||||
if not (name_match or location_match or sn_match):
|
if not (name_match or console_name_match or location_match or sn_match):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
results.append(device)
|
results.append(device)
|
||||||
@@ -222,7 +223,7 @@ def update_device(device_doc_id: str, data: DeviceUpdate) -> DeviceInDB:
|
|||||||
# Deep-merge nested structs so unmentioned sub-fields are preserved
|
# Deep-merge nested structs so unmentioned sub-fields are preserved
|
||||||
existing = doc.to_dict()
|
existing = doc.to_dict()
|
||||||
nested_keys = (
|
nested_keys = (
|
||||||
"device_attributes", "device_subscription", "device_stats",
|
"device_attributes", "device_subscription", "device_stats", "device_health_settings",
|
||||||
)
|
)
|
||||||
for key in nested_keys:
|
for key in nested_keys:
|
||||||
if key in update_data and isinstance(existing.get(key), dict):
|
if key in update_data and isinstance(existing.get(key), dict):
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ def _resolve_names(db, device_id: str | None, user_id: str | None) -> tuple[str,
|
|||||||
if device_id and isinstance(device_id, str) and device_id.strip():
|
if device_id and isinstance(device_id, str) and device_id.strip():
|
||||||
device_doc = db.collection("devices").document(device_id.strip()).get()
|
device_doc = db.collection("devices").document(device_id.strip()).get()
|
||||||
if device_doc.exists:
|
if device_doc.exists:
|
||||||
device_name = device_doc.to_dict().get("device_name", "")
|
device_data = device_doc.to_dict()
|
||||||
|
device_name = device_data.get("console_name") or device_data.get("device_name", "")
|
||||||
|
|
||||||
if user_id and isinstance(user_id, str) and user_id.strip():
|
if user_id and isinstance(user_id, str) and user_id.strip():
|
||||||
user_doc = db.collection("users").document(user_id.strip()).get()
|
user_doc = db.collection("users").document(user_id.strip()).get()
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ def _resolve_device_name(db, device_id: str) -> str:
|
|||||||
try:
|
try:
|
||||||
doc = db.collection("devices").document(device_id.strip()).get()
|
doc = db.collection("devices").document(device_id.strip()).get()
|
||||||
if doc.exists:
|
if doc.exists:
|
||||||
return doc.to_dict().get("device_name", "")
|
data = doc.to_dict()
|
||||||
|
return data.get("console_name") or data.get("device_name", "")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[helpdesk] Error resolving device name: {e}")
|
print(f"[helpdesk] Error resolving device name: {e}")
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -26,8 +26,9 @@ def _search_devices(q: str) -> list[dict]:
|
|||||||
return []
|
return []
|
||||||
out = []
|
out = []
|
||||||
for d in results[:LIMIT]:
|
for d in results[:LIMIT]:
|
||||||
label = d.device_name or d.serial_number or d.device_id or d.id
|
display_name = d.console_name or d.device_name
|
||||||
sublabel = d.serial_number if d.device_name else None
|
label = display_name or d.serial_number or d.device_id or d.id
|
||||||
|
sublabel = d.serial_number if display_name else None
|
||||||
out.append({
|
out.append({
|
||||||
"type": "device",
|
"type": "device",
|
||||||
"id": d.id,
|
"id": d.id,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Modal from '@/components/ui/Modal'
|
|||||||
import Button from '@/components/ui/Button'
|
import Button from '@/components/ui/Button'
|
||||||
|
|
||||||
export default function DeleteDeviceModal({ device, onConfirm, onCancel, error }) {
|
export default function DeleteDeviceModal({ device, onConfirm, onCancel, error }) {
|
||||||
const name = device?.device_name || 'this device'
|
const name = device?.console_name || device?.device_name || 'this device'
|
||||||
const serial = device?.serial_number || device?.device_id
|
const serial = device?.serial_number || device?.device_id
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -632,9 +632,9 @@ export default function ManageTab({ device, canEdit, deviceUsers: propUsers, use
|
|||||||
defaultType="issue"
|
defaultType="issue"
|
||||||
prefilledLinks={issueModal.entry
|
prefilledLinks={issueModal.entry
|
||||||
? undefined
|
? undefined
|
||||||
: [{ entity_type: 'device', entity_id: id, display_name: device?.device_name || sn, locked: true }]
|
: [{ entity_type: 'device', entity_id: id, display_name: device?.console_name || device?.device_name || sn, locked: true }]
|
||||||
}
|
}
|
||||||
knownEntities={id ? { [id]: device?.device_name || sn } : undefined}
|
knownEntities={id ? { [id]: device?.console_name || device?.device_name || sn } : undefined}
|
||||||
onClose={() => setIssueModal({ open: false, entry: null })}
|
onClose={() => setIssueModal({ open: false, entry: null })}
|
||||||
onSaved={handleIssueSaved}
|
onSaved={handleIssueSaved}
|
||||||
onDelete={canEdit ? (entryId) => {
|
onDelete={canEdit ? (entryId) => {
|
||||||
|
|||||||
@@ -820,7 +820,7 @@ export default function UserDetail() {
|
|||||||
<option value="">Choose a device…</option>
|
<option value="">Choose a device…</option>
|
||||||
{availableDevices.map((d) => (
|
{availableDevices.map((d) => (
|
||||||
<option key={d.id} value={d.id}>
|
<option key={d.id} value={d.id}>
|
||||||
{d.device_name || 'Unnamed'} ({d.device_id || d.id})
|
{d.console_name || d.device_name || 'Unnamed'} ({d.device_id || d.id})
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
@@ -848,7 +848,7 @@ export default function UserDetail() {
|
|||||||
onClick={() => navigate(`/devices/${device.id}`)}
|
onClick={() => navigate(`/devices/${device.id}`)}
|
||||||
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 0, fontSize: 'var(--font-size-sm)', fontWeight: 'var(--font-weight-semibold)', color: 'var(--color-text-accent)', textAlign: 'left', display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '160px' }}
|
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 0, fontSize: 'var(--font-size-sm)', fontWeight: 'var(--font-weight-semibold)', color: 'var(--color-text-accent)', textAlign: 'left', display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '160px' }}
|
||||||
>
|
>
|
||||||
{device.device_name || 'Unnamed Device'}
|
{device.console_name || device.device_name || 'Unnamed Device'}
|
||||||
</button>
|
</button>
|
||||||
<p style={{ fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', marginTop: '2px' }}>
|
<p style={{ fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', marginTop: '2px' }}>
|
||||||
{device.device_id || device.id}
|
{device.device_id || device.id}
|
||||||
|
|||||||
@@ -405,7 +405,7 @@ function LinkedDevicesModal({ userId, userName, open, onClose }) {
|
|||||||
<Icon name="device" size={14} color="var(--color-primary)" />
|
<Icon name="device" size={14} color="var(--color-primary)" />
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<div style={{ fontWeight: 'var(--font-weight-medium)', color: 'var(--color-text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
<div style={{ fontWeight: 'var(--font-weight-medium)', color: 'var(--color-text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||||
{d.device_name || d.device_id || d.id}
|
{d.console_name || d.device_name || d.device_id || d.id}
|
||||||
</div>
|
</div>
|
||||||
{d.device_id && (
|
{d.device_id && (
|
||||||
<div style={{ color: 'var(--color-text-muted)', fontSize: 'var(--font-size-xs)', fontFamily: 'var(--font-family-mono)' }}>
|
<div style={{ color: 'var(--color-text-muted)', fontSize: 'var(--font-size-xs)', fontFamily: 'var(--font-family-mono)' }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user