diff --git a/.gitignore b/.gitignore index cbf1875..d2879a5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ /public/bundles/ /var/ /vendor/ +/.superpowers/ +/.playwright-mcp/ ###< symfony/framework-bundle ### ###> phpunit/phpunit ### diff --git a/public/admin.css b/public/admin.css new file mode 100644 index 0000000..ff600e2 --- /dev/null +++ b/public/admin.css @@ -0,0 +1,322 @@ +:root { + color-scheme: light; + --admin-bg: #f6f7f9; + --admin-panel: #ffffff; + --admin-panel-soft: #eef1f5; + --admin-text: #172033; + --admin-muted: #647083; + --admin-border: #dfe4ec; + --admin-border-soft: #edf0f4; + --admin-primary: #172033; + --admin-primary-hover: #26344d; + --admin-danger: #b42318; + --admin-danger-soft: #fff1f0; + --admin-success: #0d7a4f; + --admin-success-soft: #ecfdf3; +} + +* { + box-sizing: border-box; +} + +body.admin-page, +body.admin-auth { + margin: 0; + min-height: 100vh; + background: var(--admin-bg); + color: var(--admin-text); + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-size: 14px; + line-height: 1.5; +} + +a { + color: inherit; +} + +.admin-shell { + width: min(1440px, calc(100% - 48px)); + margin: 0 auto; + padding: 32px 0; +} + +.auth-shell { + display: grid; + min-height: 100vh; + place-items: center; + padding: 24px; +} + +.admin-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-bottom: 18px; +} + +.admin-toolbar h1, +.auth-card h1 { + margin: 0; + color: var(--admin-text); + font-size: 28px; + font-weight: 700; + letter-spacing: 0; +} + +.eyebrow { + margin: 0 0 4px; + color: var(--admin-muted); + font-size: 11px; + font-weight: 700; + letter-spacing: 0; + text-transform: uppercase; +} + +.toolbar-actions, +.row-actions { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; +} + +.row-actions form { + margin: 0; +} + +.button { + display: inline-flex; + min-height: 36px; + align-items: center; + justify-content: center; + gap: 6px; + padding: 8px 11px; + border: 1px solid transparent; + border-radius: 6px; + font: inherit; + font-weight: 650; + line-height: 1.2; + text-decoration: none; + cursor: pointer; +} + +.button-primary { + background: var(--admin-primary); + color: #ffffff; +} + +.button-primary:hover { + background: var(--admin-primary-hover); +} + +.button-secondary, +.button-ghost { + border-color: var(--admin-border); + background: #ffffff; + color: var(--admin-text); +} + +.button-secondary:hover, +.button-ghost:hover { + background: var(--admin-panel-soft); +} + +.button-danger { + border-color: #f3b4ae; + background: var(--admin-danger-soft); + color: var(--admin-danger); +} + +.button-danger:hover { + background: #ffe3e0; +} + +.table-panel, +.admin-card { + overflow: hidden; + border: 1px solid var(--admin-border); + border-radius: 8px; + background: var(--admin-panel); + box-shadow: 0 16px 40px rgba(23, 32, 51, 0.06); +} + +.admin-table { + width: 100%; + border-collapse: collapse; +} + +.admin-table th { + background: var(--admin-panel-soft); + color: var(--admin-muted); + font-size: 11px; + font-weight: 800; + letter-spacing: 0; + text-align: left; + text-transform: uppercase; +} + +.admin-table th, +.admin-table td { + padding: 11px 12px; + border-bottom: 1px solid var(--admin-border-soft); + vertical-align: top; +} + +.admin-table tbody tr:last-child td { + border-bottom: 0; +} + +.admin-table code { + color: #243149; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; + font-size: 12px; +} + +.text-muted { + color: var(--admin-muted); +} + +.error-cell { + max-width: 260px; + color: var(--admin-danger); + word-break: break-word; +} + +.status-badge { + display: inline-flex; + min-width: 42px; + justify-content: center; + padding: 3px 8px; + border-radius: 999px; + font-size: 12px; + font-weight: 700; +} + +.status-success { + background: var(--admin-success-soft); + color: var(--admin-success); +} + +.status-muted { + background: var(--admin-panel-soft); + color: var(--admin-muted); +} + +.empty-state { + color: var(--admin-muted); + text-align: center; +} + +.alert { + margin: 0 0 14px; + padding: 10px 12px; + border: 1px solid var(--admin-border); + border-radius: 6px; + background: #ffffff; + font-weight: 600; +} + +.alert-success { + border-color: #b7e4ce; + background: var(--admin-success-soft); + color: var(--admin-success); +} + +.alert-error { + border-color: #f3b4ae; + background: var(--admin-danger-soft); + color: var(--admin-danger); +} + +.auth-card { + width: min(100%, 380px); + padding: 24px; +} + +.admin-form-shell { + width: min(760px, calc(100% - 48px)); +} + +.admin-form { + display: grid; + gap: 14px; +} + +.admin-form > div { + display: grid; + gap: 6px; +} + +.admin-form label { + color: var(--admin-text); + font-weight: 700; +} + +.admin-form input[type="text"], +.admin-form input[type="password"], +.admin-form input[type="url"] { + width: 100%; + min-height: 38px; + padding: 8px 10px; + border: 1px solid var(--admin-border); + border-radius: 6px; + background: #ffffff; + color: var(--admin-text); + font: inherit; +} + +.admin-form input[type="text"]:focus, +.admin-form input[type="password"]:focus, +.admin-form input[type="url"]:focus { + border-color: var(--admin-primary); + outline: 3px solid rgba(23, 32, 51, 0.12); +} + +.admin-form input[type="checkbox"] { + width: 16px; + height: 16px; + accent-color: var(--admin-primary); +} + +.admin-form .help-text, +.admin-form .form-text, +.admin-form small { + color: var(--admin-muted); + font-size: 12px; +} + +.admin-form ul { + margin: 0; + padding-left: 18px; + color: var(--admin-danger); +} + +@media (max-width: 900px) { + .admin-shell, + .admin-form-shell { + width: min(100% - 24px, 760px); + padding: 18px 0; + } + + .admin-toolbar { + align-items: flex-start; + flex-direction: column; + } + + .toolbar-actions { + width: 100%; + } + + .toolbar-actions .button { + flex: 1 1 auto; + } + + .table-panel { + overflow-x: auto; + } + + .admin-table { + min-width: 980px; + } +} diff --git a/templates/admin/base.html.twig b/templates/admin/base.html.twig new file mode 100644 index 0000000..f063f1b --- /dev/null +++ b/templates/admin/base.html.twig @@ -0,0 +1,8 @@ +{% extends 'base.html.twig' %} + +{% block stylesheets %} + {{ parent() }} + +{% endblock %} + +{% block body_class %}admin-page{% endblock %} diff --git a/templates/admin/dashboard.html.twig b/templates/admin/dashboard.html.twig index 13ede38..6394981 100644 --- a/templates/admin/dashboard.html.twig +++ b/templates/admin/dashboard.html.twig @@ -1,65 +1,78 @@ -{% extends 'base.html.twig' %} +{% extends 'admin/base.html.twig' %} {% block title %}Admin{% endblock %} {% block body %} -
-

