add new guide service

This commit is contained in:
team2
2026-02-26 20:40:42 +01:00
parent 12f2a48f88
commit a68f5182e4
11 changed files with 1017 additions and 12 deletions

View File

@@ -153,9 +153,6 @@ Sehr sauber:
- reload steuerbar - reload steuerbar
- PID-Handling robust - PID-Handling robust
Empfehlung:
Health-Check Endpoint ergänzen.
--- ---
# 7. Tag-System # 7. Tag-System
@@ -280,9 +277,8 @@ Sehr gut umgesetzt.
# 14. Identifizierte Schwachstellen # 14. Identifizierte Schwachstellen
1. Vollständiger FAISS-Rebuild bei jedem Ingest 1. Vollständiger FAISS-Rebuild bei jedem Ingest
2. Kein Vector-Service Health-Endpoint 2. Keine automatische Index-Korruptionsprüfung
3. Keine automatische Index-Korruptionsprüfung 3. Kein Backpressure bei mehreren Ingest-Jobs
4. Kein Backpressure bei mehreren Ingest-Jobs
Keine strukturelle Schwäche. Keine strukturelle Schwäche.
@@ -297,7 +293,6 @@ Keine strukturelle Schwäche.
- ChunkWriteService - ChunkWriteService
- VectorRebuildService - VectorRebuildService
- Health-Endpoint im Vector-Service
- Timeout-Absicherung beim Reload - Timeout-Absicherung beim Reload
## Phase C ## Phase C

View File

@@ -11,7 +11,9 @@
"doctrine/doctrine-bundle": "^2.18", "doctrine/doctrine-bundle": "^2.18",
"doctrine/doctrine-migrations-bundle": "^3.7", "doctrine/doctrine-migrations-bundle": "^3.7",
"doctrine/orm": "^3.6", "doctrine/orm": "^3.6",
"league/commonmark": "^2.8",
"smalot/pdfparser": "^2.12", "smalot/pdfparser": "^2.12",
"symfony/asset": "7.4.*",
"symfony/console": "^7.4", "symfony/console": "^7.4",
"symfony/dotenv": "^7.4", "symfony/dotenv": "^7.4",
"symfony/flex": "^2", "symfony/flex": "^2",

581
composer.lock generated
View File

