Merge pull request #164 from ipnet-mesh/feature/ui-bundling

Replace all CDN dependencies with locally built vendor assets
This commit is contained in:
JingleManSweep
2026-04-20 13:18:56 +01:00
committed by GitHub
13 changed files with 1224 additions and 44 deletions
+5
View File
@@ -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
+18 -3
View File
@@ -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
View File
@@ -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
+15 -3
View File
@@ -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)
+59
View File
@@ -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.");
+1049
View File
File diff suppressed because it is too large Load Diff
+16
View File
@@ -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"
}
}
+21 -21
View File
@@ -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>
+8 -9
View File
@@ -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>