diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..81aa336 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,28 @@ +# Dependências e artefatos de build +node_modules/ +dist/ + +# Arquivos de ambiente (contêm segredos) +.env + +# Logs e debug +logs/ +*.log + +# Artefatos de teste +test/*.js + +# Docs geradas +docs/_build/ + +# Dados de desenvolvimento +.git/ +.github/ + +# IDEs +.vscode/ +.idea/ + +# OS files +.DS_Store +Thumbs.db diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0dcefea --- /dev/null +++ b/.env.example @@ -0,0 +1,69 @@ +# ============================================================ +# GenieACS — variáveis de ambiente +# Copie este arquivo para .env e ajuste os valores +# NUNCA faça commit do .env com dados reais em repositórios +# públicos. +# ============================================================ + +# ── MongoDB ───────────────────────────────────────────────── +# Quando usando docker-compose o host é o nome do serviço "mongo" +GENIEACS_MONGODB_CONNECTION_URL=mongodb://mongo:27017/genieacs + +# ── Extensões ─────────────────────────────────────────────── +GENIEACS_EXT_DIR=/opt/genieacs/config/ext +GENIEACS_EXT_TIMEOUT=3000 + +# ── Logs ──────────────────────────────────────────────────── +# json | simple +GENIEACS_LOG_FORMAT=json +GENIEACS_ACCESS_LOG_FORMAT=json + +# Para gravar logs em arquivo (opcional, senão vai para stdout/stderr) +# GENIEACS_CWMP_LOG_FILE=/opt/genieacs/logs/cwmp.log +# GENIEACS_CWMP_ACCESS_LOG_FILE=/opt/genieacs/logs/cwmp-access.log +# GENIEACS_NBI_LOG_FILE=/opt/genieacs/logs/nbi.log +# GENIEACS_NBI_ACCESS_LOG_FILE=/opt/genieacs/logs/nbi-access.log +# GENIEACS_FS_LOG_FILE=/opt/genieacs/logs/fs.log +# GENIEACS_FS_ACCESS_LOG_FILE=/opt/genieacs/logs/fs-access.log +# GENIEACS_UI_LOG_FILE=/opt/genieacs/logs/ui.log +# GENIEACS_UI_ACCESS_LOG_FILE=/opt/genieacs/logs/ui-access.log + +# ── Debug (opcional) ──────────────────────────────────────── +# GENIEACS_DEBUG_FILE=/opt/genieacs/logs/debug.yaml +# GENIEACS_DEBUG_FORMAT=yaml + +# ── Worker processes (0 = 1 por CPU core) ─────────────────── +GENIEACS_CWMP_WORKER_PROCESSES=1 +GENIEACS_NBI_WORKER_PROCESSES=1 +GENIEACS_FS_WORKER_PROCESSES=1 +GENIEACS_UI_WORKER_PROCESSES=1 + +# ── Portas ────────────────────────────────────────────────── +GENIEACS_CWMP_PORT=7547 +GENIEACS_NBI_PORT=7557 +GENIEACS_FS_PORT=7567 +GENIEACS_UI_PORT=3000 + +# ── Interface de bind (:: = todas as interfaces) ──────────── +GENIEACS_CWMP_INTERFACE=:: +GENIEACS_NBI_INTERFACE=:: +GENIEACS_FS_INTERFACE=:: +GENIEACS_UI_INTERFACE=:: + +# ── UI — JWT Secret (OBRIGATÓRIO trocar em produção!) ─────── +# Mínimo 32 chars, máximo 64 chars +GENIEACS_UI_JWT_SECRET=mude-isto-em-producao-use-uma-string-aleatoria-longa + +# ── FS — URL prefix (configure se CWMP e FS estiverem em ──── +# hosts/IPs diferentes ou atrás de proxy) +# GENIEACS_FS_URL_PREFIX=http://seu-ip-publico:7567/ + +# ── SSL (opcional — passe o caminho para cert/key PEM) ────── +# GENIEACS_CWMP_SSL_CERT=/opt/genieacs/certs/cwmp.crt +# GENIEACS_CWMP_SSL_KEY=/opt/genieacs/certs/cwmp.key +# GENIEACS_NBI_SSL_CERT=/opt/genieacs/certs/nbi.crt +# GENIEACS_NBI_SSL_KEY=/opt/genieacs/certs/nbi.key +# GENIEACS_FS_SSL_CERT=/opt/genieacs/certs/fs.crt +# GENIEACS_FS_SSL_KEY=/opt/genieacs/certs/fs.key +# GENIEACS_UI_SSL_CERT=/opt/genieacs/certs/ui.crt +# GENIEACS_UI_SSL_KEY=/opt/genieacs/certs/ui.key diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..26c6a89 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,50 @@ +GitHub Copilot Instructions: GenieACS -> UniFi UI Theme +You are an expert senior software engineer and frontend architect focused on UI/UX modernization, maintainability, and clean architecture. Your primary goal is to help modify the GenieACS user interface to closely resemble the UniFi Controller, while strictly ensuring the codebase remains easy to synchronize (via merge or rebase) with the original GenieACS upstream repository for bug fixes and core updates. + +1. Core Principles (Non-Negotiable) +Sync-Friendly Architecture (Crucial): Write code defensively to prevent future merge conflicts with the original GenieACS project. Prefer extending, overriding, or wrapping components rather than heavily modifying deeply ingrained original core files. + +Visual Fidelity (UniFi): Implement the clean, dashboard-centric, modern aesthetic of the UniFi Controller. Use comparable color palettes, typography, spacing, and network topology layouts. + +Safety First: Never modify the underlying TR-069 communication logic, backend models, or database schemas unless explicitly requested. The focus is strictly on the presentation layer (View). + +Minimal Footprint: When generating files, aim to isolate custom UI assets. Keep custom CSS/SCSS and new UI components in distinct directories or files whenever possible. + +2. GenieACS Customization & Upstream Integration +CRITICAL: Whenever the user requests a modification to a page or component, you MUST prioritize the following strategies to keep the fork maintainable: + +Style Overrides: Prefer creating custom stylesheets (e.g., unifi-theme.scss) to override existing GenieACS classes instead of changing inline styles or the original CSS files directly. + +Component Wrapping: If an existing UI component needs significant structural changes, create a new "UniFi-styled" wrapper component that consumes the original GenieACS state/props, rather than gutting the original component. + +Clear Demarcation: When modifying an original GenieACS file is unavoidable, clearly mark the beginning and end of the custom code with comments (e.g., // --- BEGIN UNIFI THEME --- and // --- END UNIFI THEME ---). This makes resolving Git merge conflicts significantly easier. + +3. Code Generation Rules +A. Refactoring & Modification +Context Awareness: Read the surrounding code to understand the existing state management and API calls. Do not break the data flow between the GenieACS backend and the frontend. + +Preserve Logic: When changing how data is displayed (e.g., changing a simple table to a UniFi-style card grid), ensure all original data points remain accessible or are intentionally hidden via CSS, rather than deleting the data-fetching logic. + +B. New Features (UI Components) +Modularity: Build reusable UI components (Buttons, Cards, Navbars) that match the UniFi design system, so they can be applied globally across the GenieACS interface. + +Responsiveness: Ensure all new UI elements are fully responsive, as network management dashboards are often viewed on various screen sizes. + +C. Testing +Prioritize visual regression checks and ensure that frontend unit tests (if applicable) are updated to reflect structural DOM changes. Ensure the connection to the GenieACS API remains mocked and functional in tests. + +4. Response Format +Plan First: Before generating code, briefly outline your strategy. Specifically state which files will be modified and how you plan to implement the change while minimizing upstream merge conflicts. + +Complete Code: Output the full, functional block of code needed for the file. Do not use placeholders like // ... rest of code unless the file is massive and the change is highly localized. + +Git Context: If a change requires creating a new file to override an old one, explain how to integrate it into the project's build system. + +5. "Do No Harm" Checklist +Before outputting the final response, ask yourself: + +Does this modification break the core GenieACS device management logic? + +Will this change cause a massive merge conflict if the upstream repository updates this specific component? (If yes, rethink the approach). + +Does the resulting UI genuinely reflect the UniFi Controller design language? \ No newline at end of file diff --git a/.gitignore b/.gitignore index 87b41df..0a6b950 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules dist docs/_build +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..737a7ac --- /dev/null +++ b/Dockerfile @@ -0,0 +1,68 @@ +# ========================================================= +# GenieACS — UniFi Theme Fork +# Multi-stage build: +# Stage 1 (builder): instala devDeps + compila o projeto +# Stage 2 (runtime): imagem mínima só com dist/ + prodDeps +# ========================================================= + +# ── Stage 1: builder ───────────────────────────────────── +FROM node:20-alpine AS builder + +# Dependências de sistema necessárias para o build +# (git é exigido pelo build.ts para calcular o metadata de versão) +RUN apk add --no-cache git python3 make g++ + +WORKDIR /build + +# Copiar manifests primeiro para cache de camadas do npm install +COPY package.json npm-shrinkwrap.json ./ + +# Instalar TODAS as dependências (incluindo devDependencies) +RUN npm ci --fund=false --audit=false + +# Copiar o restante do fonte +COPY . . + +# Compilar o projeto (gera dist/) +RUN npm run build + +# ── Stage 2: runtime ───────────────────────────────────── +FROM node:20-alpine AS runtime + +# Timezone e utilitários mínimos +RUN apk add --no-cache tzdata dumb-init + +WORKDIR /opt/genieacs + +# Copiar artefatos compilados +COPY --from=builder /build/dist ./ + +# Instalar apenas dependências de produção a partir do dist/ +RUN npm ci --omit=dev --fund=false --audit=false + +# Criar diretórios de configuração e extensões +RUN mkdir -p \ + /opt/genieacs/config/ext \ + /opt/genieacs/logs \ + && addgroup -S genieacs \ + && adduser -S -G genieacs genieacs \ + && chown -R genieacs:genieacs /opt/genieacs + +USER genieacs + +# Variáveis de ambiente padrão (sobrescreva no compose ou .env) +ENV GENIEACS_MONGODB_CONNECTION_URL=mongodb://mongo:27017/genieacs \ + GENIEACS_EXT_DIR=/opt/genieacs/config/ext \ + GENIEACS_LOG_FORMAT=json \ + GENIEACS_ACCESS_LOG_FORMAT=json \ + GENIEACS_UI_JWT_SECRET=change-me-in-production \ + GENIEACS_CWMP_WORKER_PROCESSES=1 \ + GENIEACS_NBI_WORKER_PROCESSES=1 \ + GENIEACS_FS_WORKER_PROCESSES=1 \ + GENIEACS_UI_WORKER_PROCESSES=1 + +# Expor todas as portas dos serviços +EXPOSE 3000 7547 7557 7567 + +# dumb-init garante propagação correta de sinais +ENTRYPOINT ["/usr/bin/dumb-init", "--"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3688197 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,134 @@ +services: + + # ── MongoDB ────────────────────────────────────────────────────────────── + mongo: + image: mongo:6 + container_name: genieacs-mongo + restart: unless-stopped + volumes: + - mongo_data:/data/db + - mongo_config:/data/configdb + networks: + - genieacs + healthcheck: + test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 20s + + # ── CWMP — TR-069 protocol handler (porta 7547) ────────────────────────── + cwmp: + build: + context: . + dockerfile: Dockerfile + target: runtime + image: genieacs-unifi:latest + container_name: genieacs-cwmp + restart: unless-stopped + command: ["node", "bin/genieacs-cwmp"] + ports: + - "7547:7547" + env_file: .env + environment: + GENIEACS_MONGODB_CONNECTION_URL: mongodb://mongo:27017/genieacs + depends_on: + mongo: + condition: service_healthy + volumes: + - ext_dir:/opt/genieacs/config/ext + - logs:/opt/genieacs/logs + networks: + - genieacs + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:7547/ || true"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 15s + + # ── NBI — Northbound REST API (porta 7557) ─────────────────────────────── + nbi: + image: genieacs-unifi:latest + container_name: genieacs-nbi + restart: unless-stopped + command: ["node", "bin/genieacs-nbi"] + ports: + - "7557:7557" + env_file: .env + environment: + GENIEACS_MONGODB_CONNECTION_URL: mongodb://mongo:27017/genieacs + depends_on: + mongo: + condition: service_healthy + cwmp: + condition: service_started + volumes: + - ext_dir:/opt/genieacs/config/ext + - logs:/opt/genieacs/logs + networks: + - genieacs + + # ── FS — File Server / GridFS (porta 7567) ─────────────────────────────── + fs: + image: genieacs-unifi:latest + container_name: genieacs-fs + restart: unless-stopped + command: ["node", "bin/genieacs-fs"] + ports: + - "7567:7567" + env_file: .env + environment: + GENIEACS_MONGODB_CONNECTION_URL: mongodb://mongo:27017/genieacs + depends_on: + mongo: + condition: service_healthy + cwmp: + condition: service_started + volumes: + - ext_dir:/opt/genieacs/config/ext + - logs:/opt/genieacs/logs + networks: + - genieacs + + # ── UI — Web Interface (porta 3000) ────────────────────────────────────── + ui: + image: genieacs-unifi:latest + container_name: genieacs-ui + restart: unless-stopped + command: ["node", "bin/genieacs-ui"] + ports: + - "3000:3000" + env_file: .env + environment: + GENIEACS_MONGODB_CONNECTION_URL: mongodb://mongo:27017/genieacs + depends_on: + mongo: + condition: service_healthy + nbi: + condition: service_started + volumes: + - ext_dir:/opt/genieacs/config/ext + - logs:/opt/genieacs/logs + networks: + - genieacs + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:3000/ || exit 1"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 20s + +volumes: + mongo_data: + driver: local + mongo_config: + driver: local + ext_dir: + driver: local + logs: + driver: local + +networks: + genieacs: + driver: bridge diff --git a/ui/app.ts b/ui/app.ts index 456c41c..35e359f 100644 --- a/ui/app.ts +++ b/ui/app.ts @@ -21,6 +21,10 @@ import { PermissionSet, UiConfig } from "../lib/types.ts"; import drawerComponent from "./drawer-component.ts"; import { render as renderOverlay } from "./overlay.ts"; import Expression from "../lib/common/expression.ts"; +// --- BEGIN UNIFI THEME --- +import * as unifiDevicesPage from "./unifi-devices-page.ts"; +import * as unifiDevicePage from "./unifi-device-detail-page.ts"; +// --- END UNIFI THEME --- declare global { interface Window { @@ -105,8 +109,10 @@ function pagify(pageName, page): RouteResolver { m.route(document.body, "/overview", { "/wizard": pagify("wizard", wizardPage), "/overview": pagify("overview", overviewPage), - "/devices": pagify("devices", devicesPage), - "/devices/:id": pagify("devices", devicePage), + // --- BEGIN UNIFI THEME --- + "/devices": pagify("devices", unifiDevicesPage), + "/devices/:id": pagify("devices", unifiDevicePage), + // --- END UNIFI THEME --- "/faults": pagify("faults", faultsPage), "/presets": pagify("presets", presetsPage), "/provisions": pagify("provisions", provisionsPage), diff --git a/ui/css/app.css b/ui/css/app.css index 08eb256..6a3ec2c 100644 --- a/ui/css/app.css +++ b/ui/css/app.css @@ -1,5 +1,8 @@ @import "tailwindcss"; @plugin "@tailwindcss/forms"; +/* --- BEGIN UNIFI THEME --- */ +@import "./unifi-theme.css"; +/* --- END UNIFI THEME --- */ @source inline("h-full bg-stone-100"); @source "../../ui/**/*.{ts,tsx}"; diff --git a/ui/css/unifi-theme.css b/ui/css/unifi-theme.css new file mode 100644 index 0000000..68f4c38 --- /dev/null +++ b/ui/css/unifi-theme.css @@ -0,0 +1,610 @@ +/* ============================================================= + * UniFi UI Theme — GenieACS + * Isolates all UniFi Controller visual overrides so that + * upstream GenieACS merges touch this file at most. + * ============================================================= */ + +/* ---- Sidebar ---- */ +.unifi-sidebar { + background-color: #0d1b2a !important; + border-right: 1px solid #111e2e !important; +} + +.unifi-sidebar .unifi-brand { + padding: 20px 16px 12px; + display: flex; + align-items: center; + gap: 10px; +} + +.unifi-sidebar .unifi-brand img { + filter: brightness(0) invert(1); + height: 32px; + width: auto; +} + +.unifi-nav-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + border-radius: 6px; + font-size: 0.875rem; + font-weight: 500; + color: #8b9bb4; + text-decoration: none; + transition: background-color 0.15s, color 0.15s; + margin-bottom: 2px; +} + +.unifi-nav-item:hover { + background-color: rgba(255, 255, 255, 0.06); + color: #c8d6e8; +} + +.unifi-nav-item.active { + background-color: rgba(0, 111, 255, 0.14); + color: #4d9de0; +} + +.unifi-nav-item svg { + width: 18px; + height: 18px; + flex-shrink: 0; + opacity: 0.75; +} + +.unifi-nav-item.active svg { + opacity: 1; +} + +.unifi-sidebar-footer { + padding: 12px; + border-top: 1px solid rgba(255, 255, 255, 0.07); + color: #8b9bb4; + font-size: 0.8125rem; +} + +.unifi-sidebar-footer .unifi-user-info { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 4px; +} + +.unifi-sidebar-footer .unifi-logout-btn { + background: none; + border: none; + color: #4d9de0; + cursor: pointer; + font-size: 0.8125rem; + padding: 0; + margin-left: auto; + text-decoration: none; +} + +.unifi-sidebar-footer .unifi-logout-btn:hover { + color: #72b8f5; +} + +.unifi-version { + font-size: 0.6875rem; + font-family: var(--font-mono, monospace); + color: #3d5166; + text-align: right; + padding: 6px 12px; +} + +/* ---- Page header ---- */ +.unifi-page-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 24px; + padding-bottom: 0; + flex-wrap: wrap; + gap: 12px; +} + +.unifi-page-title { + font-size: 1.25rem; + font-weight: 700; + color: #1a2537; + margin: 0; +} + +/* ---- Stat cards row ---- */ +.unifi-stat-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 16px; + margin-bottom: 24px; +} + +.unifi-stat-card { + background: #ffffff; + border-radius: 8px; + padding: 16px 20px; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.07); + border: 1px solid #e8eaf0; + display: flex; + flex-direction: column; + gap: 4px; +} + +.unifi-stat-card__label { + font-size: 0.75rem; + color: #7a8699; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.unifi-stat-card__value { + font-size: 1.75rem; + font-weight: 700; + color: #1a2537; + line-height: 1; +} + +.unifi-stat-card__value.online { color: #43b02a; } +.unifi-stat-card__value.warning { color: #ffa720; } +.unifi-stat-card__value.offline { color: #e53935; } + +/* ---- Toolbar ---- */ +.unifi-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 12px; + margin-bottom: 20px; +} + +.unifi-toolbar-left { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } +.unifi-toolbar-right { display: flex; align-items: center; gap: 10px; } + +/* ---- Page size selector ---- */ +.unifi-page-size-selector { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.8125rem; + color: #7a8699; +} + +.unifi-page-size-selector select { + background: #fff; + border: 1px solid #d0d6e0; + border-radius: 6px; + padding: 4px 8px; + font-size: 0.8125rem; + color: #1a2537; + cursor: pointer; + appearance: auto; +} + +/* ---- Buttons ---- */ +.unifi-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 7px 14px; + border-radius: 6px; + font-size: 0.8125rem; + font-weight: 500; + cursor: pointer; + border: 1px solid transparent; + transition: background-color 0.15s, opacity 0.15s; + white-space: nowrap; + text-decoration: none; +} + +.unifi-btn:disabled { opacity: 0.45; cursor: not-allowed; } + +.unifi-btn--primary { + background-color: #006fff; + color: #fff; + border-color: #006fff; +} +.unifi-btn--primary:hover:not(:disabled) { background-color: #005ee0; border-color: #005ee0; } + +.unifi-btn--secondary { + background-color: #fff; + color: #1a2537; + border-color: #d0d6e0; +} +.unifi-btn--secondary:hover:not(:disabled) { background-color: #f4f5f7; } + +.unifi-btn--danger { + background-color: #fff; + color: #e53935; + border-color: #e53935; +} +.unifi-btn--danger:hover:not(:disabled) { background-color: #fff0f0; } + +.unifi-btn--icon { + padding: 6px; + background: none; + border: 1px solid #d0d6e0; + border-radius: 6px; + color: #7a8699; + cursor: pointer; + display: inline-flex; + align-items: center; +} +.unifi-btn--icon:hover { background: #f4f5f7; color: #1a2537; } + +/* ---- Device card grid ---- */ +.unifi-device-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 16px; +} + +.unifi-device-card { + background: #ffffff; + border-radius: 8px; + border: 1px solid #e8eaf0; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); + overflow: hidden; + cursor: pointer; + transition: box-shadow 0.15s, border-color 0.15s, transform 0.1s; + display: flex; + flex-direction: column; +} + +.unifi-device-card:hover { + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); + border-color: #b8c0d0; + transform: translateY(-1px); +} + +.unifi-device-card__header { + padding: 14px 16px 10px; + display: flex; + align-items: flex-start; + gap: 10px; + border-bottom: 1px solid #f0f2f6; +} + +.unifi-device-card__name { + font-size: 0.9375rem; + font-weight: 600; + color: #1a2537; + margin: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; +} + +.unifi-device-card__model { + font-size: 0.75rem; + color: #7a8699; + margin: 2px 0 0; +} + +.unifi-device-card__body { + padding: 12px 16px; + flex: 1; + display: flex; + flex-direction: column; + gap: 7px; +} + +.unifi-device-card__row { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.8125rem; +} + +.unifi-device-card__row-label { + color: #7a8699; + min-width: 52px; + font-size: 0.75rem; +} + +.unifi-device-card__row-value { + color: #1a2537; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; + font-family: var(--font-mono, monospace); + font-size: 0.75rem; +} + +.unifi-device-card__row-value.empty { + color: #b0b8c8; + font-family: var(--font-sans, sans-serif); + font-style: italic; +} + +.unifi-device-card__footer { + padding: 8px 16px; + background: #f8f9fc; + border-top: 1px solid #f0f2f6; + display: flex; + align-items: center; + justify-content: space-between; + font-size: 0.75rem; + color: #9aa5b8; +} + +.unifi-device-card__tags { + display: flex; + gap: 4px; + flex-wrap: wrap; +} + +/* ---- Status dot / badge ---- */ +.unifi-status-dot { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 50%; + flex-shrink: 0; + margin-top: 3px; +} + +.unifi-status-dot--online { background-color: #43b02a; box-shadow: 0 0 0 3px rgba(67,176,42,0.18); } +.unifi-status-dot--warning { background-color: #ffa720; box-shadow: 0 0 0 3px rgba(255,167,32,0.18); } +.unifi-status-dot--offline { background-color: #e53935; box-shadow: 0 0 0 3px rgba(229,57,53,0.18); } +.unifi-status-dot--unknown { background-color: #b0b8c8; } + +.unifi-status-badge { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 2px 7px; + border-radius: 999px; + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.unifi-status-badge--online { background: rgba(67,176,42,0.12); color: #2e8020; } +.unifi-status-badge--warning { background: rgba(255,167,32,0.12); color: #b87000; } +.unifi-status-badge--offline { background: rgba(229,57,53,0.12); color: #c62828; } +.unifi-status-badge--unknown { background: rgba(176,184,200,0.18); color: #7a8699; } + +/* ---- Tag badge ---- */ +.unifi-tag { + display: inline-block; + padding: 1px 7px; + border-radius: 999px; + font-size: 0.6875rem; + background: rgba(0, 111, 255, 0.1); + color: #006fff; + font-weight: 500; +} + +/* ---- Pagination ---- */ +.unifi-pagination { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + margin-top: 24px; + font-size: 0.8125rem; + color: #7a8699; +} + +.unifi-pagination__info { + margin-right: 8px; +} + +/* ---- Device detail page ---- */ +.unifi-device-detail-header { + background: #fff; + border: 1px solid #e8eaf0; + border-radius: 8px; + padding: 20px 24px; + margin-bottom: 20px; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); +} + +.unifi-device-detail-header__top { + display: flex; + align-items: flex-start; + gap: 14px; + flex-wrap: wrap; +} + +.unifi-device-detail-header__icon { + width: 48px; + height: 48px; + border-radius: 8px; + background: #f0f6ff; + display: flex; + align-items: center; + justify-content: center; + color: #006fff; + flex-shrink: 0; +} + +.unifi-device-detail-header__info { + flex: 1; +} + +.unifi-device-detail-header__name { + font-size: 1.25rem; + font-weight: 700; + color: #1a2537; + margin: 0 0 4px; +} + +.unifi-device-detail-header__meta { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; + font-size: 0.8125rem; + color: #7a8699; +} + +.unifi-device-detail-header__actions { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + margin-top: 4px; +} + +.unifi-device-detail-header__kv { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 12px 24px; + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid #f0f2f6; +} + +.unifi-detail-kv__label { + font-size: 0.75rem; + color: #7a8699; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.04em; + margin-bottom: 2px; +} + +.unifi-detail-kv__value { + font-size: 0.875rem; + color: #1a2537; + font-family: var(--font-mono, monospace); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.unifi-detail-kv__value.empty { + color: #b0b8c8; + font-family: var(--font-sans, sans-serif); + font-style: italic; +} + +.unifi-device-content { + background: #ffffff; + border: 1px solid #e8eaf0; + border-radius: 8px; + padding: 20px 24px; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); +} + +/* ---- Back button ---- */ +.unifi-back-link { + display: inline-flex; + align-items: center; + gap: 6px; + color: #4d9de0; + font-size: 0.875rem; + font-weight: 500; + text-decoration: none; + margin-bottom: 16px; +} + +.unifi-back-link:hover { color: #2880cc; } + +/* ---- Login page override ---- */ +.unifi-login-wrapper { + min-height: 100vh; + background: linear-gradient(135deg, #0d1b2a 0%, #1a2f45 100%); + display: flex; + align-items: center; + justify-content: center; +} + +.unifi-login-card { + background: #fff; + border-radius: 12px; + padding: 40px 36px; + width: 100%; + max-width: 400px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); +} + +/* ---- Table overrides (for inner device page content) ---- */ +.device-page table { + border-collapse: separate; + border-spacing: 0; + width: 100%; +} + +.device-page table th { + background: #f8f9fc; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: #7a8699; + padding: 10px 14px; + border-bottom: 1px solid #e8eaf0; + text-align: left; +} + +.device-page table td { + padding: 9px 14px; + border-bottom: 1px solid #f0f2f6; + font-size: 0.875rem; + color: #1a2537; + vertical-align: middle; +} + +.device-page table tr:last-child td { + border-bottom: none; +} + +.device-page table tr:hover td { + background-color: #f8faff; +} + +/* ---- Main content background ---- */ +.unifi-main-bg { + background: #f4f5f7; + min-height: 100vh; +} + +/* ---- Users / permissions pages ---- */ +.unifi-admin-section { + background: #fff; + border-radius: 8px; + border: 1px solid #e8eaf0; + overflow: hidden; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); + margin-bottom: 20px; +} + +.unifi-admin-section__header { + padding: 14px 20px; + border-bottom: 1px solid #e8eaf0; + background: #f8f9fc; + display: flex; + align-items: center; + justify-content: space-between; +} + +.unifi-admin-section__title { + font-size: 0.9375rem; + font-weight: 600; + color: #1a2537; + margin: 0; +} + +/* ---- Responsive adjustments ---- */ +@media (max-width: 640px) { + .unifi-device-grid { + grid-template-columns: 1fr; + } + + .unifi-stat-row { + grid-template-columns: 1fr 1fr; + } + + .unifi-device-detail-header__kv { + grid-template-columns: 1fr; + } +} diff --git a/ui/icons/nav-back.svg b/ui/icons/nav-back.svg new file mode 100644 index 0000000..df24586 --- /dev/null +++ b/ui/icons/nav-back.svg @@ -0,0 +1 @@ + diff --git a/ui/icons/nav-config.svg b/ui/icons/nav-config.svg new file mode 100644 index 0000000..3fb1358 --- /dev/null +++ b/ui/icons/nav-config.svg @@ -0,0 +1 @@ + diff --git a/ui/icons/nav-devices.svg b/ui/icons/nav-devices.svg new file mode 100644 index 0000000..835672f --- /dev/null +++ b/ui/icons/nav-devices.svg @@ -0,0 +1 @@ + diff --git a/ui/icons/nav-faults.svg b/ui/icons/nav-faults.svg new file mode 100644 index 0000000..aa461d4 --- /dev/null +++ b/ui/icons/nav-faults.svg @@ -0,0 +1 @@ + diff --git a/ui/icons/nav-files.svg b/ui/icons/nav-files.svg new file mode 100644 index 0000000..79cb8a2 --- /dev/null +++ b/ui/icons/nav-files.svg @@ -0,0 +1 @@ + diff --git a/ui/icons/nav-overview.svg b/ui/icons/nav-overview.svg new file mode 100644 index 0000000..dc29031 --- /dev/null +++ b/ui/icons/nav-overview.svg @@ -0,0 +1 @@ + diff --git a/ui/icons/nav-permissions.svg b/ui/icons/nav-permissions.svg new file mode 100644 index 0000000..a316455 --- /dev/null +++ b/ui/icons/nav-permissions.svg @@ -0,0 +1 @@ + diff --git a/ui/icons/nav-presets.svg b/ui/icons/nav-presets.svg new file mode 100644 index 0000000..21314f0 --- /dev/null +++ b/ui/icons/nav-presets.svg @@ -0,0 +1 @@ + diff --git a/ui/icons/nav-provisions.svg b/ui/icons/nav-provisions.svg new file mode 100644 index 0000000..d3d98f0 --- /dev/null +++ b/ui/icons/nav-provisions.svg @@ -0,0 +1 @@ + diff --git a/ui/icons/nav-users.svg b/ui/icons/nav-users.svg new file mode 100644 index 0000000..14abcd1 --- /dev/null +++ b/ui/icons/nav-users.svg @@ -0,0 +1 @@ + diff --git a/ui/icons/nav-virtualparams.svg b/ui/icons/nav-virtualparams.svg new file mode 100644 index 0000000..4450ecf --- /dev/null +++ b/ui/icons/nav-virtualparams.svg @@ -0,0 +1 @@ + diff --git a/ui/layout.tsx b/ui/layout.tsx index 8c3d03c..2a6df2b 100644 --- a/ui/layout.tsx +++ b/ui/layout.tsx @@ -102,6 +102,21 @@ const component: ClosureComponent = () => { return { name, href, active: vnode.attrs["page"] === n }; }); + // --- BEGIN UNIFI THEME --- + const NAV_ICONS: Record = { + "Overview": "nav-overview", + "Devices": "nav-devices", + "Faults": "nav-faults", + "Presets": "nav-presets", + "Provisions": "nav-provisions", + "Virtual Parameters": "nav-virtualparams", + "Files": "nav-files", + "Config": "nav-config", + "Permissions": "nav-permissions", + "Users": "nav-users", + }; + // --- END UNIFI THEME --- + return [ @@ -128,7 +143,8 @@ const component: ClosureComponent = () => { leaveFrom="translate-x-0" leaveTo="-translate-x-full" > - + {/* --- BEGIN UNIFI THEME --- */} + = () => { + {item.name} ))} - +