Some checks failed
The 2026-05-18 cache-busting system was only catching a fraction of
includes because:
1. FE-024 anti-pattern only matched `'<module>_static'` mount names
(e.g. `'core_static'`, `'billing_static'`). The bare `'static'`
mount — which is what every persona base.html uses for shared JS,
CSS, and Tailwind output — never matched.
2. The rule explicitly excluded `base.html` files, which are exactly
where most of the shared JS/CSS includes live.
User reported only a handful of files had `?v=` in their Network tab.
Swept 5 persona base.html + 15 standalone templates (login/register/
forgot/reset password, error pages, onboarding, invitation-accept,
admin module-info/config, etc.) — 53 url_for('static', ...) refs for
.js/.css converted to static_v(request, 'static', ...).
Then tightened FE-024:
- Added an anti-pattern for the bare `'static'` mount.
- Dropped `base.html` from exceptions (kept `partials/`).
Re-running the validator: same 126-warning baseline, 0 FE-024 hits.
Now every deploy flips the `?v=<sha>` query string on every shared
asset; browsers refetch automatically without a hard refresh.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
184 lines
13 KiB
HTML
184 lines
13 KiB
HTML
{# app/templates/admin/select-platform.html #}
|
|
{# standalone - This template does not extend base.html because it's shown before platform selection #}
|
|
<!DOCTYPE html>
|
|
<html :class="{ 'dark': dark }" x-data="selectPlatform()" lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Select Platform - Admin Panel</title>
|
|
<link href="{{ static_v(request, 'static', path='shared/fonts/inter.css') }}" rel="stylesheet" />
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
|
|
<link rel="stylesheet" href="{{ static_v(request, 'static', path='admin/css/tailwind.output.css') }}" />
|
|
<style>[x-cloak] { display: none !important; }</style>
|
|
</head>
|
|
<body x-cloak>
|
|
<div class="flex items-center min-h-screen p-6 bg-gray-50 dark:bg-gray-900">
|
|
<div class="flex-1 h-full max-w-xl mx-auto overflow-hidden bg-white rounded-lg shadow-xl dark:bg-gray-800">
|
|
<div class="flex flex-col overflow-y-auto">
|
|
<div class="flex items-center justify-center p-6 sm:p-12">
|
|
<div class="w-full">
|
|
<!-- Header -->
|
|
<div class="text-center mb-8">
|
|
<h1 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
|
|
Select Platform
|
|
</h1>
|
|
<p class="mt-2 text-gray-600 dark:text-gray-400">
|
|
Choose a platform to manage
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div x-show="loading" class="flex justify-center py-8">
|
|
<span x-html="$icon('spinner', 'h-8 w-8 text-purple-600')"></span>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div x-show="error" x-cloak class="mb-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
|
<p class="text-red-700 dark:text-red-400" x-text="error"></p>
|
|
</div>
|
|
|
|
<!-- Global Mode Card (Super Admins Only) -->
|
|
<div x-show="isSuperAdmin && !loading" x-cloak class="mb-3">
|
|
<button
|
|
@click="deselectPlatform()"
|
|
:disabled="selecting"
|
|
class="w-full flex items-center p-4 rounded-lg border-2 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
:class="!currentPlatformId
|
|
? 'bg-green-50 dark:bg-green-900/20 border-green-500 ring-2 ring-green-200 dark:ring-green-800'
|
|
: 'bg-gray-50 dark:bg-gray-700 border-transparent hover:border-green-500 hover:bg-green-50 dark:hover:bg-green-900/20'"
|
|
>
|
|
<!-- Globe Icon -->
|
|
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-green-100 dark:bg-green-900/30 flex items-center justify-center mr-4">
|
|
<span x-html="$icon('globe-alt', 'w-6 h-6 text-green-600 dark:text-green-400')"></span>
|
|
</div>
|
|
|
|
<!-- Info -->
|
|
<div class="flex-1 text-left">
|
|
<h3 class="text-lg font-medium text-gray-800 dark:text-gray-200">Global Mode (All Platforms)</h3>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">Access all modules across all platforms</p>
|
|
</div>
|
|
|
|
<!-- Checkmark for active -->
|
|
<div class="flex-shrink-0 ml-4" x-show="!currentPlatformId">
|
|
<span x-html="$icon('check-circle', 'w-6 h-6 text-green-600 dark:text-green-400')"></span>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Platform List -->
|
|
<div x-show="!loading && platforms.length > 0" x-cloak class="space-y-3">
|
|
<template x-for="platform in platforms" :key="platform.id">
|
|
<button
|
|
@click="choosePlatform(platform)"
|
|
:disabled="selecting"
|
|
class="w-full flex items-center p-4 rounded-lg border-2 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
:class="isCurrentPlatform(platform)
|
|
? 'bg-purple-50 dark:bg-purple-900/20 border-purple-500 ring-2 ring-purple-200 dark:ring-purple-800'
|
|
: 'bg-gray-50 dark:bg-gray-700 border-transparent hover:border-purple-500 hover:bg-purple-50 dark:hover:bg-purple-900/20'"
|
|
>
|
|
<!-- Platform Icon/Logo -->
|
|
<div class="flex-shrink-0 w-12 h-12 rounded-lg bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center mr-4">
|
|
<template x-if="platform.logo">
|
|
<img :src="platform.logo" :alt="platform.name" class="w-8 h-8 object-contain">
|
|
</template>
|
|
<template x-if="!platform.logo">
|
|
<span class="text-xl font-bold text-purple-600 dark:text-purple-400" x-text="platform.code.charAt(0).toUpperCase()"></span>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Platform Info -->
|
|
<div class="flex-1 text-left">
|
|
<h3 class="text-lg font-medium text-gray-800 dark:text-gray-200" x-text="platform.name"></h3>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400" x-text="platform.code"></p>
|
|
</div>
|
|
|
|
<!-- Checkmark for active / Arrow for others -->
|
|
<div class="flex-shrink-0 ml-4">
|
|
<template x-if="isCurrentPlatform(platform)">
|
|
<span x-html="$icon('check-circle', 'w-6 h-6 text-purple-600 dark:text-purple-400')"></span>
|
|
</template>
|
|
<template x-if="!isCurrentPlatform(platform)">
|
|
<span x-html="$icon('chevron-right', 'w-5 h-5 text-gray-400')"></span>
|
|
</template>
|
|
</div>
|
|
</button>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- No Platforms -->
|
|
<div x-show="!loading && !isSuperAdmin && platforms.length === 0" x-cloak class="text-center py-8">
|
|
<span x-html="$icon('exclamation', 'mx-auto h-12 w-12 text-gray-400')"></span>
|
|
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-gray-200">No platforms assigned</h3>
|
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Contact your administrator to get platform access.</p>
|
|
</div>
|
|
|
|
<!-- Logout Link -->
|
|
<div class="mt-8 text-center">
|
|
<button
|
|
@click="logout()"
|
|
class="text-sm text-gray-600 dark:text-gray-400 hover:text-purple-600 dark:hover:text-purple-400 underline"
|
|
>
|
|
Sign out and use a different account
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Language & Theme Toggle -->
|
|
<div class="mt-4 flex justify-center items-center gap-2">
|
|
<!-- Language selector -->
|
|
<div class="relative" x-data="{ langOpen: false, currentLang: '{{ request.state.language|default('en') }}', async setLang(lang) { this.currentLang = lang; await fetch('/api/v1/platform/language/set', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({language: lang}) }); window.location.reload(); } }">
|
|
<button
|
|
@click="langOpen = !langOpen"
|
|
@click.outside="langOpen = false"
|
|
class="inline-flex items-center gap-1 p-2 text-gray-500 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none"
|
|
aria-label="Change language"
|
|
>
|
|
<span x-html="$icon('globe-alt', 'w-5 h-5')"></span>
|
|
<span class="text-xs font-semibold uppercase" x-text="currentLang"></span>
|
|
</button>
|
|
<div
|
|
x-show="langOpen"
|
|
x-cloak
|
|
x-transition
|
|
class="absolute bottom-full mb-2 left-1/2 -translate-x-1/2 w-40 bg-white dark:bg-gray-700 rounded-lg shadow-lg border border-gray-100 dark:border-gray-600 py-1 z-50"
|
|
>
|
|
<button @click="setLang('en'); langOpen = false" class="flex items-center gap-3 w-full px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600">
|
|
<span class="font-semibold text-xs w-5">EN</span> English
|
|
</button>
|
|
<button @click="setLang('fr'); langOpen = false" class="flex items-center gap-3 w-full px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600">
|
|
<span class="font-semibold text-xs w-5">FR</span> Français
|
|
</button>
|
|
<button @click="setLang('de'); langOpen = false" class="flex items-center gap-3 w-full px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600">
|
|
<span class="font-semibold text-xs w-5">DE</span> Deutsch
|
|
</button>
|
|
<button @click="setLang('lb'); langOpen = false" class="flex items-center gap-3 w-full px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600">
|
|
<span class="font-semibold text-xs w-5">LB</span> Lëtzebuergesch
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Dark mode toggle -->
|
|
<button
|
|
@click="toggleDarkMode()"
|
|
class="p-2 text-gray-500 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none"
|
|
aria-label="Toggle dark mode"
|
|
>
|
|
<span x-show="!dark" x-html="$icon('moon', 'w-5 h-5')"></span>
|
|
<span x-show="dark" x-html="$icon('sun', 'w-5 h-5')"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scripts -->
|
|
<script defer src="{{ static_v(request, 'static', path='shared/js/log-config.js') }}"></script>
|
|
<script defer src="{{ static_v(request, 'static', path='shared/js/utils.js') }}"></script>
|
|
<script defer src="{{ static_v(request, 'static', path='shared/js/api-client.js') }}"></script>
|
|
<script defer src="{{ static_v(request, 'static', path='shared/js/icons.js') }}"></script>
|
|
<script defer src="{{ static_v(request, 'tenancy_static', path='admin/js/select-platform.js') }}"></script>
|
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js"></script>
|
|
</body>
|
|
</html>
|