Mappings

-

- Nouveau mapping - Configuration -

+
+
+
+

Administration

+

Mappings

+
+ +
{% for message in app.flashes('success') %} -

{{ message }}

+

{{ message }}

{% endfor %} {% for message in app.flashes('error') %} -

{{ message }}

+

{{ message }}

{% endfor %} - - - - - - - - - - - - - - - - {% for mapping in mappings %} +
+
Chemin publicDépôtRéf.FichierActifDernière synchroDernier succèsErreurActions
+ - - - - - - - - - + + + + + + + + + - {% else %} - - - - {% endfor %} - -
{{ mapping.publicPath }}{{ mapping.repositoryUrl }}{{ mapping.gitRef }}{{ mapping.repositoryFilePath }}{{ mapping.active ? 'oui' : 'non' }}{{ mapping.lastSyncStatus }}{{ mapping.lastSuccessfulSyncAt ? mapping.lastSuccessfulSyncAt|date('Y-m-d H:i:s') : '' }}{{ mapping.lastSyncError }} - Modifier -
- - -
-
- - -
-
Chemin publicDépôtRéf.FichierActifDernière synchroDernier succèsErreurActions
Aucun mapping.
+ + + {% for mapping in mappings %} + + {{ mapping.publicPath }} + {{ mapping.repositoryUrl }} + {{ mapping.gitRef }} + {{ mapping.repositoryFilePath }} + + + {{ mapping.active ? 'oui' : 'non' }} + + + {{ mapping.lastSyncStatus ?: 'jamais' }} + {{ mapping.lastSuccessfulSyncAt ? mapping.lastSuccessfulSyncAt|date('Y-m-d H:i:s') : '' }} + {{ mapping.lastSyncError }} + +
+ Modifier +
+ + +
+
+ + +
+
+ + + {% else %} + + Aucun mapping. + + {% endfor %} + + +
{% endblock %} diff --git a/templates/admin/login.html.twig b/templates/admin/login.html.twig index 19a6450..95816f8 100644 --- a/templates/admin/login.html.twig +++ b/templates/admin/login.html.twig @@ -1,19 +1,23 @@ -{% extends 'base.html.twig' %} +{% extends 'admin/base.html.twig' %} {% block title %}Admin login{% endblock %} +{% block body_class %}admin-auth{% endblock %} {% block body %} -
-

