Compare commits

32 Commits

Author SHA1 Message Date
smueller
f4947d5e25 Merge branch 'dev'
Some checks failed
Obfuscate Main Build / obfuscate (push) Failing after 2m6s
2026-05-28 11:14:50 +02:00
smueller
45a7067878 Update sitemap.xml with new lastmod dates: Changed last modification dates for all URLs to 2026-05-28, ensuring accurate indexing and improved SEO performance. 2026-05-28 08:28:08 +02:00
smueller
4787d7b770 Refactor JavaScript files for enhanced clarity and maintainability: Updated contact.js, cookie-consent.js, and main.js by replacing obfuscated code with clearer variable names and structures. This improves readability and facilitates future development efforts. 2026-05-28 08:23:31 +02:00
smueller
a0aa8b12ca Refactor JavaScript files for improved readability and maintainability: Replaced obfuscated code in contact.js, cookie-consent.js, and main.js with clearer structures, enhancing code clarity and facilitating future development. This update streamlines the JavaScript files, making them easier to understand and modify. 2026-05-28 08:17:25 +02:00
smueller
b113bdeaa2 Implement strict hover effect removal for legal text: Added CSS rules to ensure no hover, focus, or active effects on legal hero and content sections, enhancing readability and user experience. 2026-05-27 14:45:10 +02:00
smueller
5d2be60dfa Refine legal page styles in custom CSS: Updated hover effects for glass cards and links to enhance user interaction and visual consistency. Improved text decoration on hover for legal blocks and breadcrumbs. 2026-05-27 14:43:46 +02:00
smueller
62d0076799 Enhance legal page styles in custom CSS: Added new styles for legal hero and content sections, adjusted margins and padding, and modified section hover effects for improved visual consistency and readability. 2026-05-27 14:42:32 +02:00
smueller
e920fdfc8e Add legal styling to custom CSS: Implemented styles for legal pages, including background and text color adjustments, section borders, and hover effects to enhance readability and visual consistency. 2026-05-27 14:05:55 +02:00
smueller
5d953fda7b Enhance product configuration and ordering functionality: Updated products-config.php to include shop URLs for various packages, improving the central management of product information. Added new functions for generating order URLs based on product and package selections. Updated public pages for VPC, VPS, Mail Gateway, and Webhosting to utilize the new order URL functionality, enhancing user experience and streamlining the ordering process. 2026-05-27 13:51:54 +02:00
smueller
6ca4786955 Remove legacy build scripts: Deleted outdated PowerShell and Bash scripts for publishing and running builds, streamlining the project structure and eliminating redundancy. These scripts were previously used for production builds and have been replaced by more efficient methods. 2026-05-27 13:24:20 +02:00
smueller
b9bd339607 Refactor configuration files for HexaHost.de: Updated mail and product configuration files to improve clarity and maintainability. Added deprecation notices in the old config file, migrated email handling to a new structure, and enhanced documentation for better understanding. Improved header comments across various public pages for better organization and readability. 2026-05-27 13:22:46 +02:00
smueller
b893272d64 Merge branch 'main' of https://git.hexahost.dev/smueller/HexaHost-Frontend 2026-05-27 13:14:23 +02:00
smueller
3dd707ab93 chore(release): production build 2026-05-27 13:05 2026-05-27 13:05:22 +02:00
smueller
cc1a48943a Merge branch 'main' of https://git.hexahost.dev/smueller/HexaHost-Frontend 2026-05-27 12:39:56 +02:00
smueller
dfc781f3ed chore(release): production build 2026-05-27 12:38 2026-05-27 12:38:30 +02:00
smueller
d0e5baa443 Revert main to dev deployment model: remove bootstrap and use ../backend/ paths
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 14:10:21 +02:00
smueller
8afba16905 Enhance configuration management: Updated README.md with Windows sync instructions, refactored backend configuration files to improve loading logic, and ensured consistent function definitions. Improved error handling in bootstrap.php for better user feedback during application startup. 2026-05-22 14:07:27 +02:00
smueller
96a5977283 Refactor configuration loading: Updated multiple public PHP files to require a new bootstrap file for configuration management. Adjusted paths for product configuration to enhance maintainability and consistency across the application. 2026-05-22 13:58:30 +02:00
TheOnlyMace
ec8686761c Enhance IT services section: Updated index.php to include new CSS classes for the IT services section, improving layout and responsiveness. Added styles for a 3x2 grid layout in custom.css, ensuring better presentation across different screen sizes. 2026-04-09 21:31:32 +02:00
TheOnlyMace
d3da589a1d Add animated gradient background for "Beliebt" badge: Introduced a new CSS class for the featured badge with a linear gradient and animation effects. Added media query to disable animation for users preferring reduced motion, enhancing accessibility. 2026-04-09 21:26:29 +02:00
TheOnlyMace
b6e268855e Update CSS styles: Enhanced style.css with refined layout and improved responsiveness. Adjusted CSS variables for better visual consistency and user experience across the application. 2026-04-09 21:17:11 +02:00
TheOnlyMace
d02377c735 Update CSS styles: Refined style.css to enhance layout consistency and responsiveness. Adjusted CSS variables for improved visual appeal and user experience across the application. 2026-04-09 21:13:08 +02:00
TheOnlyMace
d62d6b576d Refactor CSS styles: Updated style.css to improve layout consistency and responsiveness. Adjusted CSS variables for better visual appeal and user experience across the application. 2026-04-09 21:12:31 +02:00
TheOnlyMace
2c0138f55d Update CSS styles: Refactored and optimized the style.css file to enhance layout and visual consistency across the application. Adjusted various CSS variables and properties for improved responsiveness and user experience. 2026-04-09 21:11:25 +02:00
TheOnlyMace
2074707c9d Add product visibility control: Introduced a mechanism to hide specific products ('vpc', 'vps') from the shop. Updated relevant templates to conditionally display these products based on visibility status, enhancing user experience and product management. 2026-04-09 21:07:48 +02:00
TheOnlyMace
55f9fdd957 Update products configuration: Removed outdated features from 'mail-gateway' offerings, including email archiving and calendar & contacts, to streamline product details and improve clarity. 2026-04-09 21:04:21 +02:00
TheOnlyMace
ab81d1c49f Update webhosting configuration: Adjusted price for 'Webhosting Professional' from '9,99' to '13,99' to reflect updated pricing strategy. 2026-04-09 21:02:52 +02:00
TheOnlyMace
b40ad53d9c Update webhosting configuration: Changed 'Domains Inkl.' value from '3' to '1' to accurately reflect product offerings and specifications. 2026-04-09 21:01:59 +02:00
TheOnlyMace
e5402189ea Update webhosting configuration: Specify database type in product details by changing 'Datenbanken' value to '20 MySQL' for improved clarity in offerings. 2026-04-09 21:01:10 +02:00
TheOnlyMace
e544720900 Update webhosting configuration and content: Enhanced product descriptions, pricing, and specifications for webhosting packages. Transitioned from cPanel/Webmin to Plesk Webhosting, and updated PHP version to 8.4. Improved clarity and appeal of offerings in both backend configuration and public-facing web page. 2026-04-09 20:59:37 +02:00
a5bba86db0 Merge pull request 'Enhance Content Security Policy in .htaccess: Updated img-src and connect-src directives to include Google Tag Manager and additional Google services, improving security while ensuring compatibility with analytics tools.' (#4) from dev into main
Reviewed-on: #4
2026-04-09 13:50:00 +00:00
d34dbbb079 Merge pull request 'Update .htaccess for enhanced security and Google Tag Assistant compatibility: Removed X-Frame-Options header and adjusted Content Security Policy to allow Google Tag Manager and Google Analytics, ensuring compliance with security standards while maint…' (#3) from dev into main
Reviewed-on: #3
2026-04-09 13:46:46 +00:00
18 changed files with 85 additions and 485 deletions

View File

@@ -1,27 +1,29 @@
name: Release Build (ci → main) name: Obfuscate Main Build
on: on:
push: push:
branches: branches:
- ci - main
workflow_dispatch: workflow_dispatch:
env:
GITEA_HOST: git.hexahost.dev
REPO_PATH: smueller/HexaHost-Frontend
jobs: jobs:
release-build: obfuscate:
if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout ci (Integration) - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
repository-url: https://git.hexahost.dev/smueller/HexaHost-Frontend
ref: ci - name: Skip loop commits
run: |
msg="$(git log -1 --pretty=%B)"
echo "Last commit message: $msg"
if echo "$msg" | grep -q "\[skip ci\]"; then
echo "Skip CI commit detected."
exit 0
fi
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
@@ -36,27 +38,14 @@ jobs:
- name: Run release obfuscation - name: Run release obfuscation
run: python scripts/obfuscate_release.py --root . --hash-assets run: python scripts/obfuscate_release.py --root . --hash-assets
- name: Publish release to main - name: Commit obfuscated build
env:
GITEA_TOKEN: ${{ github.token }}
run: | run: |
git config user.name "gitea-actions" git config user.name "gitea-actions"
git config user.email "actions@local" git config user.email "actions@local"
git remote set-url origin "https://oauth2:${GITEA_TOKEN}@${GITEA_HOST}/${REPO_PATH}.git"
git fetch origin main ci
git add -A git add -A
if git diff --cached --quiet; then if git diff --cached --quiet; then
echo "No release changes to publish." echo "No build changes to commit."
exit 0 exit 0
fi fi
git commit -m "chore(release): obfuscate and hash production assets [skip ci]"
TREE=$(git write-tree) git push
MSG="chore(release): obfuscate and hash production assets [skip ci]"
if git show-ref --verify --quiet refs/remotes/origin/main; then
PARENT=$(git rev-parse origin/main)
COMMIT=$(git commit-tree "$TREE" -p "$PARENT" -m "$MSG")
else
COMMIT=$(git commit-tree "$TREE" -m "$MSG")
fi
git push origin "${COMMIT}:refs/heads/main"

View File

@@ -1,29 +0,0 @@
#!/bin/sh
# Validiert Commit-Messages nach Conventional Commits
# https://www.conventionalcommits.org/
commit_msg_file="$1"
first_line=$(sed '/^#/d;/^$/d' "$commit_msg_file" | head -n 1)
# Merge/Revert von Git erlauben
case "$first_line" in
Merge\ *|Revert\ *)
exit 0
;;
esac
pattern='^(feat|fix|docs|style|refactor|perf|test|build|ci|chore)(\([a-z0-9._-]+\))?!?: .+'
if ! printf '%s\n' "$first_line" | grep -qE "$pattern"; then
echo >&2 "❌ Commit-Message entspricht nicht Conventional Commits."
echo >&2 ""
echo >&2 " Format: type(scope): description"
echo >&2 " Beispiel: feat(products): hide vpc in navigation"
echo >&2 ""
echo >&2 " Erlaubte types: feat, fix, docs, style, refactor, perf, test, build, ci, chore"
echo >&2 " Überspringen: git commit --no-verify"
exit 1
fi
exit 0

View File

@@ -1,63 +1,44 @@
# Hinweis: Gitea nutzt .gitea/workflows/obfuscate-main.yml (identischer Ablauf). name: Obfuscate Main Build
name: Release Build (ci → main)
on: on:
push: push:
branches: branches:
- ci - main
workflow_dispatch: workflow_dispatch:
env: permissions:
GITEA_HOST: git.hexahost.dev contents: write
REPO_PATH: smueller/HexaHost-Frontend
jobs: jobs:
release-build: obfuscate:
if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }} if: github.actor != 'github-actions[bot]'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout ci (Integration) - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
repository-url: https://git.hexahost.dev/smueller/HexaHost-Frontend
ref: ci
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.12" python-version: '3.12'
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: "20" node-version: '20'
- name: Run release obfuscation - name: Run release obfuscation
run: python scripts/obfuscate_release.py --root . --hash-assets run: python scripts/obfuscate_release.py --root . --hash-assets
- name: Publish release to main - name: Commit obfuscated build
env:
GITEA_TOKEN: ${{ github.token }}
run: | run: |
git config user.name "gitea-actions"
git config user.email "actions@local"
git remote set-url origin "https://oauth2:${GITEA_TOKEN}@${GITEA_HOST}/${REPO_PATH}.git"
git fetch origin main ci
git add -A git add -A
if git diff --cached --quiet; then if git diff --cached --quiet; then
echo "No release changes to publish." echo "No build changes to commit."
exit 0 exit 0
fi fi
git commit -m "chore(release): obfuscate and hash production assets [skip ci]"
TREE=$(git write-tree) git push
MSG="chore(release): obfuscate and hash production assets [skip ci]"
if git show-ref --verify --quiet refs/remotes/origin/main; then
PARENT=$(git rev-parse origin/main)
COMMIT=$(git commit-tree "$TREE" -p "$PARENT" -m "$MSG")
else
COMMIT=$(git commit-tree "$TREE" -m "$MSG")
fi
git push origin "${COMMIT}:refs/heads/main"

1
.gitignore vendored
View File

@@ -13,7 +13,6 @@ build/
# Environment variables # Environment variables
.env .env
.cursorrules .cursorrules
.cursor/
.cursorrules.txt .cursorrules.txt
.env.local .env.local
.env.development.local .env.development.local

View File

@@ -1,16 +0,0 @@
# Conventional Commits nur die erste nicht-kommentierte Zeile wird verwendet
# Format: type(scope): description
#
# feat Neues Feature
# fix Bugfix
# docs Dokumentation
# style Formatierung (keine Logik)
# refactor Umbau ohne Feature/Fix
# perf Performance
# test Tests
# build Build / Dependencies
# ci CI/CD
# chore Sonstiges
#
# Beispiel (diese Zeile anpassen und Kommentare löschen oder stehen lassen):
# feat(products): hide vpc and vps in navigation

View File

@@ -166,38 +166,9 @@ Für den Produktivbetrieb `public/` als Webroot konfigurieren.
### Production-Build & Veröffentlichung ### Production-Build & Veröffentlichung
| Branch | Zweck | Der Quellcode bleibt auf `dev`, der veröffentlichte Stand liegt auf `main`.
|--------|--------|
| **`dev`** | Entwicklung (lesbarer Code, Kommentare) |
| **`ci`** | Integration (du mergst `dev` hierher) |
| **`main`** | Release/Produktion (obfuskiert, gehashte Assets — nur per Pipeline) |
**Ablauf: `dev` → `ci` → `main`** Bei jedem Push/Merge auf `main` läuft die GitHub Action `.github/workflows/obfuscate-main.yml` automatisch und führt aus:
1. Auf **`dev`** entwickeln und pushen
2. **`dev` nach `ci` mergen** (manuell, z. B. in Gitea oder lokal)
3. **`ci` pushen** → startet `.gitea/workflows/obfuscate-main.yml`
4. Pipeline obfuskiert im Runner-Workspace und publiziert nach **`main`**
```powershell
# Nach fertigen Änderungen auf dev:
git checkout ci
git pull origin ci
git merge dev
git push origin ci
```
Bei jedem Push auf **`ci`**:
1. Checkout von `ci` im temporären Runner-Workspace
2. Obfuscation-Build (`scripts/obfuscate_release.py --hash-assets`)
3. Ergebnis nach `main` pushen (Bot-Commit mit `[skip ci]`)
**Nicht** `dev` oder `ci` direkt nach `main` mergen. Der Branch **`ci` bleibt lesbar** — Obfuscation wird nur nach `main` publiziert.
`ci`-Branch einmalig anlegen (falls noch nicht vorhanden): `git checkout -b ci dev && git push -u origin ci`
Der Build führt aus:
- Entfernen von Kommentaren (inkl. Block-Kommentaren) in PHP/JS/CSS - Entfernen von Kommentaren (inkl. Block-Kommentaren) in PHP/JS/CSS
- Minify + Obfuscate für JavaScript - Minify + Obfuscate für JavaScript
@@ -205,7 +176,7 @@ Der Build führt aus:
- Kein Source-Map-Output - Kein Source-Map-Output
- Hashing von JS/CSS-Dateinamen + automatische Referenz-Anpassung - Hashing von JS/CSS-Dateinamen + automatische Referenz-Anpassung
Lokal testen (nur in Kopie, nicht committen): Lokal ausführbar:
```bash ```bash
python scripts/obfuscate_release.py --root . --hash-assets python scripts/obfuscate_release.py --root . --hash-assets

View File

@@ -13,7 +13,7 @@ define('SMTP_TO_EMAIL', 'info@hexahost.de'); // Empfänger-E-Mail für Kon
// Sicherheitseinstellungen // Sicherheitseinstellungen
define('ENABLE_CSRF_PROTECTION', true); // CSRF-Schutz aktivieren define('ENABLE_CSRF_PROTECTION', true); // CSRF-Schutz aktivieren
define('ENABLE_RATE_LIMITING', true); // Rate-Limiting aktivieren define('ENABLE_RATE_LIMITING', true); // Rate-Limiting aktivieren
define('MAX_REQUESTS_PER_HOUR', 5); // Max. Anfragen pro Stunde define('MAX_REQUESTS_PER_HOUR', 10); // Max. Anfragen pro Stunde
// Spam-Schutz Einstellungen // Spam-Schutz Einstellungen
define('ENABLE_SPAM_PROTECTION', true); // Spam-Schutz aktivieren define('ENABLE_SPAM_PROTECTION', true); // Spam-Schutz aktivieren

View File

@@ -443,43 +443,10 @@ $PRODUCTS['webhosting'] = [
], ],
]; ];
// Sichtbarkeit in Navigation, Footer und auf der Startseite (Seiten bleiben per URL erreichbar)
$PRODUCT_VISIBILITY = [
'vpc' => false,
'vps' => false,
'mail-gateway' => false,
'webhosting' => true,
];
// ============================================================================ // ============================================================================
// HILFSFUNKTIONEN // HILFSFUNKTIONEN
// ============================================================================ // ============================================================================
/**
* Prüft, ob eine Produktkategorie in der Navigation angezeigt wird
*/
function isProductVisible(string $productId): bool {
global $PRODUCT_VISIBILITY;
return $PRODUCT_VISIBILITY[$productId] ?? true;
}
/**
* HTML hidden-Attribut für ausgeblendete Produktkategorien
*/
function productHiddenAttr(string $productId): string {
return isProductVisible($productId) ? '' : ' hidden';
}
/**
* Aktive Navigationsseiten für sichtbare Produktkategorien
*
* @return string[]
*/
function getVisibleProductPageIds(): array {
global $PRODUCT_VISIBILITY;
return array_keys(array_filter($PRODUCT_VISIBILITY, static fn(bool $visible): bool => $visible));
}
/** /**
* Alle Produkte abrufen * Alle Produkte abrufen
*/ */

View File

@@ -15,17 +15,10 @@
<div class="footer-section"> <div class="footer-section">
<h4>Produkte</h4> <h4>Produkte</h4>
<ul> <ul>
<li<?php echo productHiddenAttr('vpc'); ?>><a href="/vpc">Virtual Private Container</a></li> <li><a href="/vpc">Virtual Private Container</a></li>
<li<?php echo productHiddenAttr('vps'); ?>><a href="/vps">Virtual Private Server</a></li> <li><a href="/vps">Virtual Private Server</a></li>
<li<?php echo productHiddenAttr('mail-gateway'); ?>><a href="/mail-gateway">Mail Gateway</a></li> <li><a href="/mail-gateway">Mail Gateway</a></li>
<li<?php echo productHiddenAttr('webhosting'); ?>><a href="/webhosting">Webhosting</a></li> <li><a href="/webhosting">Webhosting</a></li>
</ul>
</div>
<div class="footer-section">
<h4>Andere Dienste</h4>
<ul>
<li><a href="https://hexadns.de" target="_blank" rel="noopener noreferrer">hexadns.de</a></li>
<li><a href="https://www.hexa-mail.de/" target="_blank" rel="noopener noreferrer">hexa-mail.de</a></li>
</ul> </ul>
</div> </div>
<div class="footer-section"> <div class="footer-section">

View File

@@ -3,8 +3,6 @@
* Helper functions for HexaHost.de * Helper functions for HexaHost.de
*/ */
require_once __DIR__ . '/../config/products-config.php';
// Sichere Session-Konfiguration // Sichere Session-Konfiguration
if (session_status() === PHP_SESSION_NONE) { if (session_status() === PHP_SESSION_NONE) {
// Session-Cookie-Sicherheit // Session-Cookie-Sicherheit

View File

@@ -59,12 +59,12 @@
<ul class="nav-menu"> <ul class="nav-menu">
<li><a href="/" class="nav-link <?php echo ($current_page === 'home') ? 'active' : ''; ?>">Home</a></li> <li><a href="/" class="nav-link <?php echo ($current_page === 'home') ? 'active' : ''; ?>">Home</a></li>
<li class="nav-dropdown"> <li class="nav-dropdown">
<a href="#" class="nav-link <?php echo (in_array($current_page, getVisibleProductPageIds(), true)) ? 'active' : ''; ?>">Produkte</a> <a href="#" class="nav-link <?php echo (in_array($current_page, ['vpc', 'vps', 'mail-gateway', 'webhosting'])) ? 'active' : ''; ?>">Produkte</a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li<?php echo productHiddenAttr('vpc'); ?>><a href="/vpc" class="<?php echo ($current_page === 'vpc') ? 'active' : ''; ?>">Virtual Private Container</a></li> <li><a href="/vpc" class="<?php echo ($current_page === 'vpc') ? 'active' : ''; ?>">Virtual Private Container</a></li>
<li<?php echo productHiddenAttr('vps'); ?>><a href="/vps" class="<?php echo ($current_page === 'vps') ? 'active' : ''; ?>">Virtual Private Server</a></li> <li><a href="/vps" class="<?php echo ($current_page === 'vps') ? 'active' : ''; ?>">Virtual Private Server</a></li>
<li<?php echo productHiddenAttr('mail-gateway'); ?>><a href="/mail-gateway" class="<?php echo ($current_page === 'mail-gateway') ? 'active' : ''; ?>">Mail Gateway</a></li> <li><a href="/mail-gateway" class="<?php echo ($current_page === 'mail-gateway') ? 'active' : ''; ?>">Mail Gateway</a></li>
<li<?php echo productHiddenAttr('webhosting'); ?>><a href="/webhosting" class="<?php echo ($current_page === 'webhosting') ? 'active' : ''; ?>">Webhosting</a></li> <li><a href="/webhosting" class="<?php echo ($current_page === 'webhosting') ? 'active' : ''; ?>">Webhosting</a></li>
</ul> </ul>
</li> </li>
<li><a href="/it-dienstleistungen" class="nav-link <?php echo ($current_page === 'it-dienstleistungen') ? 'active' : ''; ?>">IT-Dienstleistungen</a></li> <li><a href="/it-dienstleistungen" class="nav-link <?php echo ($current_page === 'it-dienstleistungen') ? 'active' : ''; ?>">IT-Dienstleistungen</a></li>

View File

@@ -16,26 +16,6 @@
margin-top: 2rem; margin-top: 2rem;
} }
/* IT services page: center target-group cards without affecting other pages */
.it-services-page .values .values-grid {
display: flex !important;
flex-wrap: wrap !important;
justify-content: center !important;
align-items: stretch;
grid-template-columns: none !important;
gap: 1.5rem !important;
}
.it-services-page .values .value-item {
flex: 0 1 320px !important;
max-width: 360px;
aspect-ratio: 1 / 1;
margin: 0 !important;
display: flex;
flex-direction: column;
justify-content: center;
}
/* Legal pages: plain white content with black text */ /* Legal pages: plain white content with black text */
.legal-hero, .legal-hero,
.legal-content { .legal-content {
@@ -65,10 +45,8 @@
.legal-section h3, .legal-section h3,
.legal-block p, .legal-block p,
.legal-block li, .legal-block li,
.legal-hero .breadcrumb, .breadcrumb,
.legal-hero .breadcrumb span, .breadcrumb span {
.legal-content .breadcrumb,
.legal-content .breadcrumb span {
color: #000000; color: #000000;
} }
@@ -103,14 +81,12 @@
} }
.legal-block a, .legal-block a,
.legal-hero .breadcrumb a, .breadcrumb a {
.legal-content .breadcrumb a {
color: #0b57d0; color: #0b57d0;
} }
.legal-block a:hover, .legal-block a:hover,
.legal-hero .breadcrumb a:hover, .breadcrumb a:hover {
.legal-content .breadcrumb a:hover {
color: #0b57d0; color: #0b57d0;
text-decoration: none; text-decoration: none;
} }
@@ -130,41 +106,3 @@
transition: none !important; transition: none !important;
animation: none !important; animation: none !important;
} }
/* Keep footer sections in one row on desktop */
.footer-content {
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 1.5rem 1.25rem;
align-items: start;
}
.footer-section h4 {
margin-bottom: 0.65rem;
}
.footer-section ul li {
margin-bottom: 0.4rem;
}
@media (max-width: 1100px) {
.footer-content {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 768px) {
.footer-content {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 520px) {
.footer-content {
grid-template-columns: 1fr;
}
.it-services-page .values .value-item {
flex-basis: 100%;
max-width: 100%;
}
}

View File

@@ -54,7 +54,7 @@ includeHeader($page_title, $page_description, $current_page);
</p> </p>
</div> </div>
<div class="products-grid"> <div class="products-grid">
<div class="product-card glass-card"<?php echo productHiddenAttr('vpc'); ?>> <div class="product-card glass-card">
<div class="product-icon"> <div class="product-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 7V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v3"/> <path d="M4 7V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v3"/>
@@ -74,7 +74,7 @@ includeHeader($page_title, $page_description, $current_page);
</ul> </ul>
<a href="/vpc" class="btn btn-primary">Mehr erfahren</a> <a href="/vpc" class="btn btn-primary">Mehr erfahren</a>
</div> </div>
<div class="product-card glass-card"<?php echo productHiddenAttr('vps'); ?>> <div class="product-card glass-card">
<div class="product-icon"> <div class="product-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/> <rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
@@ -92,7 +92,7 @@ includeHeader($page_title, $page_description, $current_page);
</ul> </ul>
<a href="/vps" class="btn btn-primary">Mehr erfahren</a> <a href="/vps" class="btn btn-primary">Mehr erfahren</a>
</div> </div>
<div class="product-card glass-card"<?php echo productHiddenAttr('mail-gateway'); ?>> <div class="product-card glass-card">
<div class="product-icon"> <div class="product-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/> <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>

View File

@@ -10,7 +10,7 @@ $current_page = 'it-dienstleistungen';
includeHeader($page_title, $page_description, $current_page); includeHeader($page_title, $page_description, $current_page);
?> ?>
<main id="main-content" class="it-services-page"> <main id="main-content">
<!-- Services Hero --> <!-- Services Hero -->
<section class="about-hero"> <section class="about-hero">
<div class="container"> <div class="container">

View File

@@ -3,17 +3,15 @@ from __future__ import annotations
import argparse import argparse
import hashlib import hashlib
import os
import re import re
import shutil import shutil
import subprocess import subprocess
import sys import sys
import tempfile
from collections import defaultdict
from pathlib import Path from pathlib import Path
TEXT_EXTENSIONS = {".php", ".html", ".htm", ".xml", ".txt", ".js", ".css"} TEXT_EXTENSIONS = {".php", ".html", ".htm", ".xml", ".txt", ".js", ".css"}
HASH_SUFFIX_RE = re.compile(r"\.[a-f0-9]{12}$", re.I)
def strip_comments_keep_strings(text: str) -> str: def strip_comments_keep_strings(text: str) -> str:
@@ -176,124 +174,42 @@ def minify_js_fallback(text: str) -> str:
return text.strip() return text.strip()
def canonical_asset_base(stem: str) -> str: def run_cmd(command: list[str], cwd: Path, input_text: str | None = None) -> str:
name = stem
while HASH_SUFFIX_RE.search(name):
name = HASH_SUFFIX_RE.sub("", name)
return name
def is_skipped_asset(path: Path) -> bool:
lowered = path.as_posix().lower()
if ".min." in path.name or ".obf." in path.name or ".deob." in path.name:
return True
if "deobfuscated" in lowered:
return True
return False
def is_valid_source_content(content: str) -> bool:
if "[javascript-obfuscator-cli]" in content:
return False
return len(content.strip()) >= 20
def collect_asset_groups(asset_root: Path) -> dict[tuple[Path, str, str], list[Path]]:
groups: dict[tuple[Path, str, str], list[Path]] = defaultdict(list)
for ext in (".js", ".css"):
for file_path in sorted(asset_root.rglob(f"*{ext}")):
if is_skipped_asset(file_path):
continue
base = canonical_asset_base(file_path.stem)
key = (file_path.parent, base, ext)
groups[key].append(file_path)
return groups
def pick_source_file(paths: list[Path], base: str, ext: str) -> Path | None:
if not paths:
return None
parent = paths[0].parent
plain = parent / f"{base}{ext}"
ordered: list[Path] = []
if plain in paths:
ordered.append(plain)
for candidate in sorted(paths, key=lambda p: len(p.name)):
if candidate not in ordered:
ordered.append(candidate)
for candidate in ordered:
try:
content = candidate.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
continue
if is_valid_source_content(content):
return candidate
return None
def cleanup_invalid_siblings(paths: list[Path], source: Path) -> None:
for path in paths:
if path == source or not path.exists():
continue
try:
content = path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
content = ""
if not is_valid_source_content(content):
path.unlink()
def run_cmd(command: list[str], cwd: Path) -> None:
proc = subprocess.run( proc = subprocess.run(
command, command,
cwd=str(cwd), cwd=str(cwd),
input=input_text,
text=True, text=True,
capture_output=True, capture_output=True,
check=False, check=False,
) )
if proc.returncode != 0: if proc.returncode != 0:
raise RuntimeError(proc.stderr.strip() or proc.stdout.strip() or "command failed") raise RuntimeError(proc.stderr.strip() or "command failed")
return proc.stdout
def process_js(path: Path, cwd: Path) -> None: def process_js(path: Path, cwd: Path) -> None:
original = path.read_text(encoding="utf-8") original = path.read_text(encoding="utf-8")
if not is_valid_source_content(original):
raise ValueError(
f"invalid or corrupted JS source: {path} "
f"(restore e.g. 'git checkout dev -- {path.as_posix()}')"
)
if shutil.which("npx"): if shutil.which("npx"):
tmpdir = Path(tempfile.mkdtemp())
try: try:
src = tmpdir / "input.js" minified = run_cmd(
out = tmpdir / "output.js"
src.write_text(original, encoding="utf-8")
run_cmd(
[ [
"npx", "npx",
"--yes", "--yes",
"terser", "terser",
str(src),
"-o",
str(src),
"--compress", "--compress",
"--mangle", "--mangle",
"--comments", "--comments",
"false", "false",
], ],
cwd, cwd,
input_text=original,
) )
run_cmd( obfuscated = run_cmd(
[ [
"npx", "npx",
"--yes", "--yes",
"javascript-obfuscator", "javascript-obfuscator",
str(src),
"--output",
str(out),
"--compact", "--compact",
"true", "true",
"--control-flow-flattening", "--control-flow-flattening",
@@ -308,51 +224,38 @@ def process_js(path: Path, cwd: Path) -> None:
"browser-no-eval", "browser-no-eval",
"--source-map", "--source-map",
"false", "false",
"--output",
"stdout",
], ],
cwd, cwd,
input_text=minified,
) )
if not out.exists(): path.write_text(obfuscated.strip() + "\n", encoding="utf-8")
raise RuntimeError("obfuscator produced no output file")
result = out.read_text(encoding="utf-8")
if not is_valid_source_content(result):
raise RuntimeError("obfuscator output looks invalid")
path.write_text(result.strip() + "\n", encoding="utf-8")
return return
except Exception: except Exception:
pass pass
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
path.write_text(minify_js_fallback(original) + "\n", encoding="utf-8") path.write_text(minify_js_fallback(original) + "\n", encoding="utf-8")
def process_css(path: Path, cwd: Path) -> None: def process_css(path: Path, cwd: Path) -> None:
original = path.read_text(encoding="utf-8") original = path.read_text(encoding="utf-8")
if shutil.which("npx"): if shutil.which("npx"):
tmpdir = Path(tempfile.mkdtemp())
try: try:
src = tmpdir / "input.css" minified = run_cmd(
out = tmpdir / "output.css"
src.write_text(original, encoding="utf-8")
run_cmd(
[ [
"npx", "npx",
"--yes", "--yes",
"clean-css-cli", "clean-css-cli",
str(src),
"-o",
str(out),
"--skip-rebase", "--skip-rebase",
"-O2", "-O2",
], ],
cwd, cwd,
input_text=original,
) )
path.write_text(out.read_text(encoding="utf-8").strip() + "\n", encoding="utf-8") path.write_text(minified.strip() + "\n", encoding="utf-8")
return return
except Exception: except Exception:
pass pass
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
path.write_text(minify_css_fallback(original) + "\n", encoding="utf-8") path.write_text(minify_css_fallback(original) + "\n", encoding="utf-8")
@@ -363,7 +266,8 @@ def process_php(path: Path) -> None:
def hash_file(path: Path) -> str: def hash_file(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()[:12] digest = hashlib.sha256(path.read_bytes()).hexdigest()[:12]
return digest
def replace_references(root: Path, mapping: dict[str, str]) -> None: def replace_references(root: Path, mapping: dict[str, str]) -> None:
@@ -375,7 +279,7 @@ def replace_references(root: Path, mapping: dict[str, str]) -> None:
except UnicodeDecodeError: except UnicodeDecodeError:
continue continue
updated = content updated = content
for src, dst in sorted(mapping.items(), key=lambda item: len(item[0]), reverse=True): for src, dst in mapping.items():
updated = updated.replace(src, dst) updated = updated.replace(src, dst)
updated = updated.replace("/" + src, "/" + dst) updated = updated.replace("/" + src, "/" + dst)
if updated != content: if updated != content:
@@ -385,32 +289,17 @@ def replace_references(root: Path, mapping: dict[str, str]) -> None:
def build_hash_mapping(public_root: Path) -> dict[str, str]: def build_hash_mapping(public_root: Path) -> dict[str, str]:
mapping: dict[str, str] = {} mapping: dict[str, str] = {}
asset_root = public_root / "assets" asset_root = public_root / "assets"
if not asset_root.exists(): for ext in (".js", ".css"):
return mapping for file_path in sorted(asset_root.rglob(f"*{ext}")):
if ".min." in file_path.name or ".obf." in file_path.name:
groups = collect_asset_groups(asset_root) continue
for (parent, base, ext), paths in groups.items(): digest = hash_file(file_path)
source = pick_source_file(paths, base, ext) new_name = f"{file_path.stem}.{digest}{file_path.suffix}"
if source is None: new_path = file_path.with_name(new_name)
continue file_path.rename(new_path)
digest = hash_file(source) rel_old = file_path.relative_to(public_root).as_posix()
target = parent / f"{base}.{digest}{ext}" rel_new = new_path.relative_to(public_root).as_posix()
rel_new = target.relative_to(public_root).as_posix() mapping[rel_old] = rel_new
for old in paths:
rel_old = old.relative_to(public_root).as_posix()
if rel_old != rel_new:
mapping[rel_old] = rel_new
if source != target:
if target.exists():
target.unlink()
source.replace(target)
for old in paths:
if old != target and old.exists():
old.unlink()
return mapping return mapping
@@ -427,26 +316,11 @@ def main() -> int:
print("public directory not found", file=sys.stderr) print("public directory not found", file=sys.stderr)
return 1 return 1
asset_root = public_root / "assets" for js in sorted(public_root.rglob("*.js")):
if asset_root.exists(): process_js(js, repo_root)
groups = collect_asset_groups(asset_root) for css in sorted(public_root.rglob("*.css")):
for (parent, base, ext), paths in sorted(groups.items()): process_css(css, repo_root)
source = pick_source_file(paths, base, ext) for php in sorted((repo_root / "public").rglob("*.php")):
if source is None:
rel = (parent / f"{base}{ext}").relative_to(public_root)
print(
f"ERROR: No valid source for {rel}. "
f"Restore from dev, e.g.: git checkout dev -- {rel}",
file=sys.stderr,
)
return 1
cleanup_invalid_siblings(paths, source)
if ext == ".js":
process_js(source, repo_root)
else:
process_css(source, repo_root)
for php in sorted(public_root.rglob("*.php")):
process_php(php) process_php(php)
for php in sorted((repo_root / "backend").rglob("*.php")): for php in sorted((repo_root / "backend").rglob("*.php")):
process_php(php) process_php(php)

View File

@@ -1,17 +0,0 @@
# Einmal pro Clone ausführen: Commit-Template + Conventional-Commits-Hook aktivieren
$ErrorActionPreference = "Stop"
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
Push-Location $repoRoot
try {
git config --local commit.template .gitmessage
git config --local core.hooksPath .githooks
Write-Host "Git Hooks aktiv:" -ForegroundColor Green
Write-Host " commit.template = .gitmessage"
Write-Host " core.hooksPath = .githooks"
Write-Host ""
Write-Host "Commit-Format: feat(scope): beschreibung"
} finally {
Pop-Location
}

View File

@@ -1,48 +0,0 @@
<?php
/**
* HexaHost.de E-Mail Test (nur CLI oder lokale Entwicklung)
*/
if (PHP_SAPI !== 'cli') {
$remoteAddr = $_SERVER['REMOTE_ADDR'] ?? '';
$isLocal = in_array($remoteAddr, ['127.0.0.1', '::1'], true)
|| filter_var($remoteAddr, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false;
if (!$isLocal) {
http_response_code(403);
exit('Forbidden');
}
}
require_once __DIR__ . '/../backend/config/mail-config.php';
function testEmail() {
$config = getHexaHostConfig();
$subject = '[HexaHost.de] Test-E-Mail';
$message = "Test-E-Mail von HexaHost.de\n\n";
$message .= "Zeitstempel: " . date('d.m.Y H:i:s') . "\n";
$headers = [
'From: ' . $config['from_name'] . ' <' . $config['from_email'] . '>',
'MIME-Version: 1.0',
'Content-Type: text/plain; charset=UTF-8',
'X-Mailer: HexaHost Test Email',
];
return mail($config['to_email'], $subject, $message, implode("\r\n", $headers));
}
if (PHP_SAPI === 'cli') {
echo testEmail() ? "Test-E-Mail gesendet.\n" : "Fehler beim Senden.\n";
exit;
}
if (isset($_GET['test'])) {
echo testEmail()
? 'Test-E-Mail wurde gesendet.'
: 'Fehler beim Senden der Test-E-Mail.';
} else {
echo '<h1>HexaHost.de E-Mail Test</h1>';
echo '<p><a href="?test=1">Test-E-Mail senden</a></p>';
}