Compare commits
24 Commits
7b7c027222
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| 030b0c48ae | |||
| 8461507cfb | |||
| 16c19cd6ff | |||
| 70e82cae7b | |||
| f5bb37f96e | |||
| 8647bff20f | |||
| e7708831d0 | |||
| b2dcff9cb6 | |||
| 838f4d7eec | |||
| 07989f9fba | |||
| d914a660d8 | |||
| 752d33e38a | |||
| 98dd388fda | |||
| c7d96bec85 | |||
| 316175e158 | |||
| 33313cb490 | |||
| 0990d2c757 | |||
| 2f1e12659b | |||
| 16fce3e71f | |||
| a8ccdc3fff | |||
| 07c0f8eb99 | |||
| 27a14d5751 | |||
| ec07dc6ee0 | |||
| f934a4b832 |
115
.gitea/workflows/deploy.yml
Normal file
115
.gitea/workflows/deploy.yml
Normal 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
0
.gitlab-ci.yml
Normal file → Executable file
5
.vscode/extensions.json
vendored
Normal file
5
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"openai.chatgpt"
|
||||
]
|
||||
}
|
||||
107
README.md
107
README.md
@@ -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
|
||||
- [ ] [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:
|
||||
## Product direction
|
||||
|
||||
```
|
||||
cd existing_repo
|
||||
git remote add origin https://gitlab.int.kusche.berlin/emailtemplate/emailtemplate.git
|
||||
git branch -M main
|
||||
git push -uf origin main
|
||||
```
|
||||
The first release should optimize for speed, clarity, and usability:
|
||||
|
||||
## 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)
|
||||
|
||||
## 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.
|
||||
This keeps the first public version realistic while leaving room for manufacturer catalogs, better exports, and richer cable planning later.
|
||||
|
||||
0
api/.gitkeep
Executable file
0
api/.gitkeep
Executable file
20
api/index.php
Normal file
20
api/index.php
Normal 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
0
config/.gitkeep
Executable file
10
config/config.php
Normal file
10
config/config.php
Normal 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
19
config/db.php
Normal 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
11
config/domainbase.php
Normal 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
121
config/fileload.php
Executable 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
397
config/i18n.php
Executable 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. &)
|
||||
$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
0
config/prod/.gitkeep
Executable file
33
config/prod/config.php
Executable file
33
config/prod/config.php
Executable 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
28
config/prod/db.php
Executable 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;
|
||||
}
|
||||
18
config/prod/domaindata.php
Normal file
18
config/prod/domaindata.php
Normal 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
0
config/staging/.gitkeep
Executable file
39
config/staging/config.php
Executable file
39
config/staging/config.php
Executable 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
28
config/staging/db.php
Executable 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;
|
||||
}
|
||||
22
config/staging/domaindata.php
Normal file
22
config/staging/domaindata.php
Normal 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');
|
||||
}
|
||||
122
docs/rack-planner/architecture.md
Normal file
122
docs/rack-planner/architecture.md
Normal 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.
|
||||
113
docs/rack-planner/cable-length.md
Normal file
113
docs/rack-planner/cable-length.md
Normal 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
|
||||
139
docs/rack-planner/data-model.md
Normal file
139
docs/rack-planner/data-model.md
Normal 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
79
docs/rack-planner/mvp.md
Normal 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
|
||||
138
docs/rack-planner/plugin-spec.md
Normal file
138
docs/rack-planner/plugin-spec.md
Normal 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
|
||||
71
docs/rack-planner/product-concept.md
Normal file
71
docs/rack-planner/product-concept.md
Normal 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.
|
||||
56
docs/rack-planner/roadmap.md
Normal file
56
docs/rack-planner/roadmap.md
Normal 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
155
inc/helpers.php
Executable 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
6
index.php
Normal 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
0
partials/.gitkeep
Executable file
0
public/.gitkeep
Executable file
0
public/.gitkeep
Executable file
5
public/api/index.php
Normal file
5
public/api/index.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require dirname(__DIR__, 2) . '/api/index.php';
|
||||
|
||||
1161
public/assets/app.css
Normal file
1161
public/assets/app.css
Normal file
File diff suppressed because it is too large
Load Diff
1097
public/assets/app.js
Normal file
1097
public/assets/app.js
Normal file
File diff suppressed because it is too large
Load Diff
BIN
public/assets/images/rack-template.png
Normal file
BIN
public/assets/images/rack-template.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
162
public/index.php
Normal file
162
public/index.php
Normal 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
0
src/.gitkeep
Executable file
151
src/data/components.json
Normal file
151
src/data/components.json
Normal 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 }
|
||||
]
|
||||
}
|
||||
]
|
||||
34
src/data/rack-templates.json
Normal file
34
src/data/rack-templates.json
Normal 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
63
src/functions.php
Normal 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
0
tools/.gitkeep
Executable file
Reference in New Issue
Block a user