/* * Copyright (C) 2025 l5yth * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ :root { --bg: #f6f3ee; --bg2: #ffffff; --fg: #0c0f12; --muted: #5c6773; --card: rgba(0, 0, 0, 0.03); --line: rgba(12, 15, 18, 0.08); --accent: #2b6cb0; --row-alt: rgba(0, 0, 0, 0.02); --table-head-bg: rgba(0, 0, 0, 0.06); --table-head-fg: var(--fg); --input-bg: #ffffff; --input-fg: var(--fg); --input-border: rgba(12, 15, 18, 0.18); --input-placeholder: rgba(12, 15, 18, 0.45); --control-accent: var(--accent); --pad: 16px; --map-tile-filter-light: grayscale(1) saturate(0) brightness(0.92) contrast(1.05); --map-tile-filter-dark: grayscale(1) invert(1) brightness(0.9) contrast(1.08); } html { color-scheme: light; } html[data-theme='dark'] { color-scheme: dark; } body.dark { --bg: #0e1418; --bg2: #0e141b; --fg: #e6ebf0; --muted: #9aa7b4; --card: rgba(255, 255, 255, 0.04); --line: rgba(255, 255, 255, 0.1); --accent: #5fa8ff; --row-alt: rgba(255, 255, 255, 0.05); --table-head-bg: rgba(255, 255, 255, 0.06); --table-head-fg: var(--fg); --input-bg: #1e262c; --input-fg: var(--fg); --input-border: rgba(230, 235, 240, 0.24); --input-placeholder: rgba(230, 235, 240, 0.55); --control-accent: var(--accent); } html, body { background-color: var(--bg); color: var(--fg); transition: background-color 160ms ease, color 160ms ease; } a { color: var(--accent); } hr { border-color: var(--line); } .card, .panel, .box { background: var(--card); backdrop-filter: blur(2px); border: 1px solid var(--line); border-radius: 10px; } table { border-collapse: collapse; width: 100%; border: 1px solid var(--line); } thead th { background: var(--table-head-bg); color: var(--table-head-fg); text-align: left; border-bottom: 1px solid var(--line); padding: 8px; } tbody td { padding: 8px; border-bottom: 1px solid var(--line); } tbody tr:nth-child(even) td { background: var(--row-alt); } .leaflet-container { background: transparent !important; color: var(--fg); } .neighbor-connection-line { cursor: pointer; } .neighbor-snr { margin-left: 4px; color: var(--muted); font-size: 12px; } body { font-family: system-ui, Segoe UI, Roboto, Ubuntu, Arial, sans-serif; margin: var(--pad); padding-bottom: 96px; --map-tiles-filter: var(--map-tile-filter-light); } h1 { margin: 0; } .site-header { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-bottom: 8px; } .site-title { display: inline-flex; align-items: center; gap: 12px; } .site-title img { width: 52px; height: 52px; display: block; border-radius: 12px; } .meta { color: #555; margin-bottom: 12px; } .instance-selector { display: flex; align-items: center; } .instance-select { appearance: none; -webkit-appearance: none; -moz-appearance: none; background-color: var(--input-bg); color: var(--input-fg); border: 1px solid var(--input-border); border-radius: 8px; padding: 6px 32px 6px 12px; font-size: 14px; line-height: 1.4; min-width: 220px; background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%), linear-gradient(135deg, var(--muted) 50%, transparent 50%); background-position: calc(100% - 18px) calc(50% - 4px), calc(100% - 12px) calc(50% - 4px); background-size: 6px 6px, 6px 6px; background-repeat: no-repeat; } .instance-select:focus { outline: 2px solid var(--accent); outline-offset: 2px; } .visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } @media (max-width: 900px) { .site-header { flex-direction: column; align-items: flex-start; } .instance-selector, .instance-select { width: 100%; } .instance-select { min-width: 0; } } .pill { display: inline-block; padding: 2px 8px; border-radius: 999px; background: #eee; font-size: 12px; } #map { position: relative; width: 100%; height: 60vh; border: 1px solid #ddd; border-radius: 8px; overflow: hidden; display: block; } .map-panel.is-fullscreen, .map-panel:fullscreen, .map-panel:-webkit-full-screen, .map-panel:-moz-full-screen, .map-panel:-ms-fullscreen, #map.is-fullscreen, #map:fullscreen, #map:-webkit-full-screen, #map:-moz-full-screen, #map:-ms-fullscreen { width: 100vw; height: 100vh; min-width: 100vw; min-height: 100vh; max-width: 100vw; max-height: 100vh; margin: 0; border: none; border-radius: 0; background: #000; } .map-panel.is-fullscreen, .map-panel:fullscreen, .map-panel:-webkit-full-screen, .map-panel:-moz-full-screen, .map-panel:-ms-fullscreen { display: block; } .map-panel.is-fullscreen #map, .map-panel:fullscreen #map, .map-panel:-webkit-full-screen #map, .map-panel:-moz-full-screen #map, .map-panel:-ms-fullscreen #map, #map.is-fullscreen, #map:fullscreen, #map:-webkit-full-screen, #map:-moz-full-screen, #map:-ms-fullscreen { height: 100vh !important; width: 100vw !important; min-height: 100vh; min-width: 100vw; max-height: 100vh; max-width: 100vw; border: none; border-radius: 0; flex: none; } .map-panel.is-fullscreen .map-toolbar, .map-panel:fullscreen .map-toolbar, .map-panel:-webkit-full-screen .map-toolbar, .map-panel:-moz-full-screen .map-toolbar, .map-panel:-ms-fullscreen .map-toolbar { top: 16px; right: 16px; } #map[data-map-status="placeholder"] { background: repeating-linear-gradient( 135deg, rgba(0, 0, 0, 0.02), rgba(0, 0, 0, 0.02) 12px, rgba(0, 0, 0, 0.04) 12px, rgba(0, 0, 0, 0.04) 24px ); } #map .map-placeholder-message { text-align: center; color: #555; font-size: 14px; line-height: 1.5; padding: 0 16px; } #map .map-placeholder-message strong { display: block; margin-bottom: 6px; font-size: 16px; } #map .map-placeholder-message span { display: block; margin-top: 4px; } body.dark #map .map-placeholder-message { color: #ddd; } #map .map-status-message { position: absolute; top: 12px; left: 50%; transform: translateX(-50%); padding: 6px 12px; border-radius: 999px; font-size: 12px; line-height: 1.2; backdrop-filter: blur(6px); pointer-events: none; border: 1px solid rgba(0, 0, 0, 0.15); background: rgba(255, 255, 255, 0.9); color: #333; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); z-index: 1200; } body.dark #map[data-map-status="placeholder"] { background: repeating-linear-gradient( 135deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0.02) 12px, rgba(255, 255, 255, 0.06) 12px, rgba(255, 255, 255, 0.06) 24px ); } body.dark #map .map-status-message { border-color: rgba(255, 255, 255, 0.16); background: rgba(0, 0, 0, 0.65); color: #eee; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); } body.dark .map-toolbar button { background: rgba(36, 36, 36, 0.88); color: #f1f1f1; border: 1px solid rgba(255, 255, 255, 0.25); } body.dark .map-toolbar button:hover { background: rgba(52, 52, 52, 0.9); } table { border-collapse: collapse; width: 100%; margin: 0; } th, td { padding: 4px 6px; text-align: left; } th { position: sticky; top: 0; background: #fafafa; } .mono { font-family: ui-monospace, Menlo, Consolas, monospace; } .row { display: flex; gap: var(--pad); align-items: center; justify-content: space-between; } .map-row { display: flex; gap: var(--pad); align-items: stretch; } .map-panel { position: relative; flex: 1 1 0%; min-width: 0; display: block; } .map-toolbar { position: absolute; top: 12px; right: 12px; display: flex; gap: 8px; z-index: 1300; pointer-events: none; } .map-toolbar button { display: inline-flex; align-items: center; justify-content: center; padding: 6px 12px; border-radius: 999px; border: 1px solid rgba(0, 0, 0, 0.2); background: rgba(255, 255, 255, 0.95); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); font-size: 13px; line-height: 1.2; transition: background-color 160ms ease, color 160ms ease, border-color 160ms ease, box-shadow 160ms ease; pointer-events: auto; } .map-toolbar button:hover { background: rgba(245, 245, 245, 0.95); } .map-toolbar button svg { width: 18px; height: 18px; display: block; fill: currentColor; } #mapFullscreenToggle .icon-fullscreen-exit { display: none; } #mapFullscreenToggle[aria-pressed='true'] .icon-fullscreen-exit { display: block; } #mapFullscreenToggle[aria-pressed='true'] .icon-fullscreen-enter { display: none; } #chat { flex: 0 0 33%; max-width: 33%; height: 60vh; border: 1px solid #ddd; border-radius: 8px; overflow-y: auto; padding: 6px; font-size: 12px; } .chat-entry-node { font-family: ui-monospace, Menlo, Consolas, monospace; color: #555; } .chat-entry-msg { font-family: ui-monospace, Menlo, Consolas, monospace; } .chat-entry-date { font-family: ui-monospace, Menlo, Consolas, monospace; font-weight: bold; } .short-name { display: inline-block; border-radius: 4px; padding: 0 2px; } .short-name[data-node-info] { cursor: pointer; } .short-info-overlay { position: absolute; background: #fff; color: #111; border: 1px solid #ccc; border-radius: 8px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18); padding: 8px 10px 10px; font-size: 11px; line-height: 1.4; min-width: 200px; max-width: 240px; z-index: 12000; } .short-info-overlay[hidden] { display: none; } .short-info-overlay .short-info-close { position: absolute; top: 4px; right: 4px; border: none; background: transparent; font-size: 14px; line-height: 1; padding: 2px; border-radius: 4px; cursor: pointer; color: inherit; } .short-info-overlay .short-info-close:hover { background: rgba(0, 0, 0, 0.08); } .short-info-content { margin: 0; } .meta-info { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; } .refresh-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; align-items: start; width: 100%; } .refresh-info { margin: 0; color: #555; } .refresh-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; justify-self: end; } .auto-refresh-toggle { display: inline-flex; align-items: center; gap: 6px; } .controls { display: flex; gap: 8px; align-items: center; } .controls label { display: inline-flex; align-items: center; gap: 6px; } .controls .filter-input { display: inline-flex; align-items: center; flex: 1 1 260px; position: relative; } .filter-input input[type="text"] { flex: 1 1 auto; width: 100%; padding-right: 28px; } .filter-clear { position: absolute; right: 6px; display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; border: none; background: transparent; color: var(--muted); font-size: 16px; line-height: 1; border-radius: 50%; cursor: pointer; padding: 0; } .filter-clear:hover { background: rgba(0, 0, 0, 0.06); } body.dark .filter-clear:hover { background: rgba(255, 255, 255, 0.12); } .filter-clear:focus-visible { outline: 2px solid #4a90e2; outline-offset: 2px; } button { padding: 6px 10px; border: 1px solid #ccc; background: #fff; border-radius: 6px; cursor: pointer; color: var(--fg); } .icon-button { width: 36px; height: 36px; padding: 0; display: inline-flex; align-items: center; justify-content: center; line-height: 1; } button:hover { background: #f6f6f6; } .sort-button { padding: 0; border: none; background: none; color: inherit; font: inherit; cursor: pointer; display: inline-flex; align-items: center; gap: 4px; } .sort-button:hover { background: none; } .sort-button:focus-visible { outline: 2px solid #4a90e2; outline-offset: 2px; } .sort-indicator { font-size: 0.75em; opacity: 0.6; } th[aria-sort] .sort-indicator { opacity: 1; } label { font-size: 14px; color: #333; } input[type="text"] { padding: 6px 10px; border: 1px solid var(--input-border); border-radius: 6px; background: var(--input-bg); color: var(--input-fg); transition: background-color 160ms ease, color 160ms ease, border-color 160ms ease, box-shadow 160ms ease; } input[type="text"]::placeholder { color: var(--input-placeholder); } input[type="text"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-color: var(--accent); } input[type="checkbox"], input[type="radio"] { accent-color: var(--control-accent); transition: accent-color 160ms ease, background-color 160ms ease; } .legend { position: relative; background: #fff; color: var(--fg); padding: 8px 10px 10px; border: 1px solid #ccc; border-radius: 8px; font-size: 12px; line-height: 18px; min-width: 160px; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); } .legend-header { display: flex; align-items: center; justify-content: flex-start; gap: 4px; margin-bottom: 6px; font-weight: 600; } .legend-title { font-size: 13px; } .legend-items { display: flex; flex-direction: column; gap: 2px; } .legend-item { display: flex; align-items: center; gap: 6px; font: inherit; color: inherit; background: transparent; border: 1px solid transparent; border-radius: 4px; padding: 3px 6px; cursor: pointer; width: 100%; justify-content: flex-start; text-align: left; } .legend-item:hover { background: rgba(0, 0, 0, 0.05); } .legend-item:focus-visible { outline: 2px solid #4a90e2; outline-offset: 2px; } .legend-item[aria-pressed="true"] { border-color: rgba(0, 0, 0, 0.2); background: rgba(0, 0, 0, 0.08); } .legend-swatch { display: inline-block; width: 12px; height: 12px; border-radius: 2px; } .legend-hidden { display: none !important; } .legend-toggle { margin-top: 8px; } .legend-toggle-button { font-size: 12px; color: var(--fg); } #map .leaflet-tile-pane, #map .leaflet-layer, #map .leaflet-tile.map-tiles { opacity: 0.75; filter: var(--map-tiles-filter, var(--map-tile-filter-light)); -webkit-filter: var(--map-tiles-filter, var(--map-tile-filter-light)); } .leaflet-popup-content-wrapper, .leaflet-popup-tip { background: #fff; color: #333; box-shadow: 0 3px 14px rgba(0, 0, 0, 0.4); } #nodes { font-size: 12px; } .app-footer { position: fixed; inset-block-end: 0; inset-inline: 0; width: 100%; background: #fafafa; border-top: 1px solid #ddd; font-size: 12px; z-index: 4100; } .app-footer .footer-content { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 6px; margin: 0 auto; width: 100%; max-width: 960px; padding: 6px var(--pad); text-align: center; box-sizing: border-box; } .app-footer .footer-separator { margin: 0 4px; } .app-footer .footer-links { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; justify-content: center; } .app-footer .footer-brand { font-weight: 600; } .app-footer a { color: inherit; } @media (max-width: 600px) { .app-footer .footer-content { padding: 10px 12px; justify-content: center; gap: 4px 8px; } .app-footer .footer-links { flex-direction: column; gap: 4px; } .app-footer .footer-separator { display: none; } } .info-overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.45); display: flex; align-items: center; justify-content: center; padding: var(--pad); z-index: 13000; } .info-overlay[hidden] { display: none; } .info-dialog { background: #fff; color: #111; max-width: 420px; width: min(100%, 420px); border-radius: 12px; box-shadow: 0 16px 40px rgba(0, 0, 0, 0.2); position: relative; padding: 20px 24px; outline: none; } .info-dialog:focus { outline: 2px solid #4a90e2; outline-offset: 4px; } .info-close { position: absolute; top: 10px; right: 10px; padding: 4px; border: none; background: transparent; font-size: 20px; line-height: 1; border-radius: 999px; } .info-close:hover { background: rgba(0, 0, 0, 0.06); } .info-title { margin: 0 0 8px; font-size: 20px; } .info-intro { margin: 0 0 12px; font-size: 14px; color: #444; } .info-details { margin: 0; font-size: 14px; line-height: 1.6; } .info-details dt { font-weight: 600; margin-top: 12px; color: #222; } .info-details dd { margin: 4px 0 0; } .info-details dd a { color: inherit; word-break: break-word; } @media (max-width: 1024px) { .row { flex-direction: column; align-items: stretch; gap: var(--pad); } .site-title img { width: 44px; height: 44px; } .map-row { flex-direction: column; } .controls { order: 2; display: grid; grid-template-columns: auto minmax(0, 1fr) auto auto; align-items: center; width: 100%; gap: 12px; } .controls .filter-input { width: 100%; } .controls button { justify-self: end; } .meta-info { order: 1; width: 100%; } .refresh-row { grid-template-columns: 1fr; row-gap: 8px; } .refresh-actions { flex-direction: row; align-items: center; gap: 8px; justify-self: start; flex-wrap: nowrap; } #map { order: 1; width: 100%; max-width: 100%; height: 50vh; } #chat { order: 2; flex: none; max-width: 100%; height: 30vh; } .legend { max-width: min(240px, 80vw); } } @media (max-width: 1679px) { .nodes-col--node-id { display: none; } } @media (max-width: 1559px) { .nodes-col--temperature, .nodes-col--humidity, .nodes-col--pressure { display: none; } } @media (max-width: 1319px) { .nodes-col--latitude, .nodes-col--longitude, .nodes-col--last-position { display: none; } } @media (max-width: 1109px) { .nodes-col--voltage, .nodes-col--air-util-tx, .nodes-col--altitude { display: none; } } @media (max-width: 899px) { .nodes-col--uptime, .nodes-col--frequency, .nodes-col--modem-preset { display: none; } } @media (max-width: 659px) { .nodes-col--battery, .nodes-col--channel-util, .nodes-col--hw-model { display: none; } } body.dark { background: #111; color: #eee; --map-tiles-filter: var(--map-tile-filter-dark); } body.dark .meta { color: #bbb; } body.dark .refresh-info { color: #bbb; } body.dark .pill { background: #444; } body.dark #map { border-color: #444; } body.dark #chat { border-color: #444; background: #222; color: #eee; } body.dark th { background: #222; } body.dark button { background: #333; border-color: #444; color: #eee; } body.dark button:hover { background: #444; } body.dark .sort-button { background: none; border: none; color: inherit; } body.dark .sort-button:hover { background: none; } body.dark label { color: #ddd; } body.dark .legend { background: #333; border-color: #444; color: #eee; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.45); } body.dark .legend-toggle-button { background: #333; border-color: #444; color: #eee; } body.dark .legend-toggle-button:hover { background: #444; } body.dark .legend-item:hover { background: rgba(255, 255, 255, 0.1); } body.dark .legend-item[aria-pressed="true"] { border-color: rgba(255, 255, 255, 0.3); background: rgba(255, 255, 255, 0.16); } body.dark .leaflet-popup-content-wrapper, body.dark .leaflet-popup-tip { background: #333; color: #eee; box-shadow: 0 3px 14px rgba(0, 0, 0, 0.8); } body.dark footer { background: #222; border-top-color: #444; color: #eee; } body.dark a { color: #9bd; } body.dark .chat-entry-node { color: #777; } body.dark .chat-entry-msg { color: #bbb; } body.dark .short-name { color: #555; } body.dark .chat-entry-date { color: #bbb; } body.dark .info-overlay { background: rgba(0, 0, 0, 0.7); } body.dark .info-dialog { background: #1c1c1c; color: #eee; border: 1px solid #444; } body.dark .info-intro { color: #bbb; } body.dark .info-details dt { color: #ddd; } body.dark .info-close:hover { background: rgba(255, 255, 255, 0.1); } body.dark .short-info-overlay { background: #1c1c1c; border-color: #444; color: #eee; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.55); } body.dark .short-info-overlay .short-info-close:hover { background: rgba(255, 255, 255, 0.1); } #map .leaflet-tile-pane, #map .leaflet-layer, #map .leaflet-tile.map-tiles { filter: var(--map-tiles-filter, var(--map-tile-filter-light)); -webkit-filter: var(--map-tiles-filter, var(--map-tile-filter-light)); } body.dark #map .leaflet-tile-pane, body.dark #map .leaflet-layer, body.dark #map .leaflet-tile.map-tiles { filter: var(--map-tiles-filter, var(--map-tile-filter-dark)); -webkit-filter: var(--map-tiles-filter, var(--map-tile-filter-dark)); }