feat: perinco Pulse - Zeiterfassung fuer perinco

Erste Fassung, live unter https://pulse.perinco.de.

Frontend React 19 + TypeScript + Tailwind 4 (Vite), Backend Express mit
mysql2 auf MariaDB, Exporte als PDF (pdfkit), Excel (exceljs) und CSV.
Aufbau, Designsystem und die sieben Themes kommen aus dem perinco CRM.

Fachlich:
- Buchung mit Wochen-, Monats- und Jahresansicht, Zeiten als 1:23 oder 1,5
- Projekte mit n Budgettoepfen: Laufzeit, Volumen, Sperre bei Ablauf oder
  aufgebrauchtem Budget, Buchung nur auf den passenden Topf
- Kunden, Team mit Hierarchie, Reports im Harvest-Stil, Monatsexporte
- Rollen user/lead/management/admin, doppelt geprueft (Menue und Backend)
- SSO gegen Microsoft Entra ID, Uebernahme der AD-Benutzer per Drag & Drop

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
schakal00783
2026-09-17 22:52:20 +02:00
co-authored by Claude Opus 5
commit ef676a95f4
104 changed files with 21030 additions and 0 deletions
+57
View File
@@ -0,0 +1,57 @@
# perinco Pulse - Deployment auf 31.70.144.60
#
# Baut lokal und laedt dist/ plus Backend-Quellen hoch.
#
# Aufruf aus dem Projektverzeichnis:
# $env:PULSE_SSH_PASSWORD = '...'
# powershell -File deploy/deploy.ps1
# powershell -File deploy/deploy.ps1 -SkipBuild
# powershell -File deploy/deploy.ps1 -Deps # bei neuen Backend-Paketen
param(
[string]$ServerHost = '31.70.144.60',
[int]$Port = 22,
[string]$User = 'root',
[string]$Password = $env:PULSE_SSH_PASSWORD,
[string]$RemotePath = '/var/www/pulse',
[switch]$SkipBuild,
[switch]$Deps
)
$ErrorActionPreference = 'Stop'
if (-not $Password) { throw 'Kein Passwort. Setze $env:PULSE_SSH_PASSWORD oder uebergib -Password.' }
$plink = 'C:\Program Files\PuTTY\plink.exe'
$pscp = 'C:\Program Files\PuTTY\pscp.exe'
foreach ($tool in @($plink, $pscp)) { if (-not (Test-Path $tool)) { throw "Nicht gefunden: $tool" } }
$projectRoot = Split-Path -Parent $PSScriptRoot
Set-Location $projectRoot
if (-not $SkipBuild) {
Write-Host '==> Frontend bauen' -ForegroundColor Green
npm run build
if ($LASTEXITCODE -ne 0) { throw 'Build fehlgeschlagen - es wird nichts hochgeladen.' }
}
$target = "$User@$ServerHost"
Write-Host '==> dist/ hochladen' -ForegroundColor Green
& $plink -ssh -batch -P $Port -pw $Password $target "rm -rf $RemotePath/dist.new && mkdir -p $RemotePath/dist.new"
& $pscp -r -batch -P $Port -pw $Password 'dist\*' "${target}:$RemotePath/dist.new/"
Write-Host '==> Backend hochladen' -ForegroundColor Green
& $pscp -r -batch -P $Port -pw $Password 'server\src' "${target}:$RemotePath/server/"
& $pscp -r -batch -P $Port -pw $Password 'server\assets' "${target}:$RemotePath/server/"
& $pscp -batch -P $Port -pw $Password 'server\package.json' 'server\package-lock.json' "${target}:$RemotePath/server/"
# dist atomar tauschen, damit niemand eine halb hochgeladene Version sieht.
$remote = "cd $RemotePath && rm -rf dist.old && (mv dist dist.old 2>/dev/null; true) && mv dist.new dist && rm -rf dist.old"
if ($Deps) { $remote = "$remote && cd $RemotePath/server && npm ci --omit=dev --no-audit --no-fund" }
$remote = "$remote && chown -R pulse:pulse $RemotePath/server && systemctl restart perinco-pulse-backend && sleep 3 && systemctl is-active perinco-pulse-backend && curl -s http://127.0.0.1:5010/api/health"
Write-Host '==> Tauschen, neu starten, pruefen' -ForegroundColor Green
& $plink -ssh -batch -P $Port -pw $Password $target $remote
Write-Host ''
Write-Host 'Fertig: https://pulse.perinco.de' -ForegroundColor Green
+26
View File
@@ -0,0 +1,26 @@
[Unit]
Description=perinco Pulse Backend (Express + MariaDB)
After=network.target mariadb.service
Wants=mariadb.service
[Service]
Type=simple
# Eigener Systembenutzer ohne Login - das Backend braucht keine Root-Rechte.
User=pulse
Group=pulse
WorkingDirectory=/var/www/pulse/server
EnvironmentFile=/var/www/pulse/server/.env
# Server laeuft auf UTC; "heute", Monatsgrenzen und Ablaufdaten gelten in Berlin.
Environment=TZ=Europe/Berlin
Environment=NODE_ENV=production
ExecStart=/usr/bin/node src/server.js
Restart=on-failure
RestartSec=5
# Sicherheitsnetz: ein Leck soll Apache, MariaDB und das ICON CRM nicht mitreissen.
MemoryMax=768M
NoNewPrivileges=true
ProtectSystem=full
PrivateTmp=true
[Install]
WantedBy=multi-user.target
+44
View File
@@ -0,0 +1,44 @@
# perinco Pulse - Apache Vhost
# Ablage: /etc/apache2/sites-available/pulse.perinco.de.conf
# Aktivieren: a2ensite pulse.perinco.de && systemctl reload apache2
#
# Reihenfolge beim Aufsetzen:
# 1. DNS A-Record pulse.perinco.de -> 31.70.144.60
# 2. Diese Datei aktivieren (HTTP-Teil), Apache neu laden
# 3. certbot --apache -d pulse.perinco.de (legt den :443-Block selbst an)
<VirtualHost *:80>
ServerName pulse.perinco.de
DocumentRoot /var/www/pulse/dist
<Directory /var/www/pulse/dist>
Options -Indexes +FollowSymLinks
AllowOverride None
Require all granted
FallbackResource /index.html
</Directory>
# Gehashte Build-Dateien duerfen dauerhaft im Cache liegen, index.html nie.
<IfModule mod_expires.c>
ExpiresActive On
<FilesMatch "\.(js|css|woff2|png|jpg|svg|ico)$">
ExpiresDefault "access plus 1 year"
</FilesMatch>
</IfModule>
<FilesMatch "index\.html$">
Header set Cache-Control "no-cache, must-revalidate"
</FilesMatch>
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set X-Frame-Options "SAMEORIGIN"
# API lokal auf 127.0.0.1:5010, von aussen nur hierueber erreichbar.
ProxyPreserveHost On
ProxyPass /api http://127.0.0.1:5010/api
ProxyPassReverse /api http://127.0.0.1:5010/api
RequestHeader set X-Forwarded-Proto "https"
ErrorLog ${APACHE_LOG_DIR}/pulse_error.log
CustomLog ${APACHE_LOG_DIR}/pulse_access.log combined
</VirtualHost>