// frontend/src/pages/bellcloud/devices/tabs/GeneralTab.jsx // General Information tab — 3-row bento layout import { useState, useEffect, useMemo, useCallback, useRef } from 'react' import api from '@/lib/api' import { useToast } from '@/components/ui/Toast' import Button from '@/components/ui/Button' import StatusBadge from '@/components/ui/StatusBadge' import Icon from '@/components/ui/Icon' import FormField from '@/components/ui/FormField' import Spinner from '@/components/ui/Spinner' import { fmtDateTimeMedium } from '@/lib/formatters' // ─── Constants ──────────────────────────────────────────────────────────────── const LOG_LEVELS = [0, 1, 2, 3, 4, 5] const LOG_LEVEL_META = { 0: { label: 'Disabled', color: 'var(--color-text-muted)' }, 1: { label: 'Error', color: 'var(--color-danger)' }, 2: { label: 'Warning', color: 'var(--color-warning)' }, 3: { label: 'Info', color: 'var(--color-info)' }, 4: { label: 'Debug', color: '#9f7aea' }, 5: { label: 'Verbose', color: 'var(--color-success)' }, } // Glass bg tokens const GLASS_BG = 'rgba(28, 32, 38, 0.30)' const GLASS_BG_INNER = 'rgba(28, 32, 38, 0.40)' // ─── Helpers ────────────────────────────────────────────────────────────────── function parseCoordinates(coordStr) { if (!coordStr) return null if (typeof coordStr === 'object' && coordStr !== null) { const lat = parseFloat(coordStr.lat ?? coordStr.latitude) const lng = parseFloat(coordStr.lng ?? coordStr.longitude) if (!isNaN(lat) && !isNaN(lng)) return { lat, lng } return null } const numbers = coordStr.match(/-?\d+(?:\.\d+)?/g) if (numbers && numbers.length >= 2) { let lat = parseFloat(numbers[0]) let lng = parseFloat(numbers[1]) if (!isNaN(lat) && !isNaN(lng)) { if (/\bS\b/.test(coordStr)) lat = -Math.abs(lat) if (/\bW\b/.test(coordStr)) lng = -Math.abs(lng) return { lat, lng } } } return null } function formatCoordinates(coords) { if (!coords) return '—' const latDir = coords.lat >= 0 ? 'N' : 'S' const lngDir = coords.lng >= 0 ? 'E' : 'W' return `${Math.abs(coords.lat).toFixed(6)}° ${latDir}, ${Math.abs(coords.lng).toFixed(6)}° ${lngDir}` } function getNearestPlaceLabel(data) { const addr = data?.address || {} const name = addr.city || addr.town || addr.village || addr.hamlet || addr.municipality || data?.display_name?.split(',')[0] || '' const region = addr.state || addr.county || '' const country = addr.country || '' return [name, region, country].filter(Boolean).join(', ') } function getLogLevelMeta(levelRaw) { if (!Number.isFinite(levelRaw)) return null return LOG_LEVEL_META[Number(levelRaw)] || null } // ─── OSM Tile helpers ───────────────────────────────────────────────────────── function latLngToTile(lat, lng, zoom) { const n = Math.pow(2, zoom) const xFrac = ((lng + 180) / 360) * n const latRad = (lat * Math.PI) / 180 const yFrac = ((1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2) * n const x = Math.floor(xFrac) const y = Math.floor(yFrac) return { x, y, px: (xFrac - x) * 256, py: (yFrac - y) * 256 } } function buildTileGrid(lat, lng, zoom = 13, options = {}) { const { x, y, px, py } = latLngToTile(lat, lng, zoom) const COL_RADIUS = options.colRadius ?? 3 const ROW_RADIUS = options.rowRadius ?? 1 const tiles = [] for (let dy = -ROW_RADIUS; dy <= ROW_RADIUS; dy++) { for (let dx = -COL_RADIUS; dx <= COL_RADIUS; dx++) { tiles.push({ url: `https://tiles.stadiamaps.com/tiles/stamen_toner/${zoom}/${x + dx}/${y + dy}.png`, col: dx + COL_RADIUS, row: dy + ROW_RADIUS, }) } } return { tiles, markerOffset: { x: COL_RADIUS * 256 + px, y: ROW_RADIUS * 256 + py, }, } } function getResponsiveTileCounts(width, height) { const safeWidth = Number.isFinite(width) && width > 0 ? width : 0 const safeHeight = Number.isFinite(height) && height > 0 ? height : 0 let totalCols = Math.max(7, Math.ceil(safeWidth / 256) + 2) let totalRows = Math.max(3, Math.ceil(safeHeight / 256) + 2) if (totalCols % 2 === 0) totalCols += 1 if (totalRows % 2 === 0) totalRows += 1 return { totalCols, totalRows, colRadius: Math.floor(totalCols / 2), rowRadius: Math.floor(totalRows / 2), } } // ─── Keyframes ──────────────────────────────────────────────────────────────── let keyframesInjected = false function ensureKeyframes() { if (keyframesInjected) return keyframesInjected = true const style = document.createElement('style') style.textContent = ` @keyframes gen-tab-pulse { 0% { opacity: 0.9; transform: scale(1); } 70% { opacity: 0; transform: scale(2.4); } 100% { opacity: 0; transform: scale(2.4); } } .gen-log-slider { -webkit-appearance: none; appearance: none; width: 100%; height: 4px; border-radius: 2px; outline: none; cursor: pointer; background: rgba(28,32,38,0.70); } .gen-log-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 14px; height: 14px; border-radius: 50%; border: 2px solid rgba(192,193,255,0.25); cursor: pointer; transition: transform 0.15s, box-shadow 0.15s; } .gen-log-slider::-moz-range-thumb { width: 14px; height: 14px; border-radius: 50%; border: 2px solid rgba(192,193,255,0.25); cursor: pointer; } .gen-log-slider:hover::-webkit-slider-thumb { transform: scale(1.2); } ` document.head.appendChild(style) } // ─── Primitive design atoms ─────────────────────────────────────────────────── function SectionLabel({ children }) { return ( {children} ) } // Glass card — the universal panel wrapper function GlassCard({ title, titleAction, children, noPadding, style, fullHeight }) { return (
{title && (
{title} {titleAction &&
{titleAction}
}
)}
{children}
) } // Inner surface — glass nested surface function InnerSurface({ children, style }) { return (
{children}
) } function EditActionButton({ onClick, label = 'Edit' }) { return ( ) } function GoogleMapsIcon({ size = 14 }) { return ( ) } // ─── Safety Row ─────────────────────────────────────────────────────────────── function SafetyRow({ label, description, enabled }) { return (
{label} {description}
{enabled ? 'Armed' : 'Disarmed'}
) } // ─── Log Slider ─────────────────────────────────────────────────────────────── function LogSlider({ label, level, onChange, canEdit, color }) { const meta = getLogLevelMeta(level) const safeLevel = Number.isFinite(level) ? Number(level) : 0 // Build CSS gradient for the track — filled up to thumb position const pct = (safeLevel / (LOG_LEVELS.length - 1)) * 100 const trackBg = `linear-gradient(90deg, ${color} 0%, ${color} ${pct}%, rgba(28,32,38,0.70) ${pct}%, rgba(28,32,38,0.70) 100%)` return (
{label} {meta ? meta.label : '—'}
onChange(Number(e.target.value)) : undefined} style={{ background: trackBg, opacity: canEdit ? 1 : 0.6, // thumb color via CSS custom property on inline style — works in Safari '--thumb-color': color, }} /> {/* tick marks — padding-inline of 7px (half thumb width) aligns ticks with thumb centre */}
{LOG_LEVELS.map(l => (
))}
) } // ─── Location Map ───────────────────────────────────────────────────────────── function LocationMap({ coords, locationName, locationLabel, canEdit, onEdit }) { const ZOOM = 13 const PIN_X = 72 // % across const PIN_Y = 50 // % down const containerRef = useRef(null) const [mapSize, setMapSize] = useState({ width: 0, height: 0 }) useEffect(() => { const node = containerRef.current if (!node) return const updateSize = () => { const { width, height } = node.getBoundingClientRect() setMapSize(prev => ( prev.width === width && prev.height === height ? prev : { width, height } )) } updateSize() if (typeof ResizeObserver === 'undefined') return const observer = new ResizeObserver(updateSize) observer.observe(node) return () => observer.disconnect() }, []) const { totalCols, totalRows, colRadius, rowRadius } = useMemo( () => getResponsiveTileCounts(mapSize.width, mapSize.height), [mapSize.width, mapSize.height] ) const { tiles, markerOffset } = useMemo(() => { if (!coords) return { tiles: null, markerOffset: null } return buildTileGrid(coords.lat, coords.lng, ZOOM, { colRadius, rowRadius }) }, [coords, colRadius, rowRadius]) const googleMapsUrl = coords ? `https://www.google.com/maps?q=${coords.lat},${coords.lng}` : null const handleOpenGoogleMaps = useCallback(() => { if (!googleMapsUrl || typeof window === 'undefined') return window.open(googleMapsUrl, '_blank', 'noopener,noreferrer') }, [googleMapsUrl]) return (
{/* Tiles */} {tiles ? ( ) : (