@@ -4,8 +4,83 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "ffc8b627f07f3eb413d757a4d80af3f2", "content-hash": "bf516574b65f7c2abdc053c964f769aa",
"packages": [ "packages": [
{
"name": "dflydev/dot-access-data",
"version": "v3.0.3",
"source": {
"type": "git",
"url": "https://github.com/dflydev/dflydev-dot-access-data.git",
"reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f",
"reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^0.12.42",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.3",
"scrutinizer/ocular": "1.6.0",
"squizlabs/php_codesniffer": "^3.5",
"vimeo/psalm": "^4.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Dflydev\\DotAccessData\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Dragonfly Development Inc.",
"email": "info@dflydev.com",
"homepage": "http://dflydev.com"
},
{
"name": "Beau Simensen",
"email": "beau@dflydev.com",
"homepage": "http://beausimensen.com"
},
{
"name": "Carlos Frutos",
"email": "carlos@kiwing.it",
"homepage": "https://github.com/cfrutos"
},
{
"name": "Colin O'Dell",
"email": "colinodell@gmail.com",
"homepage": "https://www.colinodell.com"
}
],
"description": "Given a deep data structure, access data by dot notation.",
"homepage": "https://github.com/dflydev/dflydev-dot-access-data",
"keywords": [
"access",
"data",
"dot",
"notation"
],
"support": {
"issues": "https://github.com/dflydev/dflydev-dot-access-data/issues",
"source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3"
},
"time": "2024-07-08T12:26:09+00:00"
},
{ {
"name": "doctrine/collections", "name": "doctrine/collections",
"version": "2.6.0", "version": "2.6.0",
@@ -1119,6 +1194,195 @@
}, },
"time": "2026-02-08T16:21:46+00:00" "time": "2026-02-08T16:21:46+00:00"
}, },
{
"name": "league/commonmark",
"version": "2.8.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/commonmark.git",
"reference": "4efa10c1e56488e658d10adf7b7b7dcd19940bfb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/4efa10c1e56488e658d10adf7b7b7dcd19940bfb",
"reference": "4efa10c1e56488e658d10adf7b7b7dcd19940bfb",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"league/config": "^1.1.1",
"php": "^7.4 || ^8.0",
"psr/event-dispatcher": "^1.0",
"symfony/deprecation-contracts": "^2.1 || ^3.0",
"symfony/polyfill-php80": "^1.16"
},
"require-dev": {
"cebe/markdown": "^1.0",
"commonmark/cmark": "0.31.1",
"commonmark/commonmark.js": "0.31.1",
"composer/package-versions-deprecated": "^1.8",
"embed/embed": "^4.4",
"erusev/parsedown": "^1.0",
"ext-json": "*",
"github/gfm": "0.29.0",
"michelf/php-markdown": "^1.4 || ^2.0",
"nyholm/psr7": "^1.5",
"phpstan/phpstan": "^1.8.2",
"phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0",
"scrutinizer/ocular": "^1.8.1",
"symfony/finder": "^5.3 | ^6.0 | ^7.0",
"symfony/process": "^5.4 | ^6.0 | ^7.0",
"symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0",
"unleashedtech/php-coding-standard": "^3.1.1",
"vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0"
},
"suggest": {
"symfony/yaml": "v2.3+ required if using the Front Matter extension"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "2.9-dev"
}
},
"autoload": {
"psr-4": {
"League\\CommonMark\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Colin O'Dell",
"email": "colinodell@gmail.com",
"homepage": "https://www.colinodell.com",
"role": "Lead Developer"
}
],
"description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)",
"homepage": "https://commonmark.thephpleague.com",
"keywords": [
"commonmark",
"flavored",
"gfm",
"github",
"github-flavored",
"markdown",
"md",
"parser"
],
"support": {
"docs": "https://commonmark.thephpleague.com/",
"forum": "https://github.com/thephpleague/commonmark/discussions",
"issues": "https://github.com/thephpleague/commonmark/issues",
"rss": "https://github.com/thephpleague/commonmark/releases.atom",
"source": "https://github.com/thephpleague/commonmark"
},
"funding": [
{
"url": "https://www.colinodell.com/sponsor",
"type": "custom"
},
{
"url": "https://www.paypal.me/colinpodell/10.00",
"type": "custom"
},
{
"url": "https://github.com/colinodell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/league/commonmark",
"type": "tidelift"
}
],
"time": "2025-11-26T21:48:24+00:00"
},
{
"name": "league/config",
"version": "v1.2.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/config.git",
"reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3",
"reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3",
"shasum": ""
},
"require": {
"dflydev/dot-access-data": "^3.0.1",
"nette/schema": "^1.2",
"php": "^7.4 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^1.8.2",
"phpunit/phpunit": "^9.5.5",
"scrutinizer/ocular": "^1.8.1",
"unleashedtech/php-coding-standard": "^3.1",
"vimeo/psalm": "^4.7.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.2-dev"
}
},
"autoload": {
"psr-4": {
"League\\Config\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Colin O'Dell",
"email": "colinodell@gmail.com",
"homepage": "https://www.colinodell.com",
"role": "Lead Developer"
}
],
"description": "Define configuration arrays with strict schemas and access values with dot notation",
"homepage": "https://config.thephpleague.com",
"keywords": [
"array",
"config",
"configuration",
"dot",
"dot-access",
"nested",
"schema"
],
"support": {
"docs": "https://config.thephpleague.com/",
"issues": "https://github.com/thephpleague/config/issues",
"rss": "https://github.com/thephpleague/config/releases.atom",
"source": "https://github.com/thephpleague/config"
},
"funding": [
{
"url": "https://www.colinodell.com/sponsor",
"type": "custom"
},
{
"url": "https://www.paypal.me/colinpodell/10.00",
"type": "custom"
},
{
"url": "https://github.com/colinodell",
"type": "github"
}
],
"time": "2022-12-11T20:36:23+00:00"
},
{ {
"name": "monolog/monolog", "name": "monolog/monolog",
"version": "3.10.0", "version": "3.10.0",
@@ -1222,6 +1486,164 @@
], ],
"time": "2026-01-02T08:56:05+00:00" "time": "2026-01-02T08:56:05+00:00"
}, },
{
"name": "nette/schema",
"version": "v1.3.5",
"source": {
"type": "git",
"url": "https://github.com/nette/schema.git",
"reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/schema/zipball/f0ab1a3cda782dbc5da270d28545236aa80c4002",
"reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002",
"shasum": ""
},
"require": {
"nette/utils": "^4.0",
"php": "8.1 - 8.5"
},
"require-dev": {
"nette/phpstan-rules": "^1.0",
"nette/tester": "^2.6",
"phpstan/extension-installer": "^1.4@stable",
"phpstan/phpstan": "^2.1.39@stable",
"tracy/tracy": "^2.8"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.3-dev"
}
},
"autoload": {
"psr-4": {
"Nette\\": "src"
},
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause",
"GPL-2.0-only",
"GPL-3.0-only"
],
"authors": [
{
"name": "David Grudl",
"homepage": "https://davidgrudl.com"
},
{
"name": "Nette Community",
"homepage": "https://nette.org/contributors"
}
],
"description": "📐 Nette Schema: validating data structures against a given Schema.",
"homepage": "https://nette.org",
"keywords": [
"config",
"nette"
],
"support": {
"issues": "https://github.com/nette/schema/issues",
"source": "https://github.com/nette/schema/tree/v1.3.5"
},
"time": "2026-02-23T03:47:12+00:00"
},
{
"name": "nette/utils",
"version": "v4.1.3",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
"reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/utils/zipball/bb3ea637e3d131d72acc033cfc2746ee893349fe",
"reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe",
"shasum": ""
},
"require": {
"php": "8.2 - 8.5"
},
"conflict": {
"nette/finder": "<3",
"nette/schema": "<1.2.2"
},
"require-dev": {
"jetbrains/phpstorm-attributes": "^1.2",
"nette/phpstan-rules": "^1.0",
"nette/tester": "^2.5",
"phpstan/extension-installer": "^1.4@stable",
"phpstan/phpstan": "^2.1@stable",
"tracy/tracy": "^2.9"
},
"suggest": {
"ext-gd": "to use Image",
"ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()",
"ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()",
"ext-json": "to use Nette\\Utils\\Json",
"ext-mbstring": "to use Strings::lower() etc...",
"ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.1-dev"
}
},
"autoload": {
"psr-4": {
"Nette\\": "src"
},
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause",
"GPL-2.0-only",
"GPL-3.0-only"
],
"authors": [
{
"name": "David Grudl",
"homepage": "https://davidgrudl.com"
},
{
"name": "Nette Community",
"homepage": "https://nette.org/contributors"
}
],
"description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.",
"homepage": "https://nette.org",
"keywords": [
"array",
"core",
"datetime",
"images",
"json",
"nette",
"paginator",
"password",
"slugify",
"string",
"unicode",
"utf-8",
"utility",
"validation"
],
"support": {
"issues": "https://github.com/nette/utils/issues",
"source": "https://github.com/nette/utils/tree/v4.1.3"
},
"time": "2026-02-13T03:05:33+00:00"
},
{ {
"name": "psr/cache", "name": "psr/cache",
"version": "3.0.0", "version": "3.0.0",
@@ -1523,6 +1945,79 @@
}, },
"time": "2026-01-08T08:04:04+00:00" "time": "2026-01-08T08:04:04+00:00"
}, },
{
"name": "symfony/asset",
"version": "v7.4.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/asset.git",
"reference": "d944ae87e4697af05aadeacfc5e603c3c18ef4fb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/asset/zipball/d944ae87e4697af05aadeacfc5e603c3c18ef4fb",
"reference": "d944ae87e4697af05aadeacfc5e603c3c18ef4fb",
"shasum": ""
},
"require": {
"php": ">=8.2"
},
"conflict": {
"symfony/http-foundation": "<6.4"
},
"require-dev": {
"symfony/http-client": "^6.4|^7.0|^8.0",
"symfony/http-foundation": "^6.4|^7.0|^8.0",
"symfony/http-kernel": "^6.4|^7.0|^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Asset\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Manages URL generation and versioning of web assets such as CSS stylesheets, JavaScript files and image files",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/asset/tree/v7.4.6"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-02-09T09:33:46+00:00"
},
{ {
"name": "symfony/cache", "name": "symfony/cache",
"version": "v7.4.3", "version": "v7.4.3",
@@ -3959,6 +4454,90 @@
], ],
"time": "2024-12-23T08:48:59+00:00" "time": "2024-12-23T08:48:59+00:00"
}, },
{
"name": "symfony/polyfill-php80",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
"reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ion Bazan",
"email": "ion.bazan@gmail.com"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-01-02T08:10:11+00:00"
},
{ {
"name": "symfony/polyfill-php83", "name": "symfony/polyfill-php83",
"version": "v1.33.0", "version": "v1.33.0",

View File

@@ -264,7 +264,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* formats?: array<string, string|list<scalar|null|Param>>, * formats?: array<string, string|list<scalar|null|Param>>,
* }, * },
* assets?: bool|array{ // Assets configuration * assets?: bool|array{ // Assets configuration
* enabled?: bool|Param, // Default: false * enabled?: bool|Param, // Default: true
* strict_mode?: bool|Param, // Throw an exception if an entry is missing from the manifest.json. // Default: false * strict_mode?: bool|Param, // Throw an exception if an entry is missing from the manifest.json. // Default: false
* version_strategy?: scalar|null|Param, // Default: null * version_strategy?: scalar|null|Param, // Default: null
* version?: scalar|null|Param, // Default: null * version?: scalar|null|Param, // Default: null

View File

@@ -0,0 +1,162 @@
/* =========================================================
Markdown Guide Styling (Admin Dark)
========================================================= */
.markdown-content {
font-size: 0.95rem;
line-height: 1.6;
color: #e2e2e2;
}
/* ---------------------------------------------------------
Headings
--------------------------------------------------------- */
.markdown-content h1,
.markdown-content h2,
.markdown-content h3,
.markdown-content h4 {
font-weight: 600;
margin-top: 2rem;
margin-bottom: 1rem;
}
.markdown-content h1 {
font-size: 1.8rem;
border-bottom: 1px solid #333;
padding-bottom: 0.5rem;
}
.markdown-content h2 {
font-size: 1.4rem;
border-bottom: 1px solid #222;
padding-bottom: 0.4rem;
}
.markdown-content h3 {
font-size: 1.15rem;
}
/* ---------------------------------------------------------
Paragraphs
--------------------------------------------------------- */
.markdown-content p {
margin-bottom: 1rem;
}
/* ---------------------------------------------------------
Lists
--------------------------------------------------------- */
.markdown-content ul,
.markdown-content ol {
padding-left: 1.5rem;
margin-bottom: 1rem;
}
.markdown-content li {
margin-bottom: 0.4rem;
}
/* ---------------------------------------------------------
Links
--------------------------------------------------------- */
.markdown-content a {
color: #0dcaf0;
text-decoration: none;
}
.markdown-content a:hover {
text-decoration: underline;
}
/* ---------------------------------------------------------
Inline Code
--------------------------------------------------------- */
.markdown-content code {
background: #1e1e1e;
color: #ffc107;
padding: 3px 6px;
border-radius: 4px;
font-size: 0.85rem;
}
/* ---------------------------------------------------------
Code Blocks
--------------------------------------------------------- */
.markdown-content pre {
background: #111;
padding: 16px;
border-radius: 6px;
overflow-x: auto;
border: 1px solid #222;
margin-bottom: 1.5rem;
}
.markdown-content pre code {
background: transparent;
padding: 0;
color: #e2e2e2;
}
/* ---------------------------------------------------------
Tables
--------------------------------------------------------- */
.markdown-content table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1.5rem;
font-size: 0.9rem;
}
.markdown-content th {
background: #1a1a1a;
border: 1px solid #333;
padding: 8px;
text-align: left;
font-weight: 600;
}
.markdown-content td {
border: 1px solid #333;
padding: 8px;
}
.markdown-content tr:nth-child(even) {
background: #141414;
}
/* ---------------------------------------------------------
Blockquotes
--------------------------------------------------------- */
.markdown-content blockquote {
border-left: 4px solid #0dcaf0;
padding-left: 1rem;
margin: 1rem 0;
color: #cfcfcf;
background: #151515;
}
/* ---------------------------------------------------------
Horizontal Rule
--------------------------------------------------------- */
.markdown-content hr {
border: none;
border-top: 1px solid #333;
margin: 2rem 0;
}
.guide-item:hover {
background-color: #151515 !important;
}
.guide-item .fw-semibold {
letter-spacing: 0.2px;
}

View File

@@ -233,4 +233,44 @@ button:disabled {
width: 100px; width: 100px;
height: 100px; height: 100px;
position: absolute; position: absolute;
}
/* =========================================================
Admin Dark Base Fix
========================================================= */
body {
color: #e2e2e2;
}
.card.bg-black,
.card.bg-dark {
color: #e2e2e2;
}
.list-group-item.bg-black,
.list-group-item.bg-dark {
color: #e2e2e2;
}
.list-group-item.bg-black a,
.list-group-item.bg-dark a {
color: #e2e2e2;
}
.text-muted {
color: #9aa0a6 !important;
}
.guide-item {
color: #e2e2e2 !important;
}
.guide-item small {
color: #9aa0a6 !important;
}
.card.bg-black .card-body
{
color: #e2e2e2 !important;
} }

View File

@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
namespace App\Controller\Admin;
use App\Service\MarkdownRenderer;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
#[Route('/admin/guides')]
final class GuideController extends AbstractController
{
private string $guidePath;
public function __construct(ParameterBagInterface $params)
{
$this->guidePath = rtrim(
$params->get('kernel.project_dir'),
'/'
);
}
#[Route('', name: 'admin_guides_index')]
public function index(): Response
{
if (!is_dir($this->guidePath)) {
return $this->render('admin/guides/index.html.twig', [
'guides' => [],
]);
}
$files = glob($this->guidePath . '/*.md') ?: [];
$guides = [];
foreach ($files as $file) {
$slug = basename($file, '.md');
if (!$this->isValidSlug($slug)) {
continue;
}
$content = file_get_contents($file) ?: '';
$title = $this->extractTitleFromMarkdown($content)
?? $this->humanizeSlug($slug);
$guides[] = [
'slug' => $slug,
'title' => $title,
];
}
usort($guides, fn ($a, $b) => strcasecmp($a['title'], $b['title']));
return $this->render('admin/guides/index.html.twig', [
'guides' => $guides,
]);
}
#[Route('/{slug}', name: 'admin_guides_view')]
public function view(string $slug, MarkdownRenderer $renderer): Response
{
if (!$this->isValidSlug($slug)) {
throw $this->createNotFoundException();
}
$file = $this->guidePath . '/' . $slug . '.md';
if (!is_file($file)) {
throw $this->createNotFoundException();
}
$content = file_get_contents($file) ?: '';
return $this->render('admin/guides/view.html.twig', [
'slug' => $slug,
'title' => $this->extractTitleFromMarkdown($content)
?? $this->humanizeSlug($slug),
'html' => $renderer->render($content),
]);
}
// =========================================================
// Helper
// =========================================================
private function extractTitleFromMarkdown(string $content): ?string
{
if (preg_match('/^#\s+(.+)$/m', $content, $matches)) {
return trim($matches[1]);
}
return null;
}
private function humanizeSlug(string $slug): string
{
return ucfirst(str_replace('-', ' ', $slug));
}
private function isValidSlug(string $slug): bool
{
return (bool) preg_match('/^[a-zA-Z0-9\-_]+$/', $slug);
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Service;
use League\CommonMark\MarkdownConverter;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
final class MarkdownRenderer
{
private MarkdownConverter $converter;
public function __construct()
{
$config = [
'html_input' => 'strip',
'allow_unsafe_links' => false,
];
$environment = new Environment($config);
// Core Markdown
$environment->addExtension(new CommonMarkCoreExtension());
// GitHub Flavored Markdown (Tables, Strikethrough, Autolinks, Task Lists)
$environment->addExtension(new GithubFlavoredMarkdownExtension());
$this->converter = new MarkdownConverter($environment);
}
public function render(string $markdown): string
{
return (string) $this->converter->convert($markdown);
}
}

View File

@@ -4,9 +4,11 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Admin{% endblock %}</title> <title>{% block title %}Admin{% endblock %}</title>
{% block stylesheets %}
<link href="/assets/styles/bootstrap.min.css" rel="stylesheet"/> <link href="/assets/styles/bootstrap.min.css" rel="stylesheet"/>
<link rel="stylesheet" href="/assets/styles/base.css"> <link rel="stylesheet" href="/assets/styles/base.css">
<link rel="stylesheet" href="{{ asset('/assets/styles/admin-markdown.css') }}">
{% endblock %}
</head> </head>
<body class="bg-dark text-light"> <body class="bg-dark text-light">
@@ -111,6 +113,15 @@
href="{{ path('admin_model_config_list') }}"> href="{{ path('admin_model_config_list') }}">
Modell-Generierung Modell-Generierung
</a> </a>
<hr class="border-secondary">
<div class="text-info text-uppercase small mb-2">
System-Guiide
</div>
<a class="nav-link text-light {% if route starts with 'admin_model_config' %}active fw-bold{% endif %}"
href="{{ path('admin_guides_index') }}">
How-To & Leitfäden
</a>
</nav> </nav>
</aside> </aside>

View File

@@ -0,0 +1,54 @@
{% extends 'admin/base.html.twig' %}
{% block title %}Guides{% endblock %}
{% block body %}
<div class="d-flex justify-content-between align-items-center mb-4 text-light">
<div>
<h1 class="h3 mb-0">How-To & Leitfäden</h1>
<small class="text-muted">
{{ guides|length }} Guide{{ guides|length == 1 ? '' : 's' }} verfügbar
</small>
</div>
</div>
<div class="card bg-black border-secondary shadow-sm text-light">
<div class="card-body p-0">
{% if guides is empty %}
<div class="p-4 text-center text-light">
Keine Guides vorhanden.
</div>
{% else %}
<div class="list-group list-group-flush">
{% for guide in guides %}
<a href="{{ path('admin_guides_view', {slug: guide.slug}) }}"
class="list-group-item list-group-item-action bg-black border-secondary guide-item">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="fw-semibold text-light">
{{ guide.title }}
</div>
<small class="text-light">
{{ guide.slug }}
</small>
</div>
<span class="text-muted small">
</span>
</div>
</a>
{% endfor %}
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% extends 'admin/base.html.twig' %}
{% block title %}Guide{% endblock %}
{% block body %}
<a href="{{ path('admin_guides_index') }}" class="btn btn-sm btn-outline-secondary mb-3">
← Zurück
</a>
<div class="card bg-black border-secondary">
<div class="card-body markdown-content">
{{ html|raw }}
</div>
</div>
{% endblock %}