Admin login

+
+
+

Administration

+

Connexion

- {% if error %} -

{{ error.messageKey|trans(error.messageData, 'security') }}

- {% endif %} + {% if error %} +

{{ error.messageKey|trans(error.messageData, 'security') }}

+ {% endif %} - {{ form_start(loginForm) }} - {{ form_row(loginForm.username) }} - {{ form_row(loginForm.password) }} - - {{ form_end(loginForm) }} + {{ form_start(loginForm, { attr: { class: 'admin-form' } }) }} + {{ form_row(loginForm.username) }} + {{ form_row(loginForm.password) }} + + {{ form_end(loginForm) }} +
{% endblock %} diff --git a/templates/admin/mapping_form.html.twig b/templates/admin/mapping_form.html.twig index 9f17cc2..7846b8f 100644 --- a/templates/admin/mapping_form.html.twig +++ b/templates/admin/mapping_form.html.twig @@ -1,19 +1,27 @@ -{% extends 'base.html.twig' %} +{% extends 'admin/base.html.twig' %} {% block title %}{{ title }}{% endblock %} {% block body %} -
-

{{ title }}

+
+
+
+

Administration

+

{{ title }}

+
+ Retour aux mappings +
- {{ form_start(form) }} +
+ {{ form_start(form, { attr: { class: 'admin-form' } }) }} {{ form_row(form.publicPath) }} {{ form_row(form.repositoryUrl) }} {{ form_row(form.gitRef) }} {{ form_row(form.repositoryFilePath) }} {{ form_row(form.accessToken) }} {{ form_row(form.active) }} - + {{ form_end(form) }} +
{% endblock %} diff --git a/templates/admin/settings.html.twig b/templates/admin/settings.html.twig index 1954592..db7013e 100644 --- a/templates/admin/settings.html.twig +++ b/templates/admin/settings.html.twig @@ -1,17 +1,25 @@ -{% extends 'base.html.twig' %} +{% extends 'admin/base.html.twig' %} {% block title %}Configuration{% endblock %} {% block body %} -
-

Configuration

+
+
+
+

Administration

+

Configuration

+
+ Retour aux mappings +
- {{ form_start(form) }} +
+ {{ form_start(form, { attr: { class: 'admin-form' } }) }} {{ form_row(form.rootRedirectUrl, { label: 'URL de redirection de /', help: 'Laisser vide pour rediriger vers /admin.' }) }} - + {{ form_end(form) }} +
{% endblock %} diff --git a/templates/base.html.twig b/templates/base.html.twig index c6fd7ad..8a58628 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -17,7 +17,7 @@ {% endif %} - + {% block body %}{% endblock %} diff --git a/tests/Controller/Admin/AuthControllerTest.php b/tests/Controller/Admin/AuthControllerTest.php index 6dfc32d..90cb690 100644 --- a/tests/Controller/Admin/AuthControllerTest.php +++ b/tests/Controller/Admin/AuthControllerTest.php @@ -35,4 +35,13 @@ final class AuthControllerTest extends DatabaseWebTestCase self::assertResponseRedirects('/admin'); } + + public function testLoginUsesUtilityAdminLayout(): void + { + $this->client->request('GET', '/admin/login'); + + self::assertSelectorExists('body.admin-auth'); + self::assertSelectorExists('.admin-card'); + self::assertSelectorExists('.button-primary'); + } } diff --git a/tests/Controller/Admin/ScriptMappingControllerTest.php b/tests/Controller/Admin/ScriptMappingControllerTest.php index 7491b90..b834274 100644 --- a/tests/Controller/Admin/ScriptMappingControllerTest.php +++ b/tests/Controller/Admin/ScriptMappingControllerTest.php @@ -36,6 +36,27 @@ final class ScriptMappingControllerTest extends DatabaseWebTestCase self::assertResponseRedirects('/admin/login'); } + public function testDashboardUsesUtilityAdminLayout(): void + { + $this->loginAsAdmin(); + + $mapping = (new ScriptMapping()) + ->setPublicPath('mcp/graylog/install.sh') + ->setRepositoryUrl('https://forge.lclr.dev/AI/graylog-mcp.git') + ->setGitRef('main') + ->setRepositoryFilePath('install.sh') + ->setActive(true); + $this->entityManager->persist($mapping); + $this->entityManager->flush(); + + $this->client->request('GET', '/admin'); + + self::assertSelectorExists('body.admin-page'); + self::assertSelectorExists('.admin-toolbar'); + self::assertSelectorExists('.admin-table'); + self::assertSelectorExists('.status-badge'); + } + public function testAdminCanCreateMappingWithNormalizedPaths(): void { $this->loginAsAdmin();