mirror of
https://github.com/ipnet-mesh/meshcore-hub.git
synced 2026-08-07 17:33:20 +02:00
Merge pull request #164 from ipnet-mesh/feature/ui-bundling
Replace all CDN dependencies with locally built vendor assets
This commit is contained in:
@@ -220,3 +220,8 @@ __marimo__/
|
||||
# MeshCore Hub specific
|
||||
*.db
|
||||
meshcore.db
|
||||
|
||||
# Frontend build artifacts
|
||||
node_modules/
|
||||
src/meshcore_hub/web/static/vendor/
|
||||
src/meshcore_hub/web/static/css/tailwind.css
|
||||
|
||||
@@ -59,7 +59,8 @@ MeshCore Hub is a Python 3.14+ monorepo for managing and orchestrating MeshCore
|
||||
| MQTT Broker | [meshcore-mqtt-broker](https://github.com/michaelhart/meshcore-mqtt-broker) (WebSocket + JWT auth) |
|
||||
| Templates | Jinja2 (server), lit-html (SPA) |
|
||||
| Frontend | ES Modules SPA with client-side routing |
|
||||
| CSS Framework | Tailwind CSS + DaisyUI |
|
||||
| CSS Framework | Tailwind CSS v4 (CLI) + DaisyUI v5 |
|
||||
| Frontend Build | Node.js 22 LTS (`npm run build`) |
|
||||
| Testing | pytest, pytest-asyncio |
|
||||
| Formatting | black |
|
||||
| Linting | flake8 |
|
||||
@@ -296,7 +297,15 @@ meshcore-hub/
|
||||
│ ├── middleware.py # Cache-Control middleware
|
||||
│ ├── templates/ # Jinja2 templates (spa.html shell)
|
||||
│ └── static/
|
||||
│ ├── css/app.css # Custom styles
|
||||
│ ├── css/
|
||||
│ │ ├── app.css # Custom styles
|
||||
│ │ ├── input.css # Tailwind v4 input (source)
|
||||
│ │ └── tailwind.css # Built Tailwind+DaisyUI CSS (generated)
|
||||
│ ├── vendor/ # Vendored JS/CSS libraries (built by npm run build)
|
||||
│ │ ├── lit-html/ # lit-html ES module
|
||||
│ │ ├── leaflet/ # Leaflet map library
|
||||
│ │ ├── chart.js/ # Chart.js library
|
||||
│ │ └── qrcodejs/ # QR code library
|
||||
│ └── js/spa/ # SPA frontend (ES modules)
|
||||
│ ├── app.js # Entry point, route registration
|
||||
│ ├── router.js # Client-side History API router
|
||||
@@ -339,7 +348,9 @@ meshcore-hub/
|
||||
├── data/ # Runtime data (gitignored, DATA_HOME default)
|
||||
│ └── collector/ # Collector data
|
||||
│ └── meshcore.db # SQLite database
|
||||
├── Dockerfile # Docker build configuration
|
||||
├── Dockerfile # Docker build configuration (multi-stage: Node.js frontend + Python)
|
||||
├── package.json # Frontend build dependencies (Tailwind, DaisyUI, lit-html, etc.)
|
||||
├── build.js # Frontend build script (Tailwind CLI + vendor copy)
|
||||
├── docker-compose.yml # Docker Compose base config
|
||||
├── docker-compose.dev.yml # Development overrides (port mappings)
|
||||
├── docker-compose.prod.yml # Production overrides (proxy network)
|
||||
@@ -591,6 +602,10 @@ For full translation guidelines, see [docs/i18n.md](docs/i18n.md).
|
||||
### Running the Development Environment
|
||||
|
||||
```bash
|
||||
# Build frontend assets (requires Node.js 22+ LTS)
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
# Create virtual environment
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
|
||||
+22
-2
@@ -2,7 +2,23 @@
|
||||
# Build and run MeshCore Hub components
|
||||
|
||||
# =============================================================================
|
||||
# Stage 1: Builder - Install dependencies and build package
|
||||
# Stage 1: Frontend - Build Tailwind CSS and vendor static assets
|
||||
# =============================================================================
|
||||
FROM node:22-slim AS frontend
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY build.js ./
|
||||
COPY src/meshcore_hub/web/static/css/input.css ./src/meshcore_hub/web/static/css/input.css
|
||||
COPY src/meshcore_hub/web/templates/ ./src/meshcore_hub/web/templates/
|
||||
COPY src/meshcore_hub/web/static/js/ ./src/meshcore_hub/web/static/js/
|
||||
RUN npm run build
|
||||
|
||||
# =============================================================================
|
||||
# Stage 2: Builder - Install dependencies and build package
|
||||
# =============================================================================
|
||||
FROM python:3.14-slim AS builder
|
||||
|
||||
@@ -28,6 +44,10 @@ COPY src/ ./src/
|
||||
COPY alembic/ ./alembic/
|
||||
COPY alembic.ini ./
|
||||
|
||||
# Overlay built frontend assets onto source tree
|
||||
COPY --from=frontend /app/src/meshcore_hub/web/static/vendor ./src/meshcore_hub/web/static/vendor
|
||||
COPY --from=frontend /app/src/meshcore_hub/web/static/css/tailwind.css ./src/meshcore_hub/web/static/css/tailwind.css
|
||||
|
||||
# Build argument for version (set via CI or manually)
|
||||
ARG BUILD_VERSION=dev
|
||||
|
||||
@@ -37,7 +57,7 @@ RUN sed -i "s|__version__ = \"dev\"|__version__ = \"${BUILD_VERSION}\"|" src/mes
|
||||
pip install .
|
||||
|
||||
# =============================================================================
|
||||
# Stage 2: Runtime - Final production image
|
||||
# Stage 3: Runtime - Final production image
|
||||
# =============================================================================
|
||||
FROM python:3.14-slim AS runtime
|
||||
|
||||
|
||||
@@ -451,6 +451,12 @@ curl -H "Authorization: Bearer <API_ADMIN_KEY>" http://localhost:8000/api/v1/mem
|
||||
# Clone and setup
|
||||
git clone https://github.com/ipnet-mesh/meshcore-hub.git
|
||||
cd meshcore-hub
|
||||
|
||||
# Install frontend dependencies and build static assets (requires Node.js 22+ LTS)
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
# Setup Python environment
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -e ".[dev]"
|
||||
@@ -467,6 +473,8 @@ meshcore-hub api
|
||||
meshcore-hub web
|
||||
```
|
||||
|
||||
> **Note:** `npm run build` compiles Tailwind CSS and copies vendor libraries (lit-html, Leaflet, Chart.js, QRCode.js) into `src/meshcore_hub/web/static/vendor/`. This step is required before the web dashboard will render correctly. In Docker, this happens automatically during the build.
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
@@ -514,8 +522,10 @@ meshcore-hub/
|
||||
│ └── web/ # Web dashboard
|
||||
│ ├── templates/ # Jinja2 templates (SPA shell)
|
||||
│ └── static/
|
||||
│ ├── js/spa/ # SPA frontend (ES modules, lit-html)
|
||||
│ └── locales/ # Translation files (en.json)
|
||||
│ ├── css/ # Stylesheets (app.css, input.css, built tailwind.css)
|
||||
│ ├── vendor/ # Vendored JS/CSS libraries (built by npm run build)
|
||||
│ ├── js/spa/ # SPA frontend (ES modules, lit-html)
|
||||
│ └── locales/ # Translation files (en.json)
|
||||
├── tests/ # Test suite
|
||||
├── alembic/ # Database migrations
|
||||
├── etc/ # Configuration files (MQTT, Prometheus, Alertmanager)
|
||||
@@ -534,7 +544,9 @@ meshcore-hub/
|
||||
│ └── media/ # Custom media files
|
||||
│ └── images/ # Custom images (logo.svg/png/jpg/jpeg/webp replace default logo)
|
||||
├── data/ # Runtime data directory (DATA_HOME, created at runtime)
|
||||
├── Dockerfile # Docker build configuration
|
||||
├── Dockerfile # Docker build configuration (multi-stage: Node.js frontend + Python)
|
||||
├── package.json # Frontend build dependencies (Tailwind, DaisyUI, lit-html, etc.)
|
||||
├── build.js # Frontend build script (Tailwind CLI + vendor copy)
|
||||
├── docker-compose.yml # Docker Compose base config
|
||||
├── docker-compose.dev.yml # Development overrides (port mappings)
|
||||
├── docker-compose.prod.yml # Production overrides (proxy network)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import { cpSync, mkdirSync, existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const STATIC = join("src", "meshcore_hub", "web", "static");
|
||||
const VENDOR = join(STATIC, "vendor");
|
||||
|
||||
function vendor(pkg, files, dest) {
|
||||
const out = join(VENDOR, dest);
|
||||
mkdirSync(out, { recursive: true });
|
||||
for (const f of files) {
|
||||
const src = join("node_modules", pkg, f);
|
||||
if (!existsSync(src)) {
|
||||
console.error(` MISSING: ${src}`);
|
||||
process.exit(1);
|
||||
}
|
||||
cpSync(src, join(out, f.split("/").pop()), { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Building Tailwind CSS...");
|
||||
execSync(
|
||||
`npx @tailwindcss/cli build --input ${join(STATIC, "css", "input.css")} --output ${join(STATIC, "css", "tailwind.css")} --minify`,
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
|
||||
console.log("Copying vendor files...");
|
||||
|
||||
vendor("lit-html", ["lit-html.js", "lit-html.js.map"], "lit-html");
|
||||
vendor(
|
||||
"lit-html",
|
||||
[
|
||||
"directive.js",
|
||||
"directive.js.map",
|
||||
"directive-helpers.js",
|
||||
"directive-helpers.js.map",
|
||||
"async-directive.js",
|
||||
"async-directive.js.map",
|
||||
],
|
||||
"lit-html",
|
||||
);
|
||||
vendor(
|
||||
"lit-html",
|
||||
["directives/unsafe-html.js", "directives/unsafe-html.js.map"],
|
||||
"lit-html/directives",
|
||||
);
|
||||
|
||||
vendor("leaflet", ["dist/leaflet.css", "dist/leaflet.js", "dist/leaflet.js.map"], "leaflet");
|
||||
mkdirSync(join(VENDOR, "leaflet", "images"), { recursive: true });
|
||||
cpSync(
|
||||
join("node_modules", "leaflet", "dist", "images"),
|
||||
join(VENDOR, "leaflet", "images"),
|
||||
{ recursive: true },
|
||||
);
|
||||
|
||||
vendor("chart.js", ["dist/chart.umd.min.js"], "chart.js");
|
||||
vendor("qrcodejs", ["qrcode.min.js"], "qrcodejs");
|
||||
|
||||
console.log("Done.");
|
||||
Generated
+1049
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "node build.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/cli": "^4",
|
||||
"chart.js": "^4",
|
||||
"daisyui": "^5",
|
||||
"leaflet": "^1.9.4",
|
||||
"lit-html": "^3",
|
||||
"qrcodejs": "^1.0.0",
|
||||
"tailwindcss": "^4"
|
||||
}
|
||||
}
|
||||
@@ -75,12 +75,12 @@
|
||||
.navbar .menu li:has(.nav-icon-map) { --nav-color: var(--color-map); }
|
||||
.navbar .menu li:has(.nav-icon-members) { --nav-color: var(--color-members); }
|
||||
|
||||
/* Section-tinted hover and active backgrounds (!important to override DaisyUI CDN) */
|
||||
/* Section-tinted hover and active backgrounds (!important to override DaisyUI) */
|
||||
.navbar .menu li > a:hover {
|
||||
background-color: color-mix(in oklch, var(--nav-color, oklch(var(--bc))) 12%, transparent) !important;
|
||||
background-color: color-mix(in oklch, var(--nav-color, var(--color-base-content)) 12%, transparent) !important;
|
||||
}
|
||||
.navbar .menu li > a.active {
|
||||
background-color: color-mix(in oklch, var(--nav-color, oklch(var(--bc))) 20%, transparent) !important;
|
||||
background-color: color-mix(in oklch, var(--nav-color, var(--color-base-content)) 20%, transparent) !important;
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
}
|
||||
|
||||
.panel-solid {
|
||||
background-color: color-mix(in oklch, var(--panel-color, transparent) 10%, oklch(var(--b1)));
|
||||
background-color: color-mix(in oklch, var(--panel-color, transparent) 10%, var(--color-base-100));
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
@@ -129,24 +129,24 @@
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: oklch(var(--b2));
|
||||
background: var(--color-base-200);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: oklch(var(--bc) / 0.3);
|
||||
background: color-mix(in oklch, var(--color-base-content) 30%, transparent);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: oklch(var(--bc) / 0.5);
|
||||
background: color-mix(in oklch, var(--color-base-content) 50%, transparent);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Table Styling
|
||||
========================================================================== */
|
||||
|
||||
.table-compact td,
|
||||
.table-compact th {
|
||||
.table-sm td,
|
||||
.table-sm th {
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
@@ -217,23 +217,23 @@
|
||||
}
|
||||
|
||||
.prose a {
|
||||
color: oklch(var(--p));
|
||||
color: var(--color-primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.prose a:hover {
|
||||
color: oklch(var(--pf));
|
||||
color: var(--color-primary-content);
|
||||
}
|
||||
|
||||
.prose code {
|
||||
background: oklch(var(--b2));
|
||||
background: var(--color-base-200);
|
||||
padding: 0.125rem 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.prose pre {
|
||||
background: oklch(var(--b2));
|
||||
background: var(--color-base-200);
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
overflow-x: auto;
|
||||
@@ -246,7 +246,7 @@
|
||||
}
|
||||
|
||||
.prose blockquote {
|
||||
border-left: 4px solid oklch(var(--bc) / 0.3);
|
||||
border-left: 4px solid color-mix(in oklch, var(--color-base-content) 30%, transparent);
|
||||
padding-left: 1rem;
|
||||
margin: 1rem 0;
|
||||
font-style: italic;
|
||||
@@ -260,19 +260,19 @@
|
||||
|
||||
.prose th,
|
||||
.prose td {
|
||||
border: 1px solid oklch(var(--bc) / 0.2);
|
||||
border: 1px solid color-mix(in oklch, var(--color-base-content) 20%, transparent);
|
||||
padding: 0.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.prose th {
|
||||
background: oklch(var(--b2));
|
||||
background: var(--color-base-200);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.prose hr {
|
||||
border: none;
|
||||
border-top: 1px solid oklch(var(--bc) / 0.2);
|
||||
border-top: 1px solid color-mix(in oklch, var(--color-base-content) 20%, transparent);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
@@ -290,18 +290,18 @@
|
||||
|
||||
/* Popup styling */
|
||||
.leaflet-popup-content-wrapper {
|
||||
background: oklch(var(--b1));
|
||||
color: oklch(var(--bc));
|
||||
background: var(--color-base-100);
|
||||
color: var(--color-base-content);
|
||||
}
|
||||
|
||||
.leaflet-popup-tip {
|
||||
background: oklch(var(--b1));
|
||||
background: var(--color-base-100);
|
||||
}
|
||||
|
||||
/* Map container defaults */
|
||||
#map,
|
||||
#node-map {
|
||||
border-radius: var(--rounded-box);
|
||||
border-radius: var(--radius-box);
|
||||
}
|
||||
|
||||
#map {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "daisyui";
|
||||
@source "../js/";
|
||||
@source "../../templates/";
|
||||
@@ -53,7 +53,7 @@ function renderRecentAds(ads) {
|
||||
});
|
||||
|
||||
return html`<div class="overflow-x-auto">
|
||||
<table class="table table-compact w-full">
|
||||
<table class="table table-sm w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>${t('entities.node')}</th>
|
||||
@@ -102,8 +102,9 @@ function renderChannelMessages(channelMessages, channelLabels) {
|
||||
|
||||
/** Return a Tailwind grid-cols class for the given visible column count. */
|
||||
function gridCols(count) {
|
||||
if (count <= 1) return '';
|
||||
return `md:grid-cols-${count}`;
|
||||
if (count === 2) return 'md:grid-cols-2';
|
||||
if (count === 3) return 'md:grid-cols-3';
|
||||
return '';
|
||||
}
|
||||
|
||||
export async function render(container, params, router) {
|
||||
|
||||
@@ -55,7 +55,7 @@ export async function render(container, params, router) {
|
||||
|
||||
const customPageButtons = features.pages !== false
|
||||
? customPages.slice(0, 3).map(page => html`
|
||||
<a href="${page.url}" class="btn btn-outline btn-neutral">
|
||||
<a href="${page.url}" class="btn btn-outline">
|
||||
${iconPage('h-5 w-5 mr-2')}
|
||||
${page.title}
|
||||
</a>`)
|
||||
|
||||
@@ -68,7 +68,7 @@ export async function render(container, params, router) {
|
||||
|
||||
const adsTableHtml = advertisements.length > 0
|
||||
? html`<div class="overflow-x-auto">
|
||||
<table class="table table-compact w-full">
|
||||
<table class="table table-sm w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>${t('common.time')}</th>
|
||||
@@ -107,7 +107,7 @@ export async function render(container, params, router) {
|
||||
const tags = node.tags || [];
|
||||
const tagsTableHtml = tags.length > 0
|
||||
? html`<div class="overflow-x-auto">
|
||||
<table class="table table-compact w-full">
|
||||
<table class="table table-sm w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>${t('common.key')}</th>
|
||||
|
||||
@@ -37,12 +37,11 @@
|
||||
</style>
|
||||
{% endif %}
|
||||
|
||||
<!-- Tailwind CSS with DaisyUI -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.4.19/dist/full.min.css" rel="stylesheet" type="text/css" />
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<!-- Tailwind CSS with DaisyUI (built from source) -->
|
||||
<link rel="stylesheet" href="/static/css/tailwind.css?v={{ version }}" />
|
||||
|
||||
<!-- Leaflet CSS for maps -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
<link rel="stylesheet" href="/static/vendor/leaflet/leaflet.css" />
|
||||
|
||||
<!-- Custom application styles -->
|
||||
<link rel="stylesheet" href="/static/css/app.css?v={{ version }}">
|
||||
@@ -51,8 +50,8 @@
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"lit-html": "https://esm.sh/lit-html@3",
|
||||
"lit-html/": "https://esm.sh/lit-html@3/"
|
||||
"lit-html": "/static/vendor/lit-html/lit-html.js",
|
||||
"lit-html/": "/static/vendor/lit-html/"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -172,13 +171,13 @@
|
||||
</footer>
|
||||
|
||||
<!-- Leaflet JS for maps -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script src="/static/vendor/leaflet/leaflet.js"></script>
|
||||
|
||||
<!-- Chart.js for charts -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="/static/vendor/chart.js/chart.umd.min.js"></script>
|
||||
|
||||
<!-- QR Code library -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script>
|
||||
<script src="/static/vendor/qrcodejs/qrcode.min.js"></script>
|
||||
|
||||
<!-- Chart helper functions -->
|
||||
<script src="/static/js/charts.js?v={{ version }}"></script>
|
||||
|
||||
Reference in New Issue
Block a user