Track HiveGrid Cloud planning reference

This commit is contained in:
david committed 2026-06-26 20:48:01 +02:00
1 parent b3d5ea6764
commit e341c5938a
5784 files changed
+1104929 -3

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# Lokaler Dev-Launcher (sauber, reproduzierbar).
#
# Warum noetig: `react-router dev` (Vite) laedt `.env`/`.env.local` NICHT in die SSR-`process.env`
# (nur VITE_-prefixed in den Client). Die App-Config-Validierung liest aber `process.env`
# (DATABASE_URL/APP_NAME/SECRETS ...). Dieser Launcher laedt `.env` + `.env.local` in die echte
# Shell-Umgebung, stellt Redis sicher und startet dann `pnpm dev`.
#
# Lokale Overrides (DB-Port, Host:3000) gehoeren in `.env.local` (gitignored, siehe .env.local.example).
#
# Nutzung: bash scripts/dev/local.sh
# Dann: http://app.localhost:3000 (admin: http://admin.localhost:3000)
set -uo pipefail
# Repo-Root (zwei Ebenen ueber dieser Datei)
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$ROOT"
# Node 22.12.0 via fnm (falls vorhanden)
if command -v fnm >/dev/null 2>&1; then
eval "$(fnm env)"
fnm use 22.12.0 >/dev/null 2>&1 || true
fi
# Env laden: zuerst .env (SOPS-pull), dann .env.local (lokale Overrides gewinnen)
set -a
# shellcheck disable=SC1091
[ -f .env ] && . ./.env
# shellcheck disable=SC1091
[ -f .env.local ] && . ./.env.local
set +a
if [ -z "${DATABASE_URL:-}" ]; then
echo "FEHLER: DATABASE_URL nicht gesetzt. Erst 'pnpm env:pull' ausfuehren (.env) und ggf. .env.local anlegen." >&2
exit 1
fi
# Lokales Redis (BullMQ / Session-Cache) sicherstellen
if ! nc -z localhost 6379 >/dev/null 2>&1; then
docker start hg-redis >/dev/null 2>&1 \
|| docker run -d --name hg-redis -p 6379:6379 redis:7-alpine >/dev/null 2>&1 \
|| echo "WARN: konnte Redis nicht starten (BullMQ/Session-Cache evtl. eingeschraenkt)." >&2
fi
echo "Lokaler Dev-Server: http://app.localhost:3000 (admin: http://admin.localhost:3000)"
exec pnpm dev
+50
View File
@@ -0,0 +1,50 @@
import { chromium } from "playwright";
const BASE="http://app.localhost:3000"; const R=[];
const ck=(n,ok,d="")=>R.push(`${ok?"✓":"✗"} ${n}${d?" — "+d:""}`);
const b=await chromium.launch(); const ctx=await b.newContext({viewport:{width:1440,height:900}}); const p=await ctx.newPage();
const errs=[]; p.on("pageerror",e=>errs.push("PAGEERR: "+e.message)); p.on("console",m=>{if(m.type()==="error")errs.push("console: "+m.text().slice(0,90));});
const open=async()=>{await p.locator('[data-ui-id="ui.action.shell.more"]').click(); await p.waitForTimeout(400);};
const dash=async()=>{await p.goto(`${BASE}/e2e-readiness/dashboard`,{waitUntil:"networkidle"}); await p.waitForTimeout(400);};
await p.goto(`${BASE}/login`,{waitUntil:"domcontentloaded"});
await p.fill('input[name="email"]',"e2e.readiness@example.test"); await p.fill('input[name="password"]',"ReadinessPass123!");
await p.click('button[type="submit"]'); await p.waitForTimeout(1500); await dash();
// collapse
const sb=p.locator('[data-ui-id="ui.surface.app.sidebar"]'); const w0=(await sb.boundingBox())?.width??0;
await p.locator('[data-ui-id="ui.surface.app.sidebar"] button').first().click(); await p.waitForTimeout(450);
const w1=(await sb.boundingBox())?.width??0; ck("Collapse",w1<w0-50,`${Math.round(w0)}->${Math.round(w1)}`);
await p.locator('[data-ui-id="ui.surface.app.sidebar"] button').first().click(); await p.waitForTimeout(450);
const w2=(await sb.boundingBox())?.width??0; ck("Expand",w2>w1+50,`${Math.round(w1)}->${Math.round(w2)}`);
// search
await p.locator('[data-ui-id="ui.action.shell.search"]').click(); await p.waitForTimeout(500);
ck("Suche->Palette",await p.locator('[data-ui-id^="ui.surface.commandPalette"],[cmdk-root],[role="dialog"]').first().isVisible().catch(()=>false));
await p.keyboard.press("Escape"); await p.waitForTimeout(300);
// theme toggle (regex fix)
const hb=await p.locator("html").getAttribute("class")??""; await open();
const ti=p.getByText(/dunkl|hell|dark mode|light mode/i).first();
const tfound=await ti.isVisible().catch(()=>false); ck("Theme-Item da",tfound);
if(tfound){await ti.click(); await p.waitForTimeout(1500);}
const ha=await p.locator("html").getAttribute("class")??""; ck("Theme aendert html",hb!==ha,`"${hb}"->"${ha}"`);
// profil
await dash(); await open();
await p.locator('[data-ui-id="ui.action.shell.profile"]').click(); await p.waitForTimeout(900);
ck("Profil navigiert",/settings\/profile/.test(p.url()),p.url().replace(BASE,""));
// tenant switch
await dash(); await open();
const nest=p.getByText(/Profil ausw|Arbeitsbereich|wechseln/i).first();
if(await nest.isVisible().catch(()=>false)){await nest.click(); await p.waitForTimeout(500);
const wagner=p.getByText(/Wagner|Klein/).first();
if(await wagner.isVisible().catch(()=>false)){await wagner.click(); await p.waitForTimeout(1200); ck("Tenant-Switch",/wagner|klein/i.test(p.url()),p.url().replace(BASE,""));}
else ck("Tenant-Eintrag sichtbar",false);
} else ck("Tenant-Nested sichtbar",false);
// bell
await dash(); await p.locator('[data-ui-id="ui.action.shell.notifications"]').click(); await p.waitForTimeout(800);
ck("Glocke->Notifications",/notifications/.test(p.url()),p.url().replace(BASE,""));
// nav
await dash(); await p.locator('[data-ui-id="ui.nav.app.crm"]').first().click(); await p.waitForTimeout(800);
ck("Nav CRM",/\/crm/.test(p.url()),p.url().replace(BASE,""));
// logout LAST
await dash(); await open();
await p.locator('[data-ui-id="ui.action.shell.signOut"]').click(); await p.waitForTimeout(1500);
ck("Logout",/\/login|:3000\/?$/.test(p.url()),p.url().replace(BASE,""));
console.log(R.join("\n")); console.log("ERRORS:",errs.length);
await b.close();
+52
View File
@@ -0,0 +1,52 @@
import { chromium } from "playwright";
import { mkdirSync } from "node:fs";
const BASE = "http://app.localhost:3000";
const EMAIL = "e2e.readiness@example.test";
const PASSWORD = "ReadinessPass123!";
const OUT = "/tmp/shots";
mkdirSync(OUT, { recursive: true });
const browser = await chromium.launch();
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await ctx.newPage();
const consoleErrors = [];
page.on("console", (m) => { if (m.type() === "error") consoleErrors.push(m.text()); });
page.on("pageerror", (e) => consoleErrors.push("PAGEERROR: " + e.message));
await page.goto(`${BASE}/login`, { waitUntil: "domcontentloaded" });
await page.fill('input[name="email"]', EMAIL);
await page.fill('input[name="password"]', PASSWORD);
await page.click('button[type="submit"]');
await page.waitForURL(/dashboard|select-tenant|\/[a-z0-9-]+\//, { timeout: 15000 }).catch(() => {});
await page.waitForTimeout(1200);
const landing = page.url();
console.log("landing:", landing);
// Tenant-Slug aus der Landing-URL ableiten (host-aware: app.localhost:3000/<slug>/...)
const m = landing.match(/app\.localhost:3000\/([a-z0-9-]+)\//);
const tenant = "e2e-readiness"; void m;
console.log("tenant:", tenant);
const screens = [
["dashboard", `/${tenant}/dashboard`],
["crm", `/${tenant}/crm`],
["email-marketing", `/${tenant}/email-marketing`],
["workflow-engine", `/${tenant}/workflow-engine`],
["notifications", `/${tenant}/notifications`],
["settings-members", `/${tenant}/settings/members`],
["settings-subscription", `/${tenant}/settings/subscription`],
["settings-account", `/${tenant}/settings/account`],
];
for (const [name, path] of screens) {
await page.goto(`${BASE}${path}`, { waitUntil: "networkidle", timeout: 20000 }).catch((e) => console.log(`goto FAIL ${name}: ${e.message}`));
await page.waitForTimeout(700);
const redirected = /\/login/.test(page.url());
await page.screenshot({ path: `${OUT}/${name}.png`, fullPage: true });
console.log(`shot ${name} <- ${page.url()}${redirected ? " [REDIRECTED TO LOGIN]" : ""}`);
}
console.log("=== CONSOLE ERRORS (" + consoleErrors.length + ") ===");
for (const e of [...new Set(consoleErrors)].slice(0, 30)) console.log(" " + e.slice(0, 160));
await browser.close();