The Struggle is Real. More UI Fixes
This commit is contained in:
@@ -65,9 +65,15 @@ function Subsection({ title, children, isFirst = false }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A single row of fields that wraps if it doesn't fit */
|
/** A grid row of fields — Nth items align across rows within a subsection */
|
||||||
function FieldRow({ children }) {
|
function FieldRow({ children, columns }) {
|
||||||
return <dl className="flex flex-wrap gap-x-8 gap-y-3 mb-3">{children}</dl>;
|
const childArray = Array.isArray(children) ? children.filter(Boolean) : [children];
|
||||||
|
const count = columns || childArray.length;
|
||||||
|
return (
|
||||||
|
<dl style={{ display: "grid", gridTemplateColumns: `repeat(${count}, 1fr)`, gap: "0.75rem 2rem", marginBottom: "0.75rem" }}>
|
||||||
|
{children}
|
||||||
|
</dl>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Log level styles ---
|
// --- Log level styles ---
|
||||||
@@ -452,9 +458,9 @@ export default function DeviceDetail() {
|
|||||||
|
|
||||||
const deviceInfoSection = (
|
const deviceInfoSection = (
|
||||||
<section className="rounded-lg border p-5" style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}>
|
<section className="rounded-lg border p-5" style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}>
|
||||||
<div className="device-info-row">
|
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gridTemplateRows: "auto auto", gap: "1rem", alignItems: "start" }}>
|
||||||
{/* Status */}
|
{/* Row 1, Col 1: Status */}
|
||||||
<div className="device-info-item">
|
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem" }}>
|
||||||
<div
|
<div
|
||||||
className="w-10 h-10 rounded-full flex items-center justify-center shrink-0"
|
className="w-10 h-10 rounded-full flex items-center justify-center shrink-0"
|
||||||
style={{ backgroundColor: isOnline ? "var(--success-bg)" : "var(--bg-card-hover)" }}
|
style={{ backgroundColor: isOnline ? "var(--success-bg)" : "var(--bg-card-hover)" }}
|
||||||
@@ -477,29 +483,28 @@ export default function DeviceDetail() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Serial + Hardware Variant */}
|
{/* Row 1, Col 2: Hardware Variant */}
|
||||||
<div className="device-info-item">
|
<div>
|
||||||
<div>
|
<div className="text-xs font-medium uppercase tracking-wide" style={{ color: "var(--text-muted)" }}>Hardware Variant</div>
|
||||||
<div className="text-xs font-medium uppercase tracking-wide" style={{ color: "var(--text-muted)" }}>Serial Number</div>
|
<div className="text-sm mt-0.5" style={{ color: "var(--text-primary)" }}>VesperCore</div>
|
||||||
<div className="text-sm font-mono mt-0.5" style={{ color: "var(--text-primary)" }}>{device.device_id}</div>
|
|
||||||
<div className="text-xs mt-0.5" style={{ color: "var(--text-muted)" }}>HW: VesperCore</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Admin Note */}
|
{/* Row 1-2, Col 3: Admin Notes (spans 2 rows) */}
|
||||||
<div className="device-info-item">
|
<div style={{ gridRow: "1 / 3" }}>
|
||||||
<div>
|
<div className="text-xs font-medium uppercase tracking-wide" style={{ color: "var(--text-muted)" }}>Admin Notes</div>
|
||||||
<div className="text-xs font-medium uppercase tracking-wide" style={{ color: "var(--text-muted)" }}>Admin Note</div>
|
<div className="text-sm mt-0.5" style={{ color: "var(--text-muted)" }}>-</div>
|
||||||
<div className="text-sm mt-0.5" style={{ color: "var(--text-muted)" }}>-</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Document ID */}
|
{/* Row 2, Col 1: Serial Number */}
|
||||||
<div className="device-info-item">
|
<div>
|
||||||
<div>
|
<div className="text-xs font-medium uppercase tracking-wide" style={{ color: "var(--text-muted)" }}>Serial Number</div>
|
||||||
<div className="text-xs font-medium uppercase tracking-wide" style={{ color: "var(--text-muted)" }}>Document ID</div>
|
<div className="text-sm font-mono mt-0.5" style={{ color: "var(--text-primary)" }}>{device.device_id}</div>
|
||||||
<div className="text-xs font-mono mt-0.5" style={{ color: "var(--text-muted)" }}>{device.id}</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Row 2, Col 2: Document ID */}
|
||||||
|
<div>
|
||||||
|
<div className="text-xs font-medium uppercase tracking-wide" style={{ color: "var(--text-muted)" }}>Document ID</div>
|
||||||
|
<div className="text-xs font-mono mt-0.5" style={{ color: "var(--text-muted)" }}>{device.id}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -534,10 +539,12 @@ export default function DeviceDetail() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const locationSection = (
|
const locationSection = (
|
||||||
<SectionCard title="Location">
|
<section className="rounded-lg border" style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}>
|
||||||
{coords ? (
|
{coords ? (
|
||||||
<div className="location-split">
|
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 0, height: "100%" }}>
|
||||||
<div className="location-fields">
|
{/* Left column: title + fields */}
|
||||||
|
<div className="p-6">
|
||||||
|
<h2 className="text-lg font-semibold mb-4" style={{ color: "var(--text-heading)" }}>Location</h2>
|
||||||
<dl className="space-y-4">
|
<dl className="space-y-4">
|
||||||
<Field label="Location">{device.device_location}</Field>
|
<Field label="Location">{device.device_location}</Field>
|
||||||
<Field label="Coordinates">
|
<Field label="Coordinates">
|
||||||
@@ -558,8 +565,9 @@ export default function DeviceDetail() {
|
|||||||
{locationName && <Field label="Nearest Place">{locationName}</Field>}
|
{locationName && <Field label="Nearest Place">{locationName}</Field>}
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
<div className="location-map">
|
{/* Right column: map with equal padding */}
|
||||||
<div className="rounded-md overflow-hidden border w-full" style={{ borderColor: "var(--border-primary)", height: 250 }}>
|
<div style={{ padding: "1rem", display: "flex" }}>
|
||||||
|
<div className="rounded-md overflow-hidden border" style={{ borderColor: "var(--border-primary)", flex: 1 }}>
|
||||||
<MapContainer center={[coords.lat, coords.lng]} zoom={13} style={{ height: "100%", width: "100%" }} scrollWheelZoom={false}>
|
<MapContainer center={[coords.lat, coords.lng]} zoom={13} style={{ height: "100%", width: "100%" }} scrollWheelZoom={false}>
|
||||||
<TileLayer
|
<TileLayer
|
||||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||||
@@ -571,12 +579,15 @@ export default function DeviceDetail() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<dl className="space-y-4">
|
<div className="p-6">
|
||||||
<Field label="Location">{device.device_location}</Field>
|
<h2 className="text-lg font-semibold mb-4" style={{ color: "var(--text-heading)" }}>Location</h2>
|
||||||
<Field label="Coordinates">{device.device_location_coordinates || "-"}</Field>
|
<dl className="space-y-4">
|
||||||
</dl>
|
<Field label="Location">{device.device_location}</Field>
|
||||||
|
<Field label="Coordinates">{device.device_location_coordinates || "-"}</Field>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</SectionCard>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
||||||
const deviceSettingsSection = (
|
const deviceSettingsSection = (
|
||||||
@@ -592,27 +603,29 @@ export default function DeviceDetail() {
|
|||||||
</FieldRow>
|
</FieldRow>
|
||||||
</Subsection>
|
</Subsection>
|
||||||
<Subsection title="Alert Settings">
|
<Subsection title="Alert Settings">
|
||||||
<FieldRow>
|
<FieldRow columns={3}>
|
||||||
<Field label="Alerts Status"><BoolBadge value={clock.ringAlertsMasterOn} yesLabel="ON" noLabel="OFF" /></Field>
|
<Field label="Alerts Status"><BoolBadge value={clock.ringAlertsMasterOn} yesLabel="ON" noLabel="OFF" /></Field>
|
||||||
<Field label="Alerts Type"><span className="capitalize">{clock.ringAlerts || "-"}</span></Field>
|
<Field label="Alerts Type"><span className="capitalize">{clock.ringAlerts || "-"}</span></Field>
|
||||||
<Field label="Ring Intervals">{clock.ringIntervals}</Field>
|
<Field label="Ring Intervals">{clock.ringIntervals}</Field>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<FieldRow>
|
<FieldRow columns={3}>
|
||||||
<Field label="Hour Bell">{clock.hourAlertsBell}</Field>
|
<Field label="Hour Bell">{clock.hourAlertsBell}</Field>
|
||||||
<Field label="Half-Hour Bell">{clock.halfhourAlertsBell}</Field>
|
<Field label="Half-Hour Bell">{clock.halfhourAlertsBell}</Field>
|
||||||
<Field label="Quarter Bell">{clock.quarterAlertsBell}</Field>
|
<Field label="Quarter Bell">{clock.quarterAlertsBell}</Field>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<FieldRow>
|
<FieldRow columns={3}>
|
||||||
<Field label="Daytime Silence"><BoolBadge value={clock.isDaySilenceOn} yesLabel="ON" noLabel="OFF" /></Field>
|
<Field label="Daytime Silence"><BoolBadge value={clock.isDaySilenceOn} yesLabel="ON" noLabel="OFF" /></Field>
|
||||||
<Field label="Day-Time Period">
|
<Field label="Day-Time Period">
|
||||||
{formatTimestamp(clock.daySilenceFrom)} - {formatTimestamp(clock.daySilenceTo)}
|
{formatTimestamp(clock.daySilenceFrom)} - {formatTimestamp(clock.daySilenceTo)}
|
||||||
</Field>
|
</Field>
|
||||||
|
<div />
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<FieldRow>
|
<FieldRow columns={3}>
|
||||||
<Field label="Nighttime Silence"><BoolBadge value={clock.isNightSilenceOn} yesLabel="ON" noLabel="OFF" /></Field>
|
<Field label="Nighttime Silence"><BoolBadge value={clock.isNightSilenceOn} yesLabel="ON" noLabel="OFF" /></Field>
|
||||||
<Field label="Nighttime Period">
|
<Field label="Nighttime Period">
|
||||||
{formatTimestamp(clock.nightSilenceFrom)} - {formatTimestamp(clock.nightSilenceTo)}
|
{formatTimestamp(clock.nightSilenceFrom)} - {formatTimestamp(clock.nightSilenceTo)}
|
||||||
</Field>
|
</Field>
|
||||||
|
<div />
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
</Subsection>
|
</Subsection>
|
||||||
<Subsection title="Backlight Settings">
|
<Subsection title="Backlight Settings">
|
||||||
@@ -635,27 +648,27 @@ export default function DeviceDetail() {
|
|||||||
{/* Right Column */}
|
{/* Right Column */}
|
||||||
<div>
|
<div>
|
||||||
<Subsection title="Network" isFirst>
|
<Subsection title="Network" isFirst>
|
||||||
<FieldRow>
|
<FieldRow columns={2}>
|
||||||
<Field label="Hostname">{net.hostname}</Field>
|
<Field label="Hostname">{net.hostname}</Field>
|
||||||
<Field label="Has Static IP"><BoolBadge value={net.useStaticIP} /></Field>
|
<Field label="Has Static IP"><BoolBadge value={net.useStaticIP} /></Field>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
</Subsection>
|
</Subsection>
|
||||||
<Subsection title="Clock Settings">
|
<Subsection title="Clock Settings">
|
||||||
<FieldRow>
|
<FieldRow columns={2}>
|
||||||
<Field label="Has Clock"><BoolBadge value={attr.hasClock} /></Field>
|
<Field label="Has Clock"><BoolBadge value={attr.hasClock} /></Field>
|
||||||
<Field label="Ring Intervals">{clock.ringIntervals}</Field>
|
<Field label="Ring Intervals">{clock.ringIntervals}</Field>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<FieldRow>
|
<FieldRow columns={2}>
|
||||||
<Field label="Odd Output">{clock.clockOutputs?.[0] ?? "-"}</Field>
|
<Field label="Odd Output">{clock.clockOutputs?.[0] ?? "-"}</Field>
|
||||||
<Field label="Even Output">{clock.clockOutputs?.[1] ?? "-"}</Field>
|
<Field label="Even Output">{clock.clockOutputs?.[1] ?? "-"}</Field>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<FieldRow>
|
<FieldRow columns={2}>
|
||||||
<Field label="Run Pulse">{clock.clockTimings?.[0] != null ? msToSeconds(clock.clockTimings[0]) : "-"}</Field>
|
<Field label="Run Pulse">{clock.clockTimings?.[0] != null ? msToSeconds(clock.clockTimings[0]) : "-"}</Field>
|
||||||
<Field label="Pause Pulse">{clock.clockTimings?.[1] != null ? msToSeconds(clock.clockTimings[1]) : "-"}</Field>
|
<Field label="Pause Pulse">{clock.clockTimings?.[1] != null ? msToSeconds(clock.clockTimings[1]) : "-"}</Field>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
</Subsection>
|
</Subsection>
|
||||||
<Subsection title="Bell Settings">
|
<Subsection title="Bell Settings">
|
||||||
<FieldRow>
|
<FieldRow columns={2}>
|
||||||
<Field label="Bells Active"><BoolBadge value={attr.hasBells} /></Field>
|
<Field label="Bells Active"><BoolBadge value={attr.hasBells} /></Field>
|
||||||
<Field label="Total">{attr.totalBells ?? "-"}</Field>
|
<Field label="Total">{attr.totalBells ?? "-"}</Field>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
@@ -844,26 +857,23 @@ export default function DeviceDetail() {
|
|||||||
const renderDoubleColumn = () => (
|
const renderDoubleColumn = () => (
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "1.5rem" }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: "1.5rem" }}>
|
||||||
{/* Row 1: Device Info + Subscription — equal height */}
|
{/* Row 1: Device Info + Subscription — equal height */}
|
||||||
<div className="device-equal-row">
|
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1.5rem", alignItems: "stretch" }}>
|
||||||
{deviceInfoSection}
|
{deviceInfoSection}
|
||||||
{subscriptionSection}
|
{subscriptionSection}
|
||||||
</div>
|
</div>
|
||||||
{/* Row 2: Device Settings — full width */}
|
{/* Row 2: Device Settings — full width */}
|
||||||
{deviceSettingsSection}
|
{deviceSettingsSection}
|
||||||
{/* Row 3: Location+Misc (left) vs Warranty (right) */}
|
{/* Row 3: Location + Warranty */}
|
||||||
<div className="device-columns">
|
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1.5rem", alignItems: "start" }}>
|
||||||
<div className="device-flex-fill" style={{ flex: 1, gap: "1.5rem" }}>
|
{locationSection}
|
||||||
<div className="flex-grow">{locationSection}</div>
|
{warrantySection}
|
||||||
{miscSection}
|
|
||||||
</div>
|
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
|
||||||
{warrantySection}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/* Row 4: Notes vs App Users */}
|
{/* Row 4: Misc full width */}
|
||||||
<div className="device-columns">
|
{miscSection}
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>{notesSection}</div>
|
{/* Row 5: Notes + App Users — equal width */}
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>{appUsersSection}</div>
|
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1.5rem", alignItems: "start" }}>
|
||||||
|
{notesSection}
|
||||||
|
{appUsersSection}
|
||||||
</div>
|
</div>
|
||||||
{/* Latest Logs */}
|
{/* Latest Logs */}
|
||||||
{logsSection}
|
{logsSection}
|
||||||
@@ -872,24 +882,22 @@ export default function DeviceDetail() {
|
|||||||
|
|
||||||
const renderTripleColumn = () => (
|
const renderTripleColumn = () => (
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: "1.5rem" }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: "1.5rem" }}>
|
||||||
{/* Row 1: DevInfo+Subscription (cols 1-2 equal height) + Location (col 3) */}
|
{/* Row 1: DevInfo + Subscription + Location — all equal height */}
|
||||||
<div className="device-columns">
|
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "1.5rem", alignItems: "stretch" }}>
|
||||||
<div className="device-equal-row" style={{ flex: 2 }}>
|
{deviceInfoSection}
|
||||||
{deviceInfoSection}
|
{subscriptionSection}
|
||||||
{subscriptionSection}
|
{locationSection}
|
||||||
</div>
|
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>{locationSection}</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/* Row 2: Device Settings (cols 1-2) + Warranty (col 3) */}
|
{/* Row 2: Device Settings (cols 1-2) + Warranty (col 3) */}
|
||||||
<div className="device-columns">
|
<div style={{ display: "grid", gridTemplateColumns: "2fr 1fr", gap: "1.5rem", alignItems: "stretch" }}>
|
||||||
<div style={{ flex: 2, minWidth: 0 }}>{deviceSettingsSection}</div>
|
{deviceSettingsSection}
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>{warrantySection}</div>
|
{warrantySection}
|
||||||
</div>
|
</div>
|
||||||
{/* Row 3: Misc (col1) + Notes (col2) + App Users (col3) */}
|
{/* Row 3: Misc + Notes + App Users — equal width */}
|
||||||
<div className="device-columns">
|
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "1.5rem", alignItems: "start" }}>
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>{miscSection}</div>
|
{miscSection}
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>{notesSection}</div>
|
{notesSection}
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>{appUsersSection}</div>
|
{appUsersSection}
|
||||||
</div>
|
</div>
|
||||||
{/* Latest Logs */}
|
{/* Latest Logs */}
|
||||||
{logsSection}
|
{logsSection}
|
||||||
|
|||||||
@@ -146,69 +146,10 @@ input[type="range"]::-moz-range-thumb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Device detail column layout */
|
/* Device detail column layout */
|
||||||
.device-columns {
|
|
||||||
display: flex;
|
|
||||||
gap: 1.5rem;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
.device-column {
|
.device-column {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.device-full-row {
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
.device-equal-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 1.5rem;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
.device-equal-row > * {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.device-flex-fill {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
.device-flex-fill > .flex-grow {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
/* Device info horizontal subsections */
|
|
||||||
.device-info-row {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
.device-info-row > .device-info-item {
|
|
||||||
padding: 0 1.5rem;
|
|
||||||
border-left: 1px solid var(--border-secondary);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
.device-info-row > .device-info-item:first-child {
|
|
||||||
padding-left: 0;
|
|
||||||
border-left: none;
|
|
||||||
}
|
|
||||||
/* Location 2-column internal layout */
|
|
||||||
.location-split {
|
|
||||||
display: flex;
|
|
||||||
gap: 1.5rem;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
.location-split > .location-fields {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
|
||||||
.location-split > .location-map {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* File input */
|
/* File input */
|
||||||
|
|||||||
Reference in New Issue
Block a user