mirror of
https://github.com/ipnet-mesh/meshcore-hub.git
synced 2026-10-30 19:52:03 +01:00
fix(web): path-node popup scrolls with page; wire frontend tests/typecheck into make + pre-commit
- PacketGroupDetail: render the path-node popover via createPortal(document.body) with position:absolute + document coordinates (rect + scrollX/scrollY) instead of position:fixed with one-shot viewport coords, so it scrolls with the page rather than staying pinned to the viewport. Outside-click/Escape close unchanged (DOM-based). - Makefile: 'make test' now runs pytest then the frontend vitest suite via a new 'test-frontend' target (npm run test:frontend). - pre-commit: add a local 'frontend-typecheck' hook (language: system) running 'npm run typecheck' (tsc --noEmit) on TS/TSX + tsconfig/package(-lock).json changes. - AGENTS.md: document the pre-commit TS gate and that 'make test' includes the frontend. Verified: tsc clean, vitest 49 passed, pre-commit --all-files green, make test (1459 backend passed + 49 frontend passed).
This commit is contained in:
4 files changed
+100
-75
No files matched your search
@@ -41,3 +41,15 @@ repos:
|
||||
- alembic>=1.7.0
|
||||
- types-paho-mqtt>=1.6.0
|
||||
- types-PyYAML>=6.0.0
|
||||
|
||||
# Frontend TypeScript gate. Runs in the host node toolchain (language: system)
|
||||
# so it resolves types from the project's node_modules — requires `npm install`.
|
||||
# `pass_filenames: false`: tsc typechecks the whole project (tsconfig include).
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: frontend-typecheck
|
||||
name: frontend typecheck (tsc --noEmit)
|
||||
entry: npm run typecheck
|
||||
language: system
|
||||
pass_filenames: false
|
||||
files: '(spa-react/.*\.tsx?$|^tsconfig\.json$|^package(-lock)?\.json$)'
|
||||
@@ -47,8 +47,9 @@ The web UI is a **React 19 + TypeScript + Vite** SPA in
|
||||
(`web/templates/spa.html`) renders only SEO/`window.__APP_CONFIG__`/footer; React renders the
|
||||
navbar, banners, and routed pages into `<div id="app">`. **Frontend tooling runs on the host**
|
||||
(not in Docker): `npm install`, `npm run build` (Tailwind → vendor fonts → `vite build` →
|
||||
`static/dist/` + `assets.json`), `npx tsc --noEmit` (the TS gate — there is no JS linter in
|
||||
pre-commit), and `npm run test:frontend` (vitest). The Vite build is required to serve the UI;
|
||||
`static/dist/` + `assets.json`), `npx tsc --noEmit` (the TS gate — also run by the
|
||||
`frontend-typecheck` pre-commit hook; there is no JS *linter* in pre-commit), and
|
||||
`npm run test:frontend` (vitest). The Vite build is required to serve the UI;
|
||||
there is no fallback bundle.
|
||||
|
||||
```bash
|
||||
@@ -84,9 +85,10 @@ Coverage is **opt-in**; add `--cov=meshcore_hub` (or `make test-cov`) when you w
|
||||
pytest -nauto --no-cov 2>&1 | grep -iE "passed|failed" | tail -3
|
||||
|
||||
# Makefile shorthands
|
||||
make test # pytest -nauto --no-cov (parallel dev loop)
|
||||
make test-cov # full run with coverage report
|
||||
make test-unit # parallel, fast unit suites only (skips e2e)
|
||||
make test # backend (pytest -nauto --no-cov) then frontend vitest
|
||||
make test-cov # full backend run with coverage report
|
||||
make test-unit # parallel, fast unit suites only (skips e2e)
|
||||
make test-frontend # frontend vitest only (npm run test:frontend)
|
||||
|
||||
# Targeted by component (run only what you changed)
|
||||
pytest --no-cov tests/test_web/ # templates, static JS, web routes
|
||||
|
||||
@@ -4,7 +4,7 @@ COMPOSE_FILES = -f docker-compose.yml -f docker-compose.dev.yml
|
||||
VOLUMES = $(COMPOSE_PROJECT_NAME)_data $(COMPOSE_PROJECT_NAME)_mqtt_data \
|
||||
$(COMPOSE_PROJECT_NAME)_observer_data
|
||||
|
||||
.PHONY: build up down logs backup restore test test-cov test-unit
|
||||
.PHONY: build up down logs backup restore test test-cov test-unit test-frontend
|
||||
|
||||
build:
|
||||
docker compose $(COMPOSE_FILES) --profile all build --no-cache
|
||||
@@ -36,11 +36,16 @@ restore:
|
||||
|
||||
# --- Tests ---------------------------------------------------------------
|
||||
# Coverage is opt-in (use test-cov). Dev loop runs in parallel across cores.
|
||||
# `test` runs the backend suite then the frontend (vitest) suite.
|
||||
test:
|
||||
pytest -nauto --no-cov
|
||||
$(MAKE) test-frontend
|
||||
|
||||
test-cov:
|
||||
pytest --cov=meshcore_hub --cov-report=term-missing
|
||||
|
||||
test-unit:
|
||||
pytest -nauto --no-cov tests/test_common/ tests/test_api/ tests/test_collector/ tests/test_web/
|
||||
|
||||
test-frontend:
|
||||
npm run test:frontend
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useParams } from "react-router";
|
||||
import { useAppConfig } from "@/context/AppConfigContext";
|
||||
@@ -351,7 +352,10 @@ export function PacketGroupDetail() {
|
||||
) {
|
||||
top = popover.top - ph - 4;
|
||||
}
|
||||
setPopoverPos({ left, top });
|
||||
setPopoverPos({
|
||||
left: left + window.scrollX,
|
||||
top: top + window.scrollY,
|
||||
});
|
||||
}, [popover, popoverNodes, popoverError]);
|
||||
|
||||
const openPathPopover = (e: React.MouseEvent, pathHash: string) => {
|
||||
@@ -629,74 +633,76 @@ export function PacketGroupDetail() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{popover && (
|
||||
<div
|
||||
ref={popoverRef}
|
||||
className="fixed z-[1000] w-64 max-w-[90vw] max-h-[60vh] overflow-y-auto bg-base-100 rounded-box shadow-lg border border-base-300"
|
||||
style={{
|
||||
left: popoverPos?.left ?? -9999,
|
||||
top: popoverPos?.top ?? -9999,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 px-3 py-2 border-b border-base-200 sticky top-0 bg-base-100 rounded-t-box">
|
||||
<span className="text-xs font-semibold uppercase opacity-70">
|
||||
{t("packets.path_nodes_title", { hash: popover.hash })}
|
||||
</span>
|
||||
<button
|
||||
className="btn btn-xs btn-ghost btn-circle"
|
||||
aria-label={t("common.close")}
|
||||
onClick={() => setPopover(null)}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
{popoverError ? (
|
||||
<div className="p-3">
|
||||
<WarningBadge message={popoverError} />
|
||||
</div>
|
||||
) : popoverNodes === null ? (
|
||||
<div className="p-3">
|
||||
<Loading />
|
||||
</div>
|
||||
) : popoverNodes.length === 0 ? (
|
||||
<div className="px-3 py-4 text-sm opacity-60 text-center">
|
||||
{t("packets.path_no_nodes")}
|
||||
</div>
|
||||
) : (
|
||||
<ul className="menu menu-sm w-full">
|
||||
{popoverNodes.map((n) => (
|
||||
<li key={n.public_key}>
|
||||
<Link
|
||||
to={`/nodes/${n.public_key}`}
|
||||
onClick={() => setPopover(null)}
|
||||
className="flex flex-col items-start gap-0"
|
||||
>
|
||||
<span className="text-sm">{nodeDisplayName(n)}</span>
|
||||
<span className="font-mono text-xs opacity-50">
|
||||
{truncateKey(n.public_key, 16)}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
{moreCount > 0 && (
|
||||
<li>
|
||||
<Link
|
||||
to={`/nodes?pubkey_prefix=${popover.hash}`}
|
||||
onClick={() => setPopover(null)}
|
||||
className="text-xs opacity-70"
|
||||
>
|
||||
{t("packets.path_nodes_more", {
|
||||
count: formatNumber(moreCount),
|
||||
})}
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{popover &&
|
||||
createPortal(
|
||||
<div
|
||||
ref={popoverRef}
|
||||
className="absolute z-[1000] w-64 max-w-[90vw] max-h-[60vh] overflow-y-auto bg-base-100 rounded-box shadow-lg border border-base-300"
|
||||
style={{
|
||||
left: popoverPos?.left ?? -9999,
|
||||
top: popoverPos?.top ?? -9999,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 px-3 py-2 border-b border-base-200 sticky top-0 bg-base-100 rounded-t-box">
|
||||
<span className="text-xs font-semibold uppercase opacity-70">
|
||||
{t("packets.path_nodes_title", { hash: popover.hash })}
|
||||
</span>
|
||||
<button
|
||||
className="btn btn-xs btn-ghost btn-circle"
|
||||
aria-label={t("common.close")}
|
||||
onClick={() => setPopover(null)}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
{popoverError ? (
|
||||
<div className="p-3">
|
||||
<WarningBadge message={popoverError} />
|
||||
</div>
|
||||
) : popoverNodes === null ? (
|
||||
<div className="p-3">
|
||||
<Loading />
|
||||
</div>
|
||||
) : popoverNodes.length === 0 ? (
|
||||
<div className="px-3 py-4 text-sm opacity-60 text-center">
|
||||
{t("packets.path_no_nodes")}
|
||||
</div>
|
||||
) : (
|
||||
<ul className="menu menu-sm w-full">
|
||||
{popoverNodes.map((n) => (
|
||||
<li key={n.public_key}>
|
||||
<Link
|
||||
to={`/nodes/${n.public_key}`}
|
||||
onClick={() => setPopover(null)}
|
||||
className="flex flex-col items-start gap-0"
|
||||
>
|
||||
<span className="text-sm">{nodeDisplayName(n)}</span>
|
||||
<span className="font-mono text-xs opacity-50">
|
||||
{truncateKey(n.public_key, 16)}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
{moreCount > 0 && (
|
||||
<li>
|
||||
<Link
|
||||
to={`/nodes?pubkey_prefix=${popover.hash}`}
|
||||
onClick={() => setPopover(null)}
|
||||
className="text-xs opacity-70"
|
||||
>
|
||||
{t("packets.path_nodes_more", {
|
||||
count: formatNumber(moreCount),
|
||||
})}
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user