-m
This commit is contained in:
@@ -1,131 +1,123 @@
|
||||
name: Deploy Feature Branch to Test
|
||||
name: Deploy Feature Branch to Test (haus.test.kies-media.de)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "feature/**"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: "Branch or tag to deploy (default: HEAD)"
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
jobs:
|
||||
lint-php:
|
||||
name: PHP Syntax Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install PHP
|
||||
run: apt-get update -qq && apt-get install -y -qq php-cli > /dev/null 2>&1
|
||||
|
||||
- name: PHP Lint
|
||||
run: |
|
||||
errors=0
|
||||
while IFS= read -r file; do
|
||||
if ! php -l "$file" > /dev/null 2>&1; then
|
||||
echo "❌ Syntax error in $file"
|
||||
php -l "$file"
|
||||
errors=1
|
||||
fi
|
||||
done < <(find . -name "*.php" -not -path "./vendor/*")
|
||||
if [ "$errors" -eq 1 ]; then
|
||||
echo "::error::PHP lint check failed"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ All PHP files pass syntax check"
|
||||
|
||||
lint-css:
|
||||
name: CSS Lint (stylelint)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js & stylelint
|
||||
run: |
|
||||
apt-get update -qq && apt-get install -y -qq npm nodejs > /dev/null 2>&1
|
||||
npm install -g stylelint stylelint-config-standard stylelint-prettier > /dev/null 2>&1
|
||||
|
||||
- name: CSS Lint
|
||||
run: |
|
||||
npx stylelint "**/*.css" --config .stylelintrc.json --allow-empty-input
|
||||
echo "✅ All CSS files pass lint"
|
||||
|
||||
lint-html:
|
||||
name: HTML Lint (htmlhint)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js & htmlhint
|
||||
run: |
|
||||
apt-get update -qq && apt-get install -y -qq npm nodejs > /dev/null 2>&1
|
||||
npm install -g htmlhint > /dev/null 2>&1
|
||||
|
||||
- name: HTML Lint
|
||||
run: |
|
||||
npx htmlhint "**/*.html" --config .htmlhintrc
|
||||
echo "✅ All HTML files pass lint"
|
||||
|
||||
deploy:
|
||||
name: Deploy to Test Environment
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint-php, lint-css, lint-html]
|
||||
container:
|
||||
volumes:
|
||||
- /var/www/test/html:/deploy
|
||||
concurrency:
|
||||
group: deploy-test
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Show branch info
|
||||
- name: Setup SSH
|
||||
run: |
|
||||
echo "=== Deploying branch: ${{ gitea.ref_name }} ==="
|
||||
echo "=== Commit: ${{ gitea.sha }} ==="
|
||||
echo "=== By: ${{ gitea.actor }} ==="
|
||||
echo "=== All lint checks passed ✅ ==="
|
||||
date
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh-keyscan -H 188.245.242.194 >> ~/.ssh/known_hosts 2>/dev/null
|
||||
echo "✅ SSH key configured"
|
||||
|
||||
- name: Deploy to test environment
|
||||
- name: Verify SSH connectivity
|
||||
run: |
|
||||
echo "Syncing files to test environment..."
|
||||
apt-get update -qq && apt-get install -y -qq rsync > /dev/null 2>&1 || true
|
||||
ssh -o StrictHostKeyChecking=yes -i ~/.ssh/id_ed25519 \
|
||||
haustest@188.245.242.194 "echo 'SSH-OK as:' \$(whoami) 'on' \$(hostname)"
|
||||
|
||||
rsync -av --delete \
|
||||
- name: Backup current test deployment
|
||||
run: |
|
||||
ssh -i ~/.ssh/id_ed25519 haustest@188.245.242.194 \
|
||||
"cd /home/haustest/htdocs && \
|
||||
tar czf /home/haustest/backup-pre-deploy-\$(date +%Y%m%d-%H%M%S).tar.gz \
|
||||
haus.test.kies-media.de && \
|
||||
ls -lh /home/haustest/backup-pre-deploy-*.tar.gz | tail -1"
|
||||
|
||||
- name: Rsync to test environment
|
||||
run: |
|
||||
rsync -avz --delete \
|
||||
--exclude='.git' \
|
||||
--exclude='node_modules' \
|
||||
--exclude='tests' \
|
||||
--exclude='docs' \
|
||||
--exclude='.gitea' \
|
||||
--exclude='.husky' \
|
||||
--exclude='.prettierrc' \
|
||||
--exclude='.prettierignore' \
|
||||
--exclude='.stylelintrc.json' \
|
||||
--exclude='.htmlhintrc' \
|
||||
--exclude='.gitignore' \
|
||||
--exclude='.dockerignore' \
|
||||
--exclude='.continue' \
|
||||
--exclude='.husky' \
|
||||
--exclude='Dockerfile' \
|
||||
--exclude='.dockerignore' \
|
||||
--exclude='nginx.conf' \
|
||||
--exclude='eslint.config.js' \
|
||||
--exclude='package.json' \
|
||||
--exclude='package-lock.json' \
|
||||
--exclude='docs/' \
|
||||
--exclude='phpunit.xml' \
|
||||
--exclude='scripts' \
|
||||
--exclude='AGENTS.md' \
|
||||
--exclude='README.md' \
|
||||
--exclude='scripts/' \
|
||||
./ /deploy/
|
||||
--exclude='CLAUDE.md' \
|
||||
--exclude='*.md' \
|
||||
--exclude='.htmlhintrc' \
|
||||
--exclude='.prettierrc' \
|
||||
--exclude='.prettierignore' \
|
||||
--exclude='.stylelintrc.json' \
|
||||
--exclude='.editorconfig' \
|
||||
--exclude='.well-known' \
|
||||
-e "ssh -i ~/.ssh/id_ed25519" \
|
||||
./ haustest@188.245.242.194:/home/haustest/htdocs/haus.test.kies-media.de/
|
||||
|
||||
|
||||
echo "✅ Deployment complete!"
|
||||
|
||||
- name: Set permissions
|
||||
- name: Smoke test
|
||||
run: |
|
||||
chown -R 33:33 /deploy/ 2>/dev/null || true
|
||||
chmod -R 755 /deploy/ 2>/dev/null || true
|
||||
echo "✅ Permissions set"
|
||||
sleep 2
|
||||
echo "--- HTTP status codes ---"
|
||||
for path in "/" "/impressum" "/datenschutz"; do
|
||||
code=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
-H "Cache-Control: no-cache" \
|
||||
"https://haus.test.kies-media.de${path}")
|
||||
echo " $path → HTTP $code"
|
||||
if [ "$code" != "200" ]; then
|
||||
echo "❌ Smoke test failed for $path"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo ""
|
||||
echo "--- Locale switcher present? ---"
|
||||
if curl -sL "https://haus.test.kies-media.de/" | grep -q "class=\"locale-switcher\""; then
|
||||
echo " ✅ Locale switcher rendered"
|
||||
else
|
||||
echo " ❌ Locale switcher MISSING"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
echo "--- All 4 locales serving? ---"
|
||||
for loc in de en uk ru; do
|
||||
lang=$(curl -sL -H "Cache-Control: no-cache" \
|
||||
-b "locale=$loc" \
|
||||
"https://haus.test.kies-media.de/" \
|
||||
| grep -oE '<html lang="[a-z]+"' | head -1)
|
||||
echo " locale=$loc → $lang"
|
||||
done
|
||||
echo ""
|
||||
echo "🎉 Test deployment verified: https://haus.test.kies-media.de"
|
||||
|
||||
- name: Deployment summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "=========================================="
|
||||
echo " 🚀 Deployment Summary"
|
||||
echo "=========================================="
|
||||
echo " Branch: ${{ gitea.ref_name }}"
|
||||
echo " Commit: ${{ gitea.sha }}"
|
||||
echo " Target: http://178.104.150.0:6427/"
|
||||
echo " Lint: ✅ All checks passed"
|
||||
echo " Time: $(date)"
|
||||
echo "=========================================="
|
||||
echo "### 🚀 Deployment Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Target:** https://haus.test.kies-media.de" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Branch:** \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Server:** haustest@188.245.242.194" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Review URL:** https://haus.test.kies-media.de" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
@@ -36,7 +36,8 @@ final class LocaleSwitcher
|
||||
'UTF-8',
|
||||
);
|
||||
|
||||
$html = '<ul class="locale-switcher" role="list" aria-label="' . $ariaLabel . '">';
|
||||
$html = '<div class="locale-switcher-wrapper">';
|
||||
$html .= '<ul class="locale-switcher" role="list" aria-label="' . $ariaLabel . '">';
|
||||
foreach (Locale::SUPPORTED as $code) {
|
||||
$isCurrent = $code === $this->currentLocale;
|
||||
$name = htmlspecialchars(
|
||||
@@ -74,6 +75,53 @@ final class LocaleSwitcher
|
||||
}
|
||||
$html .= '</ul>';
|
||||
|
||||
// Mobile dropdown — compact single-trigger switcher for narrow viewports
|
||||
$currentName = htmlspecialchars(
|
||||
I18n::t('locale.' . $this->currentLocale, [], $this->currentLocale),
|
||||
ENT_QUOTES,
|
||||
'UTF-8',
|
||||
);
|
||||
$currentCode = htmlspecialchars($this->currentLocale, ENT_QUOTES, 'UTF-8');
|
||||
$currentFlag = self::flagSvg($this->currentLocale);
|
||||
$html .= '<details class="locale-switcher-mobile">';
|
||||
$html .= '<summary class="locale-switcher-mobile__trigger" aria-label="' . $ariaLabel . '">';
|
||||
$html .= '<span class="locale-switcher-mobile__current" lang="' . $currentCode . '">';
|
||||
$html .= $currentFlag;
|
||||
$html .= '<span class="locale-switcher-mobile__current-code">' . strtoupper($currentCode) . '</span>';
|
||||
$html .= '</span>';
|
||||
$html .= '<span class="locale-switcher-mobile__caret" aria-hidden="true">▾</span>';
|
||||
$html .= '</summary>';
|
||||
$html .= '<ul class="locale-switcher-mobile__menu" role="list">';
|
||||
foreach (Locale::SUPPORTED as $code) {
|
||||
$isCurrent = $code === $this->currentLocale;
|
||||
$name = htmlspecialchars(
|
||||
I18n::t('locale.' . $code, [], $this->currentLocale),
|
||||
ENT_QUOTES,
|
||||
'UTF-8',
|
||||
);
|
||||
$codeAttr = htmlspecialchars($code, ENT_QUOTES, 'UTF-8');
|
||||
$flag = self::flagSvg($code);
|
||||
|
||||
$html .= '<li>';
|
||||
if ($isCurrent) {
|
||||
$html .= '<span class="locale-switcher-mobile__option is-current" aria-current="true" lang="' . $codeAttr . '">'
|
||||
. $flag . '<span>' . $name . '</span></span>';
|
||||
} else {
|
||||
$url = '/locale?set=' . rawurlencode($code) . '&return=' . rawurlencode($path);
|
||||
$html .= '<a class="locale-switcher-mobile__option"'
|
||||
. ' href="' . $url . '"'
|
||||
. ' hreflang="' . $codeAttr . '"'
|
||||
. ' lang="' . $codeAttr . '"'
|
||||
. ' rel="alternate"'
|
||||
. '>'
|
||||
. $flag . '<span>' . $name . '</span></a>';
|
||||
}
|
||||
$html .= '</li>';
|
||||
}
|
||||
$html .= '</ul>';
|
||||
$html .= '</details>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ a:focus-visible {
|
||||
--charcoal: #2e2b26;
|
||||
--accent: #8b6914;
|
||||
--accent-light: #c49a2a;
|
||||
--accent-strong: #5a450d;
|
||||
--white: #fdfcfa;
|
||||
}
|
||||
|
||||
@@ -338,7 +339,7 @@ nav.scrolled .nav-hamburger span::after {
|
||||
gap: 2.5rem;
|
||||
align-items: center;
|
||||
font-size: 0.82rem;
|
||||
color: rgb(255 255 255 / 60%);
|
||||
color: rgb(255 255 255 / 88%);
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
@@ -441,10 +442,10 @@ nav.scrolled .nav-hamburger span::after {
|
||||
|
||||
.section-eyebrow {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
color: var(--accent-strong);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@@ -812,15 +813,21 @@ nav.scrolled .nav-hamburger span::after {
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.15em;
|
||||
text-transform: uppercase;
|
||||
color: var(--stone);
|
||||
color: rgb(255 255 255 / 70%);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.price-card.highlight .pc-label {
|
||||
.price-card .price-label {
|
||||
display: block;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.15em;
|
||||
text-transform: uppercase;
|
||||
color: rgb(255 255 255 / 70%);
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.pc-val {
|
||||
.price-card .price-value {
|
||||
display: block;
|
||||
font-family: "Cormorant Garamond", serif;
|
||||
font-size: 2.8rem;
|
||||
font-weight: 600;
|
||||
@@ -829,15 +836,37 @@ nav.scrolled .nav-hamburger span::after {
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.pc-sub {
|
||||
.price-card .price-unit {
|
||||
display: block;
|
||||
font-size: 0.78rem;
|
||||
color: var(--stone);
|
||||
color: rgb(255 255 255 / 75%);
|
||||
}
|
||||
|
||||
.price-card.highlight .pc-sub {
|
||||
.price-card.highlight .price-label,
|
||||
.price-card.highlight .price-unit {
|
||||
color: rgb(255 255 255 / 85%);
|
||||
}
|
||||
|
||||
.pricing-section .rent-notes {
|
||||
margin-top: 2.5rem;
|
||||
display: grid;
|
||||
grid-template-columns: 12rem 1fr;
|
||||
gap: 0.5rem 1.5rem;
|
||||
}
|
||||
|
||||
.pricing-section .rent-notes dt {
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: rgb(255 255 255 / 70%);
|
||||
}
|
||||
|
||||
.pricing-section .rent-notes dd {
|
||||
margin: 0;
|
||||
color: var(--white);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.price-note {
|
||||
margin-top: 2.5rem;
|
||||
padding-top: 2.5rem;
|
||||
@@ -1210,6 +1239,24 @@ footer {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* Logo: keep icon, hide text on small viewports */
|
||||
.logo-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Locale switcher: replace 4-flag list with single-trigger dropdown */
|
||||
.locale-switcher {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.locale-switcher-mobile {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.locale-switcher-mobile[open] > .locale-switcher-mobile__menu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Mobile slide-down nav */
|
||||
nav.mobile-open .nav-links {
|
||||
display: flex;
|
||||
@@ -1324,14 +1371,16 @@ footer {
|
||||
}
|
||||
}
|
||||
|
||||
/* LOCALE SWITCHER (sub-Issue D) */
|
||||
.locale-switcher {
|
||||
/* LOCALE SWITCHER (sub-Issue D) — desktop only */
|
||||
@media (width > 900px) {
|
||||
.locale-switcher {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.locale-switcher__item {
|
||||
@@ -1443,6 +1492,110 @@ nav.scrolled .locale-switcher__option.is-current {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* MOBILE LOCALE SWITCHER (dropdown — defaults to hidden on desktop) */
|
||||
@media (width > 900px) {
|
||||
.locale-switcher-mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.locale-switcher-mobile {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.locale-switcher-mobile__trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.55rem 0.7rem;
|
||||
border: 1px solid rgb(0 0 0 / 12%);
|
||||
border-radius: 6px;
|
||||
background: rgb(255 255 255 / 95%);
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
min-height: 44px;
|
||||
color: var(--dark);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.locale-switcher-mobile__trigger::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.locale-switcher-mobile__current {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.locale-switcher-mobile__current-code {
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.locale-switcher-mobile__caret {
|
||||
font-size: 0.7rem;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.locale-switcher-mobile[open] .locale-switcher-mobile__caret {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.locale-switcher-mobile__menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
right: 0;
|
||||
min-width: 180px;
|
||||
margin: 0;
|
||||
padding: 0.4rem;
|
||||
list-style: none;
|
||||
background: var(--white);
|
||||
border: 1px solid var(--warm);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgb(0 0 0 / 12%);
|
||||
z-index: 60;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.locale-switcher-mobile__option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.6rem 0.7rem;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
color: var(--dark);
|
||||
font-size: 0.85rem;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.locale-switcher-mobile__option.is-current {
|
||||
background: var(--cream);
|
||||
color: var(--accent-strong);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.locale-switcher-mobile__option:hover,
|
||||
.locale-switcher-mobile__option:focus-visible {
|
||||
background: var(--warm);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
nav.scrolled .locale-switcher-mobile__trigger {
|
||||
background: var(--white);
|
||||
color: var(--dark);
|
||||
border-color: var(--warm);
|
||||
}
|
||||
|
||||
nav:not(.scrolled) .locale-switcher-mobile__trigger {
|
||||
background: rgb(255 255 255 / 12%);
|
||||
color: var(--white);
|
||||
border-color: rgb(255 255 255 / 30%);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
/* FORM FIELD ERRORS (sub-Issue E) */
|
||||
.form-field-error {
|
||||
margin: 0.375rem 0 0;
|
||||
|
||||
@@ -24,6 +24,7 @@ final class LocaleSwitcherTest extends TestCase
|
||||
{
|
||||
$html = (new LocaleSwitcher('en', '/'))->render();
|
||||
self::assertStringContainsString('<ul class="locale-switcher"', $html);
|
||||
self::assertStringContainsString('<details class="locale-switcher-mobile"', $html);
|
||||
|
||||
// The 3 inactive locales render as <a hreflang="..">. The active
|
||||
// locale renders as <span lang=".."> (no hreflang). Together all
|
||||
@@ -34,7 +35,10 @@ final class LocaleSwitcherTest extends TestCase
|
||||
"locale '$code' is missing from switcher",
|
||||
);
|
||||
}
|
||||
self::assertSame(4, substr_count($html, 'class="flag"'), 'expected 4 flag SVGs');
|
||||
// Both the desktop list and the mobile dropdown render a flag per locale
|
||||
self::assertSame(4, substr_count($html, 'class="locale-switcher__option'), 'expected 4 desktop options');
|
||||
self::assertSame(4, substr_count($html, 'class="locale-switcher-mobile__option'), 'expected 4 mobile options');
|
||||
self::assertSame(9, substr_count($html, 'class="flag"'), 'expected 9 flag SVGs (4 desktop + 4 mobile menu + 1 mobile trigger)');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
Reference in New Issue
Block a user