Nexus upgrade design and refresh

This commit is contained in:
2026-04-11 01:23:28 +02:00
parent 9d5bb2d3cf
commit e83925ba64
53 changed files with 13388 additions and 60 deletions

View File

@@ -496,3 +496,386 @@ body {
.site-footer { margin: 0 12px 12px; }
.header-nav { flex-wrap: wrap; justify-content: flex-end; }
}
:root {
--surface: rgba(255, 255, 255, 0.9);
--surface-strong: #ffffff;
--accent-pink: #ed1671;
--accent-cyan: #06a9c8;
--accent-orange: #f6aa21;
--accent-green: #8bc53f;
--brand-accent: var(--accent-pink);
--brand-accent-2: var(--accent-cyan);
--brand-accent-3: var(--accent-orange);
}
:root[data-accent="pink"] {
--brand-accent: var(--accent-pink);
--brand-accent-2: var(--accent-orange);
--brand-accent-3: var(--accent-cyan);
}
:root[data-accent="cyan"] {
--brand-accent: var(--accent-cyan);
--brand-accent-2: var(--accent-green);
--brand-accent-3: var(--accent-pink);
}
:root[data-accent="orange"] {
--brand-accent: var(--accent-orange);
--brand-accent-2: var(--accent-pink);
--brand-accent-3: var(--accent-cyan);
}
:root[data-accent="green"] {
--brand-accent: var(--accent-green);
--brand-accent-2: var(--accent-cyan);
--brand-accent-3: var(--accent-orange);
}
:root[data-theme="day"] {
--bg: #f7fbfb;
--panel: rgba(255, 255, 255, 0.92);
--panel-2: #f1fbf7;
--surface: rgba(255, 255, 255, 0.9);
--surface-strong: #ffffff;
--text: #10212b;
--muted: #66737b;
--accent: var(--brand-accent);
--accent-2: var(--brand-accent-2);
--line: rgba(16, 33, 43, 0.12);
}
:root[data-theme="night"] {
--bg: #07121a;
--panel: rgba(8, 18, 28, 0.9);
--panel-2: rgba(18, 33, 48, 0.92);
--surface: rgba(8, 18, 28, 0.88);
--surface-strong: #101d2a;
--text: #eff8fb;
--muted: #a6b8c2;
--accent: var(--brand-accent);
--accent-2: var(--brand-accent-2);
--line: rgba(255, 255, 255, 0.12);
--shadow: 0 22px 60px rgba(0, 0, 0, 0.34);
}
html {
min-height: 100%;
background:
radial-gradient(circle at top left, color-mix(in srgb, var(--brand-accent-2) 20%, transparent), transparent 26%),
radial-gradient(circle at top right, color-mix(in srgb, var(--brand-accent) 18%, transparent), transparent 24%),
linear-gradient(135deg, #f7fbfb 0%, #eef7f5 52%, #fff4df 100%);
}
:root[data-theme="night"] {
background:
radial-gradient(circle at top left, color-mix(in srgb, var(--brand-accent-2) 28%, transparent), transparent 28%),
radial-gradient(circle at top right, color-mix(in srgb, var(--brand-accent) 24%, transparent), transparent 24%),
linear-gradient(135deg, #050b12 0%, #0c1721 52%, #111827 100%);
}
body {
background:
radial-gradient(circle at 12% 20%, color-mix(in srgb, var(--accent-green) 16%, transparent), transparent 24%),
radial-gradient(circle at 90% 6%, color-mix(in srgb, var(--accent-orange) 16%, transparent), transparent 20%),
var(--bg);
}
.site-header {
padding: 8px 14px;
}
.site-logo {
height: 46px;
}
.main-content {
width: min(80vw, 1680px);
margin: 0 auto;
background: transparent;
box-shadow: none;
padding: 0;
}
.main-content > .card,
.home-hero,
.module-row,
.empty-state,
.module-host-card {
border: 1px solid var(--line);
background: var(--surface);
box-shadow: 0 12px 30px rgba(1, 22, 32, 0.08);
backdrop-filter: blur(8px);
}
.home-hero {
position: relative;
overflow: hidden;
display: flex;
align-items: center;
gap: 18px;
margin-bottom: 16px;
padding: 16px 18px;
border-radius: 20px;
}
.brand-mark {
position: relative;
z-index: 1;
display: inline-grid;
place-items: center;
width: 76px;
height: 76px;
flex: 0 0 auto;
border-radius: 20px;
background: #ffffff;
box-shadow: inset 0 0 0 1px rgba(16, 33, 43, 0.08), 0 12px 30px rgba(6, 169, 200, 0.12);
}
.brand-mark img {
display: block;
width: 62px;
height: 62px;
object-fit: contain;
}
.brand-copy {
position: relative;
z-index: 1;
min-width: 0;
}
.eyebrow,
.module-kicker {
color: var(--brand-accent);
font-size: 0.74rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.eyebrow {
display: inline-flex;
margin-bottom: 6px;
padding: 4px 9px;
border-radius: 999px;
background: color-mix(in srgb, var(--brand-accent) 12%, transparent);
}
.home-hero h1,
.section-title {
margin: 0;
font-weight: 700;
letter-spacing: -0.03em;
}
.home-hero h1 {
font-size: clamp(1.5rem, 4vw, 2.35rem);
line-height: 1;
}
.home-hero p,
.section-head p,
.module-desc {
color: var(--muted);
}
.section-head {
display: flex;
align-items: end;
justify-content: space-between;
gap: 16px;
margin: 8px 0 12px;
}
.module-list {
display: grid;
gap: 10px;
}
.module-row {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: 14px;
padding: 14px;
border-radius: 18px;
transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease, background 160ms ease;
}
.module-row:hover,
.module-row:focus-visible {
transform: translateY(-2px);
border-color: color-mix(in srgb, var(--brand-accent) 36%, transparent);
background: var(--surface-strong);
}
.module-row__icon {
display: inline-grid;
place-items: center;
width: 44px;
height: 44px;
border-radius: 14px;
color: #ffffff;
font-weight: 800;
background: linear-gradient(135deg, var(--brand-accent-2), var(--brand-accent)), var(--brand-accent);
}
.module-row__content {
display: grid;
gap: 3px;
min-width: 0;
}
.module-row__action,
.auth-pill {
display: inline-flex;
align-items: center;
padding: 9px 12px;
border-radius: 999px;
color: #ffffff;
font-size: 0.86rem;
font-weight: 800;
background: linear-gradient(135deg, var(--brand-accent), var(--brand-accent-3));
}
.module-row__action::after {
content: "->";
margin-left: 8px;
}
.theme-switcher {
position: relative;
z-index: 1;
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-left: auto;
}
.theme-switcher label {
display: grid;
gap: 4px;
color: var(--muted);
font-size: 0.68rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.theme-switcher select,
.card select {
background: var(--surface-strong);
color: var(--text);
}
.theme-switcher select {
min-width: 118px;
border: 1px solid var(--line);
border-radius: 999px;
padding: 8px 30px 8px 11px;
font: inherit;
font-size: 0.86rem;
letter-spacing: 0;
text-transform: none;
}
.empty-state {
padding: 28px;
border-radius: 18px;
color: var(--muted);
line-height: 1.7;
}
.module-host-card {
position: relative;
overflow: hidden;
border-radius: 18px;
}
.reveal {
opacity: 0;
transform: translateY(18px);
animation: rise 480ms ease forwards;
}
@keyframes rise {
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 720px) {
.site-header {
align-items: flex-start;
flex-direction: column;
margin: 10px;
}
.header-nav {
width: 100%;
justify-content: flex-start;
}
.layout-body,
.module-subnav {
padding-left: 0;
padding-right: 0;
margin-left: 0;
margin-right: 0;
}
.main-content {
width: min(100% - 20px, 1680px);
}
.main-content:has(#mining-checker-app) {
width: 100%;
}
.module-host-card:has(#mining-checker-app) {
border-left: 0;
border-right: 0;
border-radius: 0;
}
.home-hero {
align-items: flex-start;
flex-wrap: wrap;
padding: 14px;
border-radius: 18px;
}
.theme-switcher {
width: 100%;
margin-left: 0;
}
.brand-mark {
width: 60px;
height: 60px;
border-radius: 16px;
}
.brand-mark img {
width: 49px;
height: 49px;
}
.module-row {
grid-template-columns: auto minmax(0, 1fr);
}
.module-row__action {
grid-column: 2;
justify-self: start;
padding: 7px 10px;
}
.section-head {
align-items: start;
flex-direction: column;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -28,6 +28,55 @@
});
})();
document.documentElement.classList.add('js');
function readThemePreference(key, fallback) {
try {
return localStorage.getItem(key) || fallback;
} catch (error) {
return fallback;
}
}
const themeMode = readThemePreference('nexus.theme', document.documentElement.dataset.theme || 'day');
const themeAccent = readThemePreference('nexus.accent', document.documentElement.dataset.accent || 'logo');
function applyTheme(mode, accent) {
const normalizedMode = ['day', 'night'].includes(mode) ? mode : 'day';
const normalizedAccent = ['logo', 'pink', 'cyan', 'orange', 'green'].includes(accent) ? accent : 'logo';
document.documentElement.dataset.theme = normalizedMode;
document.documentElement.dataset.accent = normalizedAccent;
try {
localStorage.setItem('nexus.theme', normalizedMode);
localStorage.setItem('nexus.accent', normalizedAccent);
} catch (error) {
// Ignore blocked storage; the current page still receives the theme.
}
}
applyTheme(themeMode, themeAccent);
const themeModeSelect = document.querySelector('[data-theme-mode]');
const themeAccentSelect = document.querySelector('[data-theme-accent]');
if (themeModeSelect) {
themeModeSelect.value = document.documentElement.dataset.theme;
themeModeSelect.addEventListener('change', () => {
applyTheme(themeModeSelect.value, document.documentElement.dataset.accent);
});
}
if (themeAccentSelect) {
themeAccentSelect.value = document.documentElement.dataset.accent;
themeAccentSelect.addEventListener('change', () => {
applyTheme(document.documentElement.dataset.theme, themeAccentSelect.value);
});
}
for (const element of document.querySelectorAll('[data-reveal]')) {
element.classList.add('reveal');
}
(() => {
const openBtn = document.querySelector('[data-debug-open]');
const modal = document.getElementById('debug-modal');

View File

@@ -1,6 +1,9 @@
<?php
declare(strict_types=1);
use Modules\MiningChecker\Support\ApiException as MiningApiException;
use Modules\MiningChecker\Support\DebugState as MiningDebugState;
// boot application (config, autoload, services)
require_once __DIR__ . '/../config/fileload.php';
@@ -8,6 +11,8 @@ require_once __DIR__ . '/../config/fileload.php';
$uriPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
$uriPath = preg_replace('~/{2,}~', '/', $uriPath);
$uriPath = trim($uriPath, '/');
$projectRoot = dirname(__DIR__);
$auth = app()->auth();
$isRetoolPath = ($uriPath === 'retool' || str_starts_with($uriPath, 'retool/'));
if (defined('APP_BASIC_AUTH') && APP_BASIC_AUTH && !$isRetoolPath) {
$authUser = getenv('STAGING_AUTH_USER') ?: 'staging';
@@ -27,9 +32,15 @@ $publicPaths = [
'auth/login',
'auth/callback',
'auth/logout',
'auth/keycloak/login',
'auth/keycloak/callback',
'auth/keycloak/logout',
'auth/me',
'module/pi_control/terminal_info',
];
if (defined('APP_AUTH_ENABLED') && APP_AUTH_ENABLED && !in_array($uriPath, $publicPaths, true)) {
$requiresGlobalAuth = in_array($uriPath, ['settings', 'users', 'modules', 'modules/install', 'debug'], true)
|| str_starts_with($uriPath, 'modules/setup/');
if (defined('APP_AUTH_ENABLED') && APP_AUTH_ENABLED && $requiresGlobalAuth && !in_array($uriPath, $publicPaths, true)) {
$user = auth_user();
if (!$user) {
header('Location: /auth/login', true, 302);
@@ -43,6 +54,139 @@ if (str_contains($uriPath, '..')) {
exit('Bad request');
}
if ($uriPath === 'auth/keycloak/login') {
$returnTo = (string)($_GET['return_to'] ?? '/');
$auth->login($returnTo);
}
if ($uriPath === 'auth/keycloak/callback') {
$uriPath = 'auth/callback';
}
if ($uriPath === 'auth/keycloak/logout') {
$uriPath = 'auth/logout';
}
if ($uriPath === 'auth/me') {
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'authenticated' => $auth->isAuthenticated(),
'user' => $auth->user(),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
if (preg_match('~^api/module-auth/([a-zA-Z0-9_-]+)$~', $uriPath, $moduleAuthMatches)) {
$moduleName = $moduleAuthMatches[1];
$moduleMeta = app()->modules()->get($moduleName);
if ($moduleMeta === null) {
http_response_code(404);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['error' => 'module_not_found'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
if (!$auth->isAuthenticated()) {
http_response_code(401);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['error' => 'auth_required'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
if (!$auth->canAccessModule($moduleMeta)) {
http_response_code(403);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['error' => 'forbidden'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
header('Content-Type: application/json; charset=utf-8');
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
echo json_encode(['data' => ($moduleMeta['auth'] ?? ['required' => false, 'users' => [], 'groups' => []])], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'PUT') {
$input = json_decode((string)file_get_contents('php://input'), true);
if (!is_array($input)) {
$input = [];
}
echo json_encode(['data' => app()->modules()->saveAuth($moduleName, $input)], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
http_response_code(405);
echo json_encode(['error' => 'method_not_allowed'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
if (preg_match('~^api/mining-checker(?:/(.*))?$~', $uriPath, $apiMatches)) {
$moduleMeta = app()->modules()->get('mining-checker') ?? ['auth' => ['required' => false]];
if (!$auth->canAccessModule($moduleMeta)) {
http_response_code($auth->isAuthenticated() ? 403 : 401);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'error' => $auth->isAuthenticated() ? 'forbidden' : 'auth_required',
'login_url' => '/auth/login',
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
require_once $projectRoot . '/modules/mining-checker/bootstrap.php';
try {
(new Modules\MiningChecker\Api\Router($projectRoot . '/modules/mining-checker'))->handle($apiMatches[1] ?? '');
} catch (MiningApiException $exception) {
$debugTrace = MiningDebugState::export();
http_response_code($exception->statusCode());
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'error' => $exception->getMessage(),
'context' => $exception->context(),
'debug' => $debugTrace !== [] ? $debugTrace : null,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
} catch (Throwable $exception) {
$debugTrace = MiningDebugState::export();
http_response_code(500);
header('Content-Type: application/json; charset=utf-8');
echo json_encode([
'error' => 'Unerwarteter Mining-Checker Fehler.',
'context' => ['message' => $exception->getMessage()],
'debug' => $debugTrace !== [] ? $debugTrace : null,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
}
if (preg_match('~^module-assets/([a-zA-Z0-9_-]+)/(.*)$~', $uriPath, $assetMatches)) {
$module = $assetMatches[1];
$relativeAssetPath = trim($assetMatches[2], '/');
if ($relativeAssetPath === '' || str_contains($relativeAssetPath, '..')) {
http_response_code(400);
exit('Bad request');
}
$assetFile = $projectRoot . '/modules/' . $module . '/assets/' . $relativeAssetPath;
if (!is_file($assetFile)) {
http_response_code(404);
exit('Asset not found');
}
$extension = strtolower(pathinfo($assetFile, PATHINFO_EXTENSION));
$contentType = match ($extension) {
'css' => 'text/css; charset=utf-8',
'js' => 'application/javascript; charset=utf-8',
'json' => 'application/json; charset=utf-8',
'png' => 'image/png',
'jpg', 'jpeg' => 'image/jpeg',
'webp' => 'image/webp',
'svg' => 'image/svg+xml',
default => 'application/octet-stream',
};
header('Content-Type: ' . $contentType);
readfile($assetFile);
exit;
}
// Basispfad fuer Landingpages
$pagesBase = realpath(__DIR__ . '/../partials/landingpages') ?: (__DIR__ . '/../partials/landingpages');
$page404 = $pagesBase . '/errorpages/404.php';
@@ -68,7 +212,15 @@ if (str_starts_with($uriPath, 'modules/install')) {
} elseif (preg_match('~^module/([a-zA-Z0-9_-]+)(?:/(.+))?$~', $uriPath, $m)) {
$module = $m[1];
$page = isset($m[2]) && $m[2] !== '' ? trim($m[2], '/') : 'index';
$moduleMeta = app()->modules()->get($module);
if ($moduleMeta !== null) {
$auth->requireModuleAccess($moduleMeta);
}
$modulePage = app()->modules()->resolvePage($module, $page);
$moduleBootstrap = $projectRoot . '/modules/' . $module . '/bootstrap.php';
if (is_file($moduleBootstrap)) {
require_once $moduleBootstrap;
}
if ($modulePage) {
$target = $modulePage;
} else {