Compare commits

..

24 Commits

Author SHA1 Message Date
030b0c48ae dsfsdf
All checks were successful
Deploy / deploy (push) Successful in 16s
2026-05-17 01:22:54 +02:00
8461507cfb dssadf
All checks were successful
Deploy / deploy (push) Successful in 17s
2026-05-17 01:12:27 +02:00
16c19cd6ff adasd
All checks were successful
Deploy / deploy (push) Successful in 17s
2026-05-17 01:07:33 +02:00
70e82cae7b sadsdf
All checks were successful
Deploy / deploy (push) Successful in 18s
2026-05-17 00:54:41 +02:00
f5bb37f96e dsfsdf
All checks were successful
Deploy / deploy (push) Successful in 44s
2026-05-17 00:47:09 +02:00
8647bff20f adasd
All checks were successful
Deploy / deploy (push) Successful in 18s
2026-05-15 23:37:09 +02:00
e7708831d0 ui rack
All checks were successful
Deploy / deploy (push) Successful in 15s
2026-05-15 23:23:35 +02:00
b2dcff9cb6 yssadasd
All checks were successful
Deploy / deploy (push) Successful in 16s
2026-05-15 23:08:24 +02:00
838f4d7eec asdasd
All checks were successful
Deploy / deploy (push) Successful in 19s
2026-05-15 23:01:08 +02:00
07989f9fba xxx
All checks were successful
Deploy / deploy (push) Successful in 15s
2026-05-15 22:49:39 +02:00
d914a660d8 c0de.it 2
All checks were successful
Deploy / deploy (push) Successful in 16s
2026-05-15 22:46:38 +02:00
752d33e38a code it los
All checks were successful
Deploy / deploy (push) Successful in 15s
2026-05-15 22:27:49 +02:00
98dd388fda asdasd
All checks were successful
Deploy / deploy (push) Successful in 14s
2026-05-15 22:15:31 +02:00
c7d96bec85 Yyy
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-05-07 23:33:43 +02:00
316175e158 Delete .gitkeep 2025-12-02 01:19:07 +01:00
33313cb490 Add new directory 2025-12-02 01:17:32 +01:00
0990d2c757 Add new directory 2025-12-02 01:17:20 +01:00
2f1e12659b Add new directory 2025-12-02 01:17:04 +01:00
16fce3e71f Add new directory 2025-12-02 01:16:52 +01:00
a8ccdc3fff Add new directory 2025-12-02 01:16:41 +01:00
07c0f8eb99 Add new directory 2025-12-02 01:16:13 +01:00
27a14d5751 Add new directory 2025-12-02 01:16:02 +01:00
ec07dc6ee0 Add new directory 2025-12-02 01:14:56 +01:00
f934a4b832 Add new directory 2025-12-02 01:14:43 +01:00
41 changed files with 4442 additions and 83 deletions

115
.gitea/workflows/deploy.yml Normal file
View File

@@ -0,0 +1,115 @@
name: Deploy
on:
push:
branches:
- main
- develop
env:
BASE_DIRS: "src public api partials tools"
CONFIG_BASE_DIR: "config"
jobs:
deploy:
runs-on: private-server
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install lftp
shell: sh
run: |
if command -v lftp >/dev/null 2>&1; then
echo "✅ lftp bereits installiert"
elif command -v apk >/dev/null 2>&1; then
apk add --no-cache lftp ca-certificates
elif command -v apt-get >/dev/null 2>&1; then
apt-get update
apt-get install -y lftp ca-certificates
else
echo "❌ Kein unterstützter Paketmanager gefunden"
exit 1
fi
- name: Set environment
run: |
if [ "${{ gitea.ref_name }}" = "main" ]; then
echo "TARGET_PATH=${{ vars.FTP_PATH_PROD }}" >> "$GITHUB_ENV"
echo "CONFIG_ENV_DIR=config/prod" >> "$GITHUB_ENV"
elif [ "${{ gitea.ref_name }}" = "develop" ]; then
echo "TARGET_PATH=${{ vars.FTP_PATH_STAGING }}" >> "$GITHUB_ENV"
echo "CONFIG_ENV_DIR=config/staging" >> "$GITHUB_ENV"
else
echo "Unsupported branch"
exit 1
fi
- name: Debug workspace
run: |
echo "📂 CI Workspace:"
pwd
ls -la
- name: Deploy via FTPS
run: |
set -e
echo "🚀 Deploy to ${TARGET_PATH}"
VALID_DIRS=""
for d in $BASE_DIRS; do
if [ -d "$d" ]; then
VALID_DIRS="$VALID_DIRS $d"
else
echo "⚠️ Überspringe fehlendes Verzeichnis: $d"
fi
done
if [ -z "$VALID_DIRS" ]; then
echo "❌ Kein deploybares Verzeichnis gefunden."
exit 1
fi
for d in $VALID_DIRS; do
echo "🔁 ${d}/ → ${TARGET_PATH}${d}/"
lftp -u "${{ secrets.FTP_USER }}","${{ secrets.FTP_PASSWORD }}" "${{ vars.FTP_HOST }}" -e "
set ftp:ssl-force true;
set ftp:passive-mode true;
set ftp:ssl-protect-data true;
set ssl:verify-certificate no;
mirror -R --delete --exclude .gitkeep ${d}/ ${TARGET_PATH}${d}/;
bye
" || exit 1
done
if [ -d "$CONFIG_BASE_DIR" ] && [ -d "$CONFIG_ENV_DIR" ]; then
echo "🧩 Baue gemischtes Config-Verzeichnis"
rm -rf .ci_config_deploy
mkdir -p .ci_config_deploy
for f in ${CONFIG_BASE_DIR}/*.php; do
[ -f "$f" ] && cp "$f" .ci_config_deploy/
done
cp -R ${CONFIG_ENV_DIR}/. .ci_config_deploy/
echo "🔁 config → ${TARGET_PATH}${CONFIG_BASE_DIR}/"
lftp -u "${{ secrets.FTP_USER }}","${{ secrets.FTP_PASSWORD }}" "${{ vars.FTP_HOST }}" -e "
set ftp:ssl-force true;
set ftp:passive-mode true;
set ftp:ssl-protect-data true;
set ssl:verify-certificate no;
lcd .ci_config_deploy;
mirror -R --delete --exclude .gitkeep ./ ${TARGET_PATH}${CONFIG_BASE_DIR}/;
bye
" || exit 1
else
echo "⚠️ Config-Deploy übersprungen: ${CONFIG_BASE_DIR} oder ${CONFIG_ENV_DIR} fehlt"
fi
echo "✅ Deploy abgeschlossen"

0
.gitlab-ci.yml Normal file → Executable file
View File

5
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"recommendations": [
"openai.chatgpt"
]
}

107
README.md
View File

@@ -1,93 +1,34 @@
# emailtemplate # code.it Rack Planner
Web-based planning tool for `10"` and `19"` racks with a visual editor, component library, plugin system, bill of materials generation, and optional cable-length estimation.
The goal is to make rack planning available online for technicians, integrators, and planners who need a practical tool without having to use heavyweight CAD software for everyday layout work.
## Getting started ## Core goals
To make it easy for you to get started with GitLab, here's a list of recommended next steps. - Visual planning interface for `10"` and `19"` racks
- Drag-and-drop placement and repositioning of components
- Extensible component system via plugins
- Automatic bill of materials generation
- Optional cable-length calculation based on installed positions
- Internet-ready architecture for multi-user access
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! ## Documentation
## Add your files - [Product concept](docs/rack-planner/product-concept.md)
- [MVP scope](docs/rack-planner/mvp.md)
- [System architecture](docs/rack-planner/architecture.md)
- [Data model](docs/rack-planner/data-model.md)
- [Plugin specification](docs/rack-planner/plugin-spec.md)
- [Cable length calculation](docs/rack-planner/cable-length.md)
- [Roadmap](docs/rack-planner/roadmap.md)
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files ## Product direction
- [ ] [Add files using the command line](https://docs.gitlab.com/topics/git/add_files/#add-files-to-a-git-repository) or push an existing Git repository with the following command:
``` The first release should optimize for speed, clarity, and usability:
cd existing_repo
git remote add origin https://gitlab.int.kusche.berlin/emailtemplate/emailtemplate.git
git branch -M main
git push -uf origin main
```
## Integrate with your tools - 2D rack planning before full 3D support
- structured component metadata before open CAD ingestion
- reliable BOM and validation before advanced collaboration features
- [ ] [Set up project integrations](https://gitlab.int.kusche.berlin/emailtemplate/emailtemplate/-/settings/integrations) This keeps the first public version realistic while leaving room for manufacturer catalogs, better exports, and richer cable planning later.
## Collaborate with your team
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Set auto-merge](https://docs.gitlab.com/user/project/merge_requests/auto_merge/)
## Test and Deploy
Use the built-in continuous integration in GitLab.
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/)
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
***
# Editing this README
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Suggestions for a good README
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
## Name
Choose a self-explaining name for your project.
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
## License
For open source projects, say how it is licensed.
## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.

0
api/.gitkeep Executable file
View File

20
api/index.php Normal file
View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
require_once dirname(__DIR__) . '/config/fileload.php';
$action = $_GET['action'] ?? 'bootstrap';
if ($action === 'bootstrap') {
echo json_encode(app_bootstrap_payload(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
exit;
}
http_response_code(404);
echo json_encode([
'error' => 'Unknown action',
'action' => $action,
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);

0
config/.gitkeep Executable file
View File

10
config/config.php Normal file
View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
$host = $_SERVER['HTTP_HOST'] ?? '';
$isCli = PHP_SAPI === 'cli';
$isStagingHost = is_string($host) && str_starts_with($host, 'staging.');
$envDir = $isStagingHost ? __DIR__ . '/staging' : __DIR__ . '/prod';
require_once $envDir . '/config.php';

19
config/db.php Normal file
View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
$pdo = null;
if (!defined('APP_ENV')) {
return;
}
$dbLoader = __DIR__ . '/' . APP_ENV . '/db.php';
// The prototype should run without a database. Explicit opt-in keeps the
// current concept environment usable while the data layer is still evolving.
$enableDb = getenv('APP_ENABLE_DB') === '1';
if ($enableDb && is_file($dbLoader)) {
require_once $dbLoader;
}

11
config/domainbase.php Normal file
View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
if (!defined('APP_PREFIX')) {
define('APP_PREFIX', 'c0de');
}
if (!defined('APP_DOMAIN_BASE')) {
define('APP_DOMAIN_BASE', 'c0de.it');
}

121
config/fileload.php Executable file
View File

@@ -0,0 +1,121 @@
<?php
// -----------------------------------------------------------
// 0) Umgebung / Domains / Error-Level laden
// → Diese Datei DEFINIERT die Konstanten wie
// APP_COOKIE_PREFIX, APP_COOKIE_DOMAIN, APP_ENV etc.
// -----------------------------------------------------------
require_once __DIR__ . "/config.php";
// Diese Werte später ins Template schieben:
$GLOBALS['app_env'] = APP_ENV;
$GLOBALS['app_base_url'] = APP_URL_PRIMARY;
$GLOBALS['app_api_base'] = $apiBaseUrl;
// -----------------------------------------------------------
// set cookie / session parameters
// -----------------------------------------------------------
if (!defined('CUSTOM_PREFIX')) {
define('CUSTOM_PREFIX', APP_PREFIX);
}
if(!defined('APP_COOKIE_PREFIX')) {
if(APP_ENV==="staging"){
define('APP_COOKIE_PREFIX', APP_PREFIX.'_stg'.'_');
} else
{
define('APP_COOKIE_PREFIX', APP_PREFIX.'_');
}
}
if (!defined('APP_COOKIE_DOMAIN')) {
// Fallback: aktuelle Domain des Hosts
define('APP_COOKIE_DOMAIN', '.'.APP_DOMAIN_PRIMARY);
define('APP_PRIMARY_DOMAIN', APP_DOMAIN_PRIMARY);
}
if (!defined('APP_CLIENT_COOKIE_LIFETIME')) {
define('APP_CLIENT_COOKIE_LIFETIME', 365 * 24 * 60 * 60); // 1 Jahr
}
// Einheitliche Cookie-Namen (projektübergreifend steuerbar)
$sessionCookieName = APP_COOKIE_PREFIX . 'session';
$clientCookieName = APP_COOKIE_PREFIX . 'client';
// -----------------------------------------------------------
// 1) PHP-Session starten
// -----------------------------------------------------------
if (php_sapi_name() !== 'cli') {
if (session_status() === PHP_SESSION_NONE) {
session_name($sessionCookieName);
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => APP_COOKIE_DOMAIN ?: '',
'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
}
}
// -----------------------------------------------------------
// 2) Persistente Client-ID (für Tracking über Besuche hinweg)
// -----------------------------------------------------------
if (php_sapi_name() !== 'cli') {
$clientId = $_COOKIE[$clientCookieName] ?? null;
// Erwartet wird: 64 Hex-Zeichen (32 Bytes)
if (
!is_string($clientId) ||
$clientId === '' ||
!preg_match('/^[a-f0-9]{64}$/', $clientId)
) {
// neue ID erzeugen
try {
$clientId = bin2hex(random_bytes(32)); // 32 bytes → 64 hex
} catch (Throwable $e) {
$clientId = bin2hex(openssl_random_pseudo_bytes(32));
}
$cookieOpts = [
'expires' => time() + APP_CLIENT_COOKIE_LIFETIME,
'path' => '/',
'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
'httponly' => false, // JS darf es lesen, wenn erwünscht
'samesite' => 'Lax',
];
if (!empty(APP_COOKIE_DOMAIN)) {
$cookieOpts['domain'] = APP_COOKIE_DOMAIN;
}
setcookie($clientCookieName, $clientId, $cookieOpts);
$_COOKIE[$clientCookieName] = $clientId;
}
// global verfügbar machen (NEUER NAME!)
$GLOBALS['cookie_client_id'] = $clientId;
}
// -----------------------------------------------------------
// 3) Sprachlogik laden (bleibt sinnvoll zentral)
// -----------------------------------------------------------
require_once __DIR__ . '/i18n.php';
// -----------------------------------------------------------
// 4) Rest des Systems laden (DB, Funktionen, Hilfs-Libs)
// -----------------------------------------------------------
require_once __DIR__ . "/db.php";
require_once __DIR__ . '/../src/functions.php';

397
config/i18n.php Executable file
View File

@@ -0,0 +1,397 @@
<?php
// config/i18n.php
// Zentrale Sprachlogik für das gesamte Projekt
if (session_status() !== PHP_SESSION_ACTIVE) {
@session_start();
}
/**
* Liest die meta-Infos einer Sprachdatei.
* Erwartet Struktur:
* {
* "meta": { "code": "de", "label": "Deutsch", "flag": "🇩🇪", "enabled": true },
* ...
* }
*
* Gibt NULL zurück, wenn:
* - Datei nicht lesbar
* - JSON ungültig
* - kein meta vorhanden
* - meta.enabled existiert und NICHT true ist
*/
function app_i18n_load_language_meta_from_file(string $file): ?array
{
$json = @file_get_contents($file);
if ($json === false) {
return null;
}
$data = json_decode($json, true);
if (!is_array($data)) {
return null;
}
$meta = $data['meta'] ?? null;
if (!is_array($meta)) {
return null;
}
// Nur aktive Sprachen (enabled === true)
if (array_key_exists('enabled', $meta) && !$meta['enabled']) {
return null;
}
// Code aus meta, Fallback: Dateiname
$code = '';
if (!empty($meta['code'])) {
$code = strtolower(substr((string)$meta['code'], 0, 5));
} else {
$base = basename($file, '.json');
$code = strtolower($base);
}
// Auf 2-Buchstaben-Codes normalisieren (de-DE → de)
if (strlen($code) > 2) {
$code = substr($code, 0, 2);
}
if ($code === '') {
return null;
}
$label = isset($meta['label']) && $meta['label'] !== ''
? (string)$meta['label']
: strtoupper($code);
$flag = isset($meta['flag']) ? (string)$meta['flag'] : '';
return [
'code' => $code,
'label' => $label,
'flag' => $flag,
];
}
/**
* Alle verfügbaren Sprachen aus /public/assets/i18n/*.json ermitteln.
* Verfügbar = JSON mit meta.enabled === true.
* EN wird garantiert hinzugefügt (Fallback), falls nicht gefunden.
*/
function app_i18n_detect_available_languages(): array
{
$baseDir = realpath(__DIR__ . '/../public/assets/i18n');
if ($baseDir === false) {
// Wenn gar kein Verzeichnis da ist: minimaler EN-Fallback
return [
'en' => [
'code' => 'en',
'label' => 'English',
'flag' => '',
],
];
}
$files = glob($baseDir . '/*.json') ?: [];
$langs = [];
foreach ($files as $file) {
$meta = app_i18n_load_language_meta_from_file($file);
if ($meta === null) {
continue;
}
$code = $meta['code'];
// Erste gültige Definition pro Code gewinnt
if (!isset($langs[$code])) {
$langs[$code] = $meta;
}
}
// EN muss immer vorhanden sein (laut deiner Vorgabe)
if (!isset($langs['en'])) {
// Versuch: gibt es eine en.json, auch wenn enabled=false?
foreach ($files as $file) {
$base = strtolower(basename($file, '.json'));
if ($base === 'en') {
$json = @file_get_contents($file);
$data = json_decode($json, true);
$meta = is_array($data['meta'] ?? null) ? $data['meta'] : [];
$label = isset($meta['label']) && $meta['label'] !== ''
? (string)$meta['label']
: 'English';
$flag = isset($meta['flag']) ? (string)$meta['flag'] : '';
$langs['en'] = [
'code' => 'en',
'label' => $label,
'flag' => $flag,
];
break;
}
}
}
// Wenn immer noch kein EN → minimaler Stub
if (!isset($langs['en'])) {
$langs['en'] = [
'code' => 'en',
'label' => 'English',
'flag' => '',
];
}
ksort($langs);
return $langs;
}
/**
* Browsersprache aus HTTP_ACCEPT_LANGUAGE extrahieren (2-Buchstaben),
* aber nur, wenn sie in $available existiert.
*/
function app_i18n_detect_browser_lang(array $available): ?string
{
$header = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '';
if ($header === '') {
return null;
}
$parts = explode(',', $header);
foreach ($parts as $part) {
$part = trim($part);
if ($part === '') {
continue;
}
$code = strtolower(substr($part, 0, 2)); // "de-DE" → "de"
if (isset($available[$code])) {
return $code;
}
}
return null;
}
/**
* Aktuelle Sprache bestimmen:
* 1) ?lang=xx (wenn in $available)
* 2) Browsersprache (wenn in $available)
* 3) Fallback "en"
*/
function app_i18n_resolve_current_lang(array $available): string
{
// 1) URL-Parameter ?lang=xx
if (!empty($_GET['lang'])) {
$param = strtolower(substr($_GET['lang'], 0, 2));
if (isset($available[$param])) {
return $param;
}
}
// 2) Browsersprache
$browser = app_i18n_detect_browser_lang($available);
if ($browser !== null) {
return $browser;
}
// 3) Standard EN
if (isset($available['en'])) {
return 'en';
}
// Sicherheitsfallback: erste verfügbare Sprache
$keys = array_keys($available);
return $keys[0] ?? 'en';
}
// -----------------------------------------------------
// Bootstrap ausführen
// -----------------------------------------------------
$availableLangs = app_i18n_detect_available_languages();
$currentLang = app_i18n_resolve_current_lang($availableLangs);
// Global bereitstellen
$GLOBALS['availableLangs'] = $availableLangs;
$GLOBALS['lang'] = $currentLang;
// Optional in Session merken (muss nicht, schadet aber auch nicht)
$_SESSION['lang'] = $currentLang;
/**
* Frontend-Config für JS (usbConfig.i18n)
* → benutzt direkt die Meta-Daten aus den JSONs (code, label, flag)
*/
function app_i18n_get_frontend_config(): array
{
return [
'current' => $GLOBALS['lang'] ?? 'en',
'available' => $GLOBALS['availableLangs'] ?? [],
];
}
/**
* Komplette JSON-Struktur für eine Sprache laden.
* Nutzt einfachen Request-Cache, damit pro Sprache nur einmal von Platte gelesen wird.
*/
function app_i18n_load_lang_json(string $lang): array
{
static $cache = [];
$lang = strtolower(substr($lang, 0, 5));
if (isset($cache[$lang])) {
return $cache[$lang];
}
$baseDir = realpath(__DIR__ . '/../public/assets/i18n');
if ($baseDir === false) {
$cache[$lang] = [];
return $cache[$lang];
}
$path = $baseDir . '/' . $lang . '.json';
if (!is_file($path)) {
// Fallback: en.json, falls vorhanden
$fallback = $baseDir . '/en.json';
if (is_file($fallback)) {
$json = @file_get_contents($fallback);
$data = json_decode($json, true);
$cache[$lang] = is_array($data) ? $data : [];
return $cache[$lang];
}
$cache[$lang] = [];
return $cache[$lang];
}
$json = @file_get_contents($path);
$data = json_decode($json, true);
$cache[$lang] = is_array($data) ? $data : [];
return $cache[$lang];
}
/**
* Aus einem Label einen stabilen i18n-Key für Nav-Anker bauen.
* Beispiel: "So funktioniert USBCheck!" -> "nav_so_funktioniert_usbcheck"
*/
function app_i18n_make_anchor_key(string $label): string
{
// HTML-Entities entfernen (z. B. &amp;)
$decoded = html_entity_decode($label, ENT_QUOTES | ENT_HTML5, 'UTF-8');
// Kleinbuchstaben
$decoded = mb_strtolower($decoded, 'UTF-8');
// Alles, was kein a-z oder 0-9 ist, durch Unterstrich ersetzen
$key = preg_replace('/[^a-z0-9]+/u', '_', $decoded);
// Mehrfache Unterstriche trimmen
$key = trim($key, '_');
if ($key === '') {
$key = 'item';
}
// Prefix, damit klar ist, dass es Navigationskeys sind
return 'nav_' . $key;
}
/**
* Nav-Anker für eine Seite aus der Sprachdatei holen.
*
* Haupt-Variante im JSON:
*
* "pages": {
* "landing": {
* "anchors": {
* "how": "So funktioniert USBCheck",
* "problem": "Warum gefälschte USB-Sticks gefährlich sind",
* "features": "Funktionen",
* "security": "Sicherheit",
* "faq": "FAQ"
* }
* }
* }
*
* Optional explizit:
* "anchors": {
* "how": { "label": "So funktioniert USBCheck", "i18n": "nav_how" },
* "faq": { "i18n": "nav_faq" }
* }
*
* Rückgabe-Format:
* [
* [ 'href' => '#how', 'label' => 'So funktioniert USBCheck', 'i18n' => 'nav_so_funktioniert_usbcheck' ],
* [ 'href' => '#faq', 'label' => '', 'i18n' => 'nav_faq' ],
* ]
*/
function app_get_nav_anchors(string $pageKey): array
{
$lang = $GLOBALS['lang'] ?? 'en';
$data = app_i18n_load_lang_json($lang);
$cfg = $data['pages'][$pageKey]['anchors'] ?? null;
if (!is_array($cfg)) {
return [];
}
$anchors = [];
foreach ($cfg as $id => $value) {
$id = trim((string)$id);
if ($id === '') {
continue;
}
$href = '#' . $id;
$label = '';
$i18n = '';
if (is_string($value)) {
// String IMMER als Label übernehmen
$labelTrim = trim($value);
if ($labelTrim === '') {
continue;
}
$label = $labelTrim;
// i18n-Key automatisch aus dem Label ableiten
$i18n = app_i18n_make_anchor_key($labelTrim);
} elseif (is_array($value)) {
// Explizite Variante:
// "how": { "label": "...", "i18n": "nav_how" }
if (!empty($value['label'])) {
$label = trim((string)$value['label']);
}
if (!empty($value['i18n'])) {
$i18n = trim((string)$value['i18n']);
}
if ($label === '' && $i18n === '') {
continue;
}
// Wenn Label gesetzt, aber kein i18n: automatisch generieren
if ($label !== '' && $i18n === '') {
$i18n = app_i18n_make_anchor_key($label);
}
} else {
// Weder String noch Array → ignorieren
continue;
}
$anchors[] = [
'href' => $href,
'label' => $label,
'i18n' => $i18n,
];
}
return $anchors;
}

0
config/prod/.gitkeep Executable file
View File

33
config/prod/config.php Executable file
View File

@@ -0,0 +1,33 @@
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . "/domaindata.php";
// Umgebung (optional, aber hilfreich für Debugging / Logik)
define('APP_ENV', 'prod'); // oder 'prod', 'local', ...
if (!defined('ASSET_VERSION')) {
define('ASSET_VERSION', '2024-11-22'); // oder deine aktuelle Version
}
// Domain-Konfiguration (kann pro Umgebung angepasst werden)
if (!defined('APP_DOMAIN_PRIMARY')) {
define('APP_DOMAIN_PRIMARY', APP_DOMAIN_NAME);
}
if (!defined('APP_URL_PRIMARY')) {
define('APP_URL_PRIMARY', 'https://' . APP_DOMAIN_PRIMARY);
}
if (!defined('APP_DOMAIN_FAKECHECK')) {
define('APP_DOMAIN_FAKECHECK', 'ismyusbfake.com');
}
if (!defined('APP_URL_FAKECHECK')) {
define('APP_URL_FAKECHECK', 'https://' . APP_DOMAIN_FAKECHECK);
}
// Matomo Einstellungen
define('MATOMO_URL', 'https://matomo.my-statistics.info/');
define('MATOMO_ENABLED', true);
define('MATOMO_SITE_ID', 7);
$env = 'prod';
$baseUrl = 'https://' . APP_DOMAIN_PRIMARY;
$apiBaseUrl = 'https://api.' . APP_DOMAIN_PRIMARY;

28
config/prod/db.php Executable file
View File

@@ -0,0 +1,28 @@
<?php
// config/db.php
declare(strict_types=1);
$DB_HOST = 'localhost';
$DB_NAME = 'd0455ede';
$DB_USER = 'd0455ede';
$DB_PASS = 'fF8PhxfCibdLBrSxowIo'; // anpassen
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO(
"mysql:host={$DB_HOST};dbname={$DB_NAME};charset=utf8mb4",
$DB_USER,
$DB_PASS,
$options
);
} catch (PDOException $e) {
// In Produktion Logging, keine Details ausgeben
http_response_code(500);
echo 'Database connection error.';
exit;
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
$domainBaseCandidates = [
__DIR__ . '/domainbase.php',
dirname(__DIR__) . '/domainbase.php',
];
foreach ($domainBaseCandidates as $candidate) {
if (is_file($candidate)) {
require_once $candidate;
break;
}
}
if (!defined('APP_DOMAIN_NAME')) {
define('APP_DOMAIN_NAME', APP_DOMAIN_BASE);
}

0
config/staging/.gitkeep Executable file
View File

39
config/staging/config.php Executable file
View File

@@ -0,0 +1,39 @@
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . "/domaindata.php";
// Umgebung (optional, aber hilfreich für Debugging / Logik)
define('APP_ENV', 'staging'); // oder 'prod', 'local', ...
if (!defined('ASSET_VERSION')) {
define('ASSET_VERSION', time()); // oder deine aktuelle Version
}
if (!defined('APP_STAGE_PREFIX')) {
define('APP_STAGE_PREFIX', 'staging');
}
// Domain-Konfiguration (kann pro Umgebung angepasst werden)
if (!defined('APP_DOMAIN_PRIMARY')) {
define('APP_DOMAIN_PRIMARY', APP_STAGE_PREFIX . '.' . APP_DOMAIN_NAME);
}
if (!defined('APP_URL_PRIMARY')) {
define('APP_URL_PRIMARY', 'https://' . APP_DOMAIN_PRIMARY);
}
if (!defined('APP_DOMAIN_FAKECHECK')) {
define('APP_DOMAIN_FAKECHECK', APP_STAGE_PREFIX . '.ismyusbfake.com');
}
if (!defined('APP_URL_FAKECHECK')) {
define('APP_URL_FAKECHECK', 'https://' . APP_DOMAIN_FAKECHECK);
}
// Matomo Einstellungen
define('MATOMO_URL', 'https://matomo.my-statistics.info/');
define('MATOMO_ENABLED', false);
define('MATOMO_SITE_ID', 8);
$baseUrl = 'https://' . APP_DOMAIN_PRIMARY;
$apiBaseUrl = 'https://api.' . APP_DOMAIN_PRIMARY;

28
config/staging/db.php Executable file
View File

@@ -0,0 +1,28 @@
<?php
// config/db.php
declare(strict_types=1);
$DB_HOST = 'localhost';
$DB_NAME = 'd047169d';
$DB_USER = 'd047169d';
$DB_PASS = ')yÜ.ysv19y!/bZ_nM)3Ö'; // anpassen
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO(
"mysql:host={$DB_HOST};dbname={$DB_NAME};charset=utf8mb4",
$DB_USER,
$DB_PASS,
$options
);
} catch (PDOException $e) {
// In Produktion Logging, keine Details ausgeben
http_response_code(500);
echo 'Database connection error.';
exit;
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
$domainBaseCandidates = [
__DIR__ . '/domainbase.php',
dirname(__DIR__) . '/domainbase.php',
];
foreach ($domainBaseCandidates as $candidate) {
if (is_file($candidate)) {
require_once $candidate;
break;
}
}
if (!defined('APP_DOMAIN_NAME')) {
define('APP_DOMAIN_NAME', APP_DOMAIN_BASE);
}
if (!defined('APP_STAGE_PREFIX')) {
define('APP_STAGE_PREFIX', 'staging');
}

View File

@@ -0,0 +1,122 @@
# System Architecture
## Overview
The system should be built as a web platform with a thin, responsive client and a server-side core for persistence, catalog management, plugin validation, and export generation.
## Main building blocks
### Frontend
- browser-based single-page application
- visual rack editor
- component library browser
- BOM and cable views
- import/export screens
### Backend API
- project CRUD
- component catalog API
- plugin upload and validation
- bill of materials generation
- cable estimation service
- authentication and authorization
### Database
- users
- projects
- rack definitions
- component instances
- cable links
- plugin manifests
- catalog components
### Asset storage
- component `SVG` files
- optional `glTF` assets
- import files
- generated exports such as `CSV` and `PDF`
## Recommended domain model split
### Planning domain
- racks
- placements
- connections
- validation rules
### Catalog domain
- components
- manufacturers
- plugin packs
- pricing metadata
### Output domain
- BOM
- exports
- printable documentation
## Rendering approach
For V1, prefer a 2D editor based on `SVG`.
Reasons:
- precise snap and measurement handling
- easier labeling and overlays
- simpler hit-testing for drag and resize interactions
- lower implementation cost than a full 3D editor
3D can be added later as a secondary visualization layer using `glTF` assets and `Three.js`.
## Plugin ingestion approach
Do not load arbitrary CAD files directly into the editor runtime.
Use a controlled import pipeline:
1. user uploads a plugin package
2. backend validates manifest and metadata
3. backend validates assets and dimensions
4. optional CAD conversion creates internal preview assets
5. approved components become available in the catalog
This keeps the editor stable and the plugin model secure.
## Deployment model
### Public SaaS mode
- hosted centrally
- user accounts and teams
- shared component packs
- subscription or usage-based pricing possible later
### Self-hosted mode
- optional future enterprise offering
- internal catalogs
- private component packs
## Security considerations
- signed or verified plugin packs if third-party distribution is allowed
- strict asset and file-type validation
- no execution of untrusted code in plugins
- server-side permission checks for project access
- rate limits on public endpoints
## Scaling considerations
- stateless API instances
- database-backed persistence
- object storage for assets
- asynchronous jobs for heavy imports and exports
This is enough for an internet-facing product without overengineering the first release.

View File

@@ -0,0 +1,113 @@
# Cable Length Calculation
## Goal
Estimate practical cable lengths from the physical arrangement of devices in a rack so users can preselect realistic patch and power cable variants.
The result should support planning, not exact installation certification.
## Use cases
- patch cable length between patch panel and switch
- DAC or fiber estimate between devices in the same rack
- power cable estimate from PDU to active device
- BOM generation for preconfigured cable sets
## V1 approach
Cable length should be estimated from:
- rack unit position of both components
- side of connection: front or rear
- optional port offsets
- route style
- slack factor
## Suggested routing model
For V1, use a rule-based path estimate instead of full geometric routing.
### Inputs
- source component `U` position
- target component `U` position
- source and target side
- rack depth
- routing preference
- slack percentage or fixed reserve
### Routing preferences
- `front_vertical`
- `rear_vertical`
- `front_to_rear`
- `shortest_practical`
## Example calculation logic
Estimate length as:
1. vertical distance between source and target mounting positions
2. plus horizontal or side transition allowance
3. plus front/rear traversal allowance
4. plus slack reserve
Illustrative formula:
```text
estimated_length =
vertical_distance_mm
+ side_transition_mm
+ front_rear_allowance_mm
+ service_loop_mm
```
Then round up to the next marketable cable length, for example:
- `0.25 m`
- `0.5 m`
- `1.0 m`
- `1.5 m`
- `2.0 m`
- `3.0 m`
## Accuracy strategy
The tool should display:
- raw estimated length
- recommended order length
- confidence note such as `estimate only`
## Data requirements
Better cable estimation becomes possible if components expose ports with offsets.
Minimum V1:
- component `U` position only
Better V1:
- per-port offsets in the component metadata
## BOM integration
Cable entries can be generated as:
- one line per connection
- or aggregated by cable type and recommended order length
Example:
- `12 x RJ45 patch cable Cat6A, 0.5 m`
- `4 x C13 to C14 power cable, 1.5 m`
## Future improvements
- front and rear cable manager objects
- reserved routing channels
- left/right side path selection
- bend radius constraints
- color-coded cable groups
- cross-rack cable planning

View File

@@ -0,0 +1,139 @@
# Data Model
## Design principles
- components are metadata-driven
- placement data is separate from catalog data
- plugin-imported components must behave like built-in components
- cable links are first-class entities
## Core entities
### RackTemplate
Describes a rack type that can be instantiated in projects.
Suggested fields:
- `id`
- `name`
- `rack_standard` as `10_inch` or `19_inch`
- `total_u`
- `usable_depth_mm`
- `max_weight_kg`
- `mounting_style`
### Project
- `id`
- `name`
- `owner_id`
- `created_at`
- `updated_at`
- `status`
### RackInstance
- `id`
- `project_id`
- `rack_template_id`
- `label`
- `position_index`
### ComponentDefinition
Catalog entry for a reusable part.
- `id`
- `source_type` as `builtin` or `plugin`
- `plugin_id`
- `manufacturer`
- `part_number`
- `name`
- `category`
- `rack_standard`
- `height_u`
- `width_mm`
- `depth_mm`
- `weight_kg`
- `power_w`
- `price_net`
- `currency`
- `front_svg_asset_id`
- `preview_3d_asset_id`
### ComponentInstance
Placed component inside a rack.
- `id`
- `project_id`
- `rack_instance_id`
- `component_definition_id`
- `start_u`
- `orientation`
- `notes`
### PortDefinition
Optional port model for cable planning.
- `id`
- `component_definition_id`
- `name`
- `side` as `front` or `rear`
- `offset_x_mm`
- `offset_y_mm`
- `offset_z_mm`
- `port_type`
### CableLink
- `id`
- `project_id`
- `from_component_instance_id`
- `from_port_definition_id`
- `to_component_instance_id`
- `to_port_definition_id`
- `cable_type`
- `estimated_length_mm`
- `slack_percent`
- `manual_override_length_mm`
### PluginPack
- `id`
- `name`
- `version`
- `vendor`
- `manifest_version`
- `status`
- `imported_at`
## Validation rules
- `ComponentDefinition.rack_standard` must match `RackTemplate.rack_standard`
- `ComponentInstance.start_u` plus `height_u` must not exceed rack height
- placed components must not overlap
- cable links should only reference valid ports belonging to the selected components
## BOM generation logic
The BOM should aggregate by:
- `manufacturer`
- `part_number`
- optional variant attributes such as color or length
The BOM should be able to combine:
- placed components
- accessory parts
- calculated cables
## Future extensions
- multi-rack room coordinates
- thermal zones
- separate front and rear accessories
- stock and supplier integration

79
docs/rack-planner/mvp.md Normal file
View File

@@ -0,0 +1,79 @@
# MVP Scope
## Objective
Release a public web application that solves the core planning workflow for `10"` and `19"` racks without waiting for full CAD-grade capabilities.
## MVP features
### 1. Rack editor
- create a rack project
- choose rack type: `10"` or `19"`
- define rack height in `U`
- define depth and optional width variant
- front view editor with occupied `U` grid
- drag-and-drop placement of components
- move and reorder components
- remove and duplicate components
### 2. Component library
- built-in catalog with common rack elements
- categories such as patch panel, switch, server, PDU, shelf, UPS, blank panel
- searchable component picker
- component detail panel with dimensions and metadata
### 3. Validation
- enforce rack width compatibility
- enforce `U` occupancy
- warn on depth overflow
- warn on weight limit overflow if rack limits are defined
### 4. Bill of materials
- aggregate identical parts
- export as `CSV`
- show quantity, part number, manufacturer, description, unit price, total price
- optionally list accessories separately
### 5. Plugin support
- import component packs based on a defined manifest
- metadata-driven components
- 2D visuals via `SVG`
- optional 3D visual asset reference for future use
### 6. Cable planning V1
- define cable links between two ports or two devices
- estimate route length using rack positions
- allow manual slack factor
- include cables in bill of materials
## Deliberately excluded from MVP
- freeform 3D editing
- native browser editing of `STEP` or `DXF`
- multi-rack room layout
- thermal simulation
- live collaboration
- advanced permission and approval workflows
- ERP integration
## Recommended MVP stack
- frontend: `React` + `TypeScript`
- editor rendering: `SVG` or `Canvas` for 2D rack view
- backend: `Node.js` or `PHP` API, depending on preferred team stack
- database: `PostgreSQL`
- storage: object storage for plugin assets and exports
## Success criteria
- a user can build a rack layout in under 10 minutes
- the system prevents invalid `U` placement
- the generated bill of materials is usable for procurement
- plugin packs can be added without code changes in the editor
- cable length estimates are close enough for practical preselection

View File

@@ -0,0 +1,138 @@
# Plugin Specification
## Goal
The plugin system should allow additional rack components to be imported without changing application code.
Plugins should be data packages, not executable extensions.
## Principles
- no arbitrary code execution
- manifest-based validation
- stable schema versioning
- internal conversion of optional CAD-derived assets
## Package structure
Example:
```text
vendor-pack-1/
manifest.json
components/
switch-24p.json
patchpanel-24p.json
assets/
front/
switch-24p.svg
patchpanel-24p.svg
preview3d/
switch-24p.gltf
```
## Manifest example
```json
{
"manifestVersion": 1,
"name": "Example Network Pack",
"vendor": "Example Vendor",
"version": "1.0.0",
"components": [
"components/switch-24p.json",
"components/patchpanel-24p.json"
]
}
```
## Component definition example
```json
{
"name": "24-Port Patch Panel",
"manufacturer": "Example Vendor",
"partNumber": "PP-24-1U",
"category": "patch_panel",
"rackStandard": "19_inch",
"heightU": 1,
"depthMm": 95,
"weightKg": 1.2,
"priceNet": 79.0,
"currency": "EUR",
"assets": {
"frontSvg": "assets/front/patchpanel-24p.svg"
},
"ports": [
{
"name": "Port 1",
"side": "front",
"offsetXmm": 18,
"offsetYmm": 10,
"offsetZmm": 0,
"portType": "rj45"
}
]
}
```
## Supported file formats
### V1 mandatory
- `JSON` for manifest and component metadata
- `SVG` for 2D visuals
### V1 optional
- `glTF` for future 3D preview assets
### Later import formats
These should be handled through an import pipeline, not as runtime-native editor formats:
- `DXF`
- `STEP`
- `OBJ`
## Validation rules
- schema version must be supported
- required metadata fields must exist
- asset references must resolve inside the package
- all dimensions must be plausible and positive
- category values must match allowed enums
- unsupported formats are rejected or ignored
## Security model
Plugins must never contain executable JavaScript for the editor.
Allowed plugin content:
- metadata
- images
- vector graphics
- optional validated model assets
Rejected:
- scripts
- external network callbacks
- embedded active content
## Import workflow
1. upload package
2. unpack in isolated temporary storage
3. validate manifest
4. validate component schema
5. validate assets
6. register catalog entries
7. make components available to selected users or globally
## Versioning strategy
- application schema versioning is independent from plugin pack version
- manifest schema must be explicitly versioned
- deprecated fields should have a migration path where possible

View File

@@ -0,0 +1,71 @@
# Product Concept
## Problem
There is no widely available, lightweight online tool focused on practical planning of `10"` and `19"` racks with a visual workflow, reusable components, and a usable bill of materials.
Existing workflows are often split across:
- CAD tools that are too heavy for simple planning
- spreadsheets for the bill of materials
- manual sketches for positioning
- separate notes for cabling
## Vision
`code.it Rack Planner` should be a browser-based planning platform where users can:
- create and save rack layouts online
- visually place and move rack components
- load manufacturer-specific or custom components as plugins
- generate a bill of materials automatically
- estimate cable lengths between ports or devices
## Target users
- system integrators
- installers
- IT administrators
- AV and media technicians
- control cabinet / small infrastructure planners
- organizations documenting existing rack installations
## Primary use cases
1. Plan a new `19"` network rack with switch, patch panel, PDU, UPS, shelves, and servers.
2. Plan a compact `10"` wall rack for small office or edge installations.
3. Compare multiple rack variants with different hardware selections.
4. Export a bill of materials for ordering.
5. Estimate patch cable and power cable lengths based on mounting positions.
## Functional goals
- visual rack editor
- component catalog
- drag-and-drop placement
- snap-to-unit positioning
- collision and fit validation
- bill of materials generation
- import of additional component packs
- optional cable planning and length estimation
## Non-functional goals
- runs in a modern browser
- usable on desktop first, tablet second
- clean project persistence
- suitable for public internet deployment
- secure plugin loading model
- scalable enough for many concurrent users
## Scope boundary
The product should not try to replace full mechanical CAD in the first versions. The planning focus is:
- layout
- occupancy
- compatibility
- documentation
- ordering support
Mechanical precision modeling, freeform CAD editing, and simulation should remain outside the initial scope.

View File

@@ -0,0 +1,56 @@
# Roadmap
## Phase 0: Product foundation
- finalize terminology and target market
- define core rack and component schemas
- prepare UX wireframes for the editor
- prepare initial built-in component catalog
## Phase 1: MVP
- project creation
- rack editor for `10"` and `19"`
- drag-and-drop placement
- `U` grid and collision checks
- built-in catalog
- BOM export as `CSV`
- plugin pack import with `JSON` plus `SVG`
- simple cable estimation
## Phase 2: Public beta
- user accounts
- project persistence
- shareable project links
- improved BOM formatting
- printable rack reports
- better plugin management UI
## Phase 3: Professional planning
- 3D preview
- manufacturer packs
- advanced validation rules
- richer cable routing logic
- multi-rack projects
- PDF export packages
## Phase 4: Commercial maturity
- team workspaces
- access control
- audit log
- supplier and ERP exports
- private catalogs
- self-hosted deployment option
## Product recommendation
The strongest path is:
1. ship a narrow and useful browser editor first
2. prove demand with real planners and installers
3. expand catalogs, exports, and collaboration after usage feedback
The public internet goal is realistic, but only if V1 stays disciplined and does not start as a generalized CAD platform.

155
inc/helpers.php Executable file
View File

@@ -0,0 +1,155 @@
<?php
function helper_asset_version(): ?string
{
if (isset($GLOBALS['layoutContext']['asset_version'])) {
return (string)$GLOBALS['layoutContext']['asset_version'];
}
if (defined('ASSET_VERSION')) {
return (string)ASSET_VERSION;
}
return null;
}
function helper_append_version(string $url, ?string $version = null): string
{
if ($version === null) {
$version = helper_asset_version();
}
if ($version === null || $version === '') {
return $url;
}
if (preg_match('/[?&]v=/', $url)) {
return $url;
}
return $url . (strpos($url, '?') === false ? '?' : '&') . 'v=' . rawurlencode($version);
}
function tpl_add_script(
string $src,
string $pos = 'footer',
bool $defer = false,
bool $async = false,
string $type = '',
?string $version = null,
bool $module = false
): void {
$data = [
'src' => helper_append_version($src, $version),
'defer' => $defer,
'async' => $async,
'type' => $type,
'module' => $module,
];
if ($pos === 'header') {
$GLOBALS['page_header_scripts'][] = $data;
} else {
$GLOBALS['page_footer_scripts'][] = $data;
}
}
function tpl_add_style(
string $href,
string $pos = 'header',
string $priority = 'normal',
?string $version = null,
string $media = 'all'
): void {
$GLOBALS['page_styles'][] = [
'href' => helper_append_version($href, $version),
'pos' => $pos,
'priority' => $priority,
'media' => $media,
];
}
function tpl(string $file, string $type = 'structure', string $site = 'admin'): void
{
$base = __DIR__ . '/../partials/';
$safe = static function ($value) {
return preg_match('/^[a-zA-Z0-9_\-]+$/', $value) === 1;
};
if (!$safe($file) || !$safe($type) || !$safe($site)) {
echo "<!-- tpl(): Ungültiger Parameter -->";
return;
}
$path = $type === 'landing'
? $base . "landingpage/{$site}/{$file}.php"
: $base . "structure/{$file}.php";
extract($GLOBALS, EXTR_SKIP);
if (is_file($path)) {
include $path;
} else {
echo "<!-- tpl(): Datei nicht gefunden: {$path} -->";
}
}
function tpl_render_scripts(?array $scripts = null, string $pos = 'footer'): void
{
if ($scripts === null) {
$scripts = $pos === 'header'
? ($GLOBALS['page_header_scripts'] ?? [])
: ($GLOBALS['page_footer_scripts'] ?? []);
}
foreach ($scripts as $script) {
if (empty($script['src'])) {
continue;
}
$attrs = [];
if (!empty($script['module'])) {
$attrs[] = 'type="module"';
} elseif (!empty($script['type'])) {
$attrs[] = 'type="' . htmlspecialchars((string)$script['type'], ENT_QUOTES) . '"';
}
if (!empty($script['defer'])) {
$attrs[] = 'defer';
}
if (!empty($script['async'])) {
$attrs[] = 'async';
}
$attrString = '';
if (!empty($attrs)) {
$attrString = ' ' . implode(' ', $attrs);
}
echo '<script src="' . htmlspecialchars($script['src'], ENT_QUOTES) . '"' . $attrString . '></script>' . PHP_EOL;
}
}
function tpl_render_styles(?array $styles = null, string $pos = 'header'): void
{
if ($styles === null) {
$styles = array_filter(
$GLOBALS['page_styles'] ?? [],
static fn ($style) => ($style['pos'] ?? 'header') === $pos
);
}
if (!$styles) {
return;
}
$priorityOrder = ['critical' => 0, 'high' => 1, 'normal' => 2, 'low' => 3];
usort($styles, static function ($a, $b) use ($priorityOrder) {
$aPriority = strtolower($a['priority'] ?? 'normal');
$bPriority = strtolower($b['priority'] ?? 'normal');
$aScore = $priorityOrder[$aPriority] ?? $priorityOrder['normal'];
$bScore = $priorityOrder[$bPriority] ?? $priorityOrder['normal'];
return $aScore <=> $bScore;
});
foreach ($styles as $style) {
if (empty($style['href'])) {
continue;
}
$media = trim((string)($style['media'] ?? ''));
$mediaAttr = $media && strtolower($media) !== 'all' ? ' media="' . htmlspecialchars($media, ENT_QUOTES) . '"' : '';
echo '<link rel="stylesheet" href="' . htmlspecialchars($style['href'], ENT_QUOTES) . '"' . $mediaAttr . '>' . PHP_EOL;
}
}

6
index.php Normal file
View File

@@ -0,0 +1,6 @@
<?php
declare(strict_types=1);
$GLOBALS['app_public_base'] = '/public';
require __DIR__ . '/public/index.php';

0
partials/.gitkeep Executable file
View File

0
public/.gitkeep Executable file
View File

5
public/api/index.php Normal file
View File

@@ -0,0 +1,5 @@
<?php
declare(strict_types=1);
require dirname(__DIR__, 2) . '/api/index.php';

1161
public/assets/app.css Normal file

File diff suppressed because it is too large Load Diff

1097
public/assets/app.js Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

162
public/index.php Normal file
View File

@@ -0,0 +1,162 @@
<?php
declare(strict_types=1);
require_once dirname(__DIR__) . '/config/fileload.php';
$pageTitle = 'code.it Rack Planner';
$assetVersion = defined('ASSET_VERSION') ? (string) ASSET_VERSION : 'dev';
$publicBase = rtrim((string)($GLOBALS['app_public_base'] ?? ''), '/');
$assetBase = $publicBase === '' ? '' : $publicBase;
$apiBase = $publicBase === '' ? '' : $publicBase;
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars($pageTitle, ENT_QUOTES) ?></title>
<meta name="description" content="Visual rack planning prototype for 10-inch and 19-inch installations.">
<link rel="stylesheet" href="<?= htmlspecialchars($assetBase . '/assets/app.css?v=' . rawurlencode($assetVersion), ENT_QUOTES) ?>">
</head>
<body>
<div class="app-shell">
<header class="hero">
<div class="hero__copy">
<p class="eyebrow">code.it concept workspace</p>
<h1>Rack Planner</h1>
<p class="hero__lead">
Browserbasierter Arbeitsstand fuer die Planung von <strong>10"</strong> und <strong>19"</strong> Racks.
Die aktuelle Domain ist nur Konzeptionsumgebung, nicht die spaetere Produkt-Domain.
</p>
</div>
<div class="hero__meta">
<div class="meta-card">
<span class="meta-card__label">Stand</span>
<strong>MVP Prototype</strong>
</div>
<div class="meta-card">
<span class="meta-card__label">Fokus</span>
<strong>Editor, BOM, Kabel</strong>
</div>
</div>
</header>
<main class="workspace">
<section class="panel panel--dashboard">
<div class="dashboard-grid">
<section class="dashboard-block dashboard-block--controls">
<div class="panel__heading">
<h2>Projekt</h2>
<p>Rack-Vorlage, Bibliothek und Plugin-Import.</p>
</div>
<div class="dashboard-controls">
<label class="field">
<span>Rack-Vorlage</span>
<select id="rack-template-select"></select>
</label>
<label class="field">
<span>Projektname</span>
<input id="project-name" type="text" value="Neues Rack-Projekt">
</label>
<label class="field">
<span>Rack-Farbe</span>
<input id="rack-color" type="color" value="#50545c">
</label>
<label class="field">
<span>Komponenten filtern</span>
<input id="component-filter" type="search" placeholder="z. B. Switch, UPS, Patch Panel">
</label>
<label class="field">
<span>Plugin-Pack laden</span>
<input id="plugin-input" type="file" accept=".json,application/json">
</label>
<div class="field field--status">
<span>Einsteckmodus</span>
<div id="selected-component-info" class="selection-info">Kein Element ausgewaehlt</div>
</div>
</div>
</section>
<section class="dashboard-block dashboard-block--stats">
<div class="panel__heading">
<h2>Auswertung</h2>
<p>Stueckliste, Platzreserve und Kabelschaetzung.</p>
</div>
<div class="stat-grid" id="project-stats"></div>
<section class="subpanel">
<div class="subpanel__header">
<h3>Stueckliste</h3>
<button id="export-bom" type="button">CSV kopieren</button>
</div>
<div id="bom-output" class="list-output"></div>
</section>
<section class="subpanel">
<div class="subpanel__header">
<h3>Kabel</h3>
</div>
<div class="field-row">
<label class="field">
<span>Von</span>
<select id="cable-from"></select>
</label>
<label class="field">
<span>Zu</span>
<select id="cable-to"></select>
</label>
</div>
<label class="field">
<span>Reserve in %</span>
<input id="cable-slack" type="number" min="0" max="100" step="5" value="20">
</label>
<div id="cable-output" class="notice"></div>
</section>
<section class="subpanel">
<div class="subpanel__header">
<h3>Hinweise</h3>
</div>
<ul class="notes" id="validation-output"></ul>
</section>
</section>
</div>
<section class="dashboard-block dashboard-block--library">
<div class="panel__heading">
<h2>Bibliothek</h2>
<p>Module per Drag-and-Drop direkt aus der Liste ins Rack ziehen.</p>
</div>
<div class="library library--wide" id="component-library"></div>
</section>
</section>
<section class="panel panel--editor">
<div class="panel__heading">
<h2>Rack-Editor</h2>
<p>Komponenten koennen per Drag-and-Drop verschoben werden. Jede Position referenziert eine U-Einheit.</p>
</div>
<div class="rack-stage">
<div class="rack-grid" id="rack-grid" aria-live="polite"></div>
</div>
</section>
</main>
</div>
<script>
window.APP_CONFIG = {
apiBootstrapUrl: <?= json_encode($apiBase . '/api/index.php?action=bootstrap', JSON_UNESCAPED_SLASHES) ?>,
assetVersion: <?= json_encode($assetVersion, JSON_UNESCAPED_SLASHES) ?>
};
</script>
<script src="<?= htmlspecialchars($assetBase . '/assets/app.js?v=' . rawurlencode($assetVersion), ENT_QUOTES) ?>" defer></script>
</body>
</html>

0
src/.gitkeep Executable file
View File

151
src/data/components.json Normal file
View File

@@ -0,0 +1,151 @@
[
{
"id": "patchpanel-24-cat6a",
"name": "Patch Panel 24 Port Cat6A",
"manufacturer": "Generic",
"partNumber": "PP-24-C6A-1U",
"category": "patch_panel",
"rackStandard": "19_inch",
"heightU": 1,
"depthMm": 95,
"weightKg": 1.2,
"priceNet": 79,
"currency": "EUR",
"powerW": 0,
"ports": [
{ "name": "Front Ports", "side": "front", "offsetYmm": 10 }
]
},
{
"id": "switch-24-gbe",
"name": "24 Port Gigabit Switch",
"manufacturer": "Generic",
"partNumber": "SW-24-GBE-1U",
"category": "switch",
"rackStandard": "19_inch",
"heightU": 1,
"depthMm": 220,
"weightKg": 2.4,
"priceNet": 320,
"currency": "EUR",
"powerW": 35,
"ports": [
{ "name": "Uplink", "side": "front", "offsetYmm": 10 }
]
},
{
"id": "pdu-8-way",
"name": "PDU 8-way 1U",
"manufacturer": "Generic",
"partNumber": "PDU-8-1U",
"category": "pdu",
"rackStandard": "19_inch",
"heightU": 1,
"depthMm": 60,
"weightKg": 1.6,
"priceNet": 119,
"currency": "EUR",
"powerW": 0,
"ports": [
{ "name": "Power Out", "side": "rear", "offsetYmm": 10 }
]
},
{
"id": "ups-lineinteractive-2u",
"name": "UPS Line-Interactive 2U",
"manufacturer": "Generic",
"partNumber": "UPS-LI-2U",
"category": "ups",
"rackStandard": "19_inch",
"heightU": 2,
"depthMm": 420,
"weightKg": 18,
"priceNet": 499,
"currency": "EUR",
"powerW": 900,
"ports": [
{ "name": "Power In", "side": "rear", "offsetYmm": 45 }
]
},
{
"id": "shelf-1u",
"name": "Rack Shelf 1U",
"manufacturer": "Generic",
"partNumber": "SHELF-1U",
"category": "shelf",
"rackStandard": "19_inch",
"heightU": 1,
"depthMm": 350,
"weightKg": 3.5,
"priceNet": 49,
"currency": "EUR",
"powerW": 0,
"ports": []
},
{
"id": "blank-panel-1u",
"name": "Blank Panel 1U",
"manufacturer": "Generic",
"partNumber": "BLANK-1U",
"category": "blank_panel",
"rackStandard": "10_inch",
"heightU": 1,
"depthMm": 20,
"weightKg": 0.2,
"priceNet": 9,
"currency": "EUR",
"powerW": 0,
"ports": []
},
{
"id": "switch-8-10inch",
"name": "8 Port Switch 10\"",
"manufacturer": "Generic",
"partNumber": "SW-8-10-1U",
"category": "switch",
"rackStandard": "10_inch",
"heightU": 1,
"depthMm": 140,
"weightKg": 1.1,
"priceNet": 139,
"currency": "EUR",
"powerW": 18,
"ports": [
{ "name": "Front Ports", "side": "front", "offsetYmm": 10 }
]
},
{
"id": "patchpanel-12-10inch",
"name": "Patch Panel 12 Port 10\"",
"manufacturer": "Generic",
"partNumber": "PP-12-10-1U",
"category": "patch_panel",
"rackStandard": "10_inch",
"heightU": 1,
"depthMm": 85,
"weightKg": 0.9,
"priceNet": 55,
"currency": "EUR",
"powerW": 0,
"ports": [
{ "name": "Front Ports", "side": "front", "offsetYmm": 10 }
]
},
{
"id": "mini-ups-10inch",
"name": "Mini UPS 10\" 2U",
"manufacturer": "Generic",
"partNumber": "UPS-10-2U",
"category": "ups",
"rackStandard": "10_inch",
"heightU": 2,
"depthMm": 240,
"weightKg": 9.8,
"priceNet": 269,
"currency": "EUR",
"powerW": 400,
"ports": [
{ "name": "Power In", "side": "rear", "offsetYmm": 45 }
]
}
]

View File

@@ -0,0 +1,34 @@
[
{
"id": "rack-10-wall-9u",
"name": "10\" Wall Rack 9U",
"rackStandard": "10_inch",
"totalU": 9,
"usableDepthMm": 260,
"maxWeightKg": 35
},
{
"id": "rack-10-wall-12u",
"name": "10\" Wall Rack 12U",
"rackStandard": "10_inch",
"totalU": 12,
"usableDepthMm": 300,
"maxWeightKg": 45
},
{
"id": "rack-19-floor-24u",
"name": "19\" Floor Rack 24U",
"rackStandard": "19_inch",
"totalU": 24,
"usableDepthMm": 800,
"maxWeightKg": 600
},
{
"id": "rack-19-floor-42u",
"name": "19\" Floor Rack 42U",
"rackStandard": "19_inch",
"totalU": 42,
"usableDepthMm": 1000,
"maxWeightKg": 1000
}
]

63
src/functions.php Normal file
View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
function app_base_path(string $path = ''): string
{
$base = dirname(__DIR__);
if ($path === '') {
return $base;
}
return $base . '/' . ltrim($path, '/');
}
function app_data_path(string $path = ''): string
{
return app_base_path('src/data' . ($path !== '' ? '/' . ltrim($path, '/') : ''));
}
function app_load_json_file(string $path): array
{
if (!is_file($path)) {
return [];
}
$content = file_get_contents($path);
if ($content === false) {
return [];
}
$decoded = json_decode($content, true);
return is_array($decoded) ? $decoded : [];
}
function app_get_catalog_components(): array
{
return app_load_json_file(app_data_path('components.json'));
}
function app_get_rack_templates(): array
{
return app_load_json_file(app_data_path('rack-templates.json'));
}
function app_bootstrap_payload(): array
{
return [
'app' => [
'name' => 'code.it Rack Planner',
'environment' => defined('APP_ENV') ? APP_ENV : 'prod',
'baseUrl' => $GLOBALS['app_base_url'] ?? '',
'apiBaseUrl' => $GLOBALS['app_api_base'] ?? '',
'projectDomainNote' => 'Current domain is a concept workspace only, not the final product domain.',
],
'rackTemplates' => app_get_rack_templates(),
'components' => app_get_catalog_components(),
'pluginFormat' => [
'version' => 1,
'acceptedExtensions' => ['json'],
'requiredKeys' => ['manifestVersion', 'name', 'version', 'components'],
],
];
}

0
tools/.gitkeep Executable file
View File