Refactor installation scripts and update documentation

- Replaced PowerShell scripts with PHP scripts for checking prerequisites and preparing the environment.
- Added new PHP scripts: `check-prerequisites.php`, `prepare-saas-env.php`, `install-saas.php`, `build-migration-bundle.php`, and `run-sql-migrations.php`.
- Updated README and installation documentation to reflect the new PHP scripts and installation steps.
- Created a generated SQL migration bundle file structure and added SQL migration scripts.
- Enhanced the public index page with navigation and installation steps.
- Removed obsolete PowerShell scripts.
This commit is contained in:
2026-03-21 19:49:52 +01:00
parent f298802c38
commit 70e6d59c63
13 changed files with 840 additions and 129 deletions
+5 -2
View File
@@ -31,8 +31,11 @@ Archivbestand erhalten.
## Hilfsskripte
- `scripts/check-prerequisites.ps1` prueft lokale Voraussetzungen.
- `scripts/prepare-saas-env.ps1` legt aus `.env.example` eine lokale `.env` an.
- `scripts/check-prerequisites.php` prueft lokale Voraussetzungen.
- `scripts/prepare-saas-env.php` legt aus `.env.example` eine lokale `.env` an.
- `scripts/install-saas.php` fuehrt den lokalen Setup-Grundlauf aus.
- `scripts/build-migration-bundle.php` baut die SQL-Migrationen zu einer Datei.
- `scripts/run-sql-migrations.php` fuehrt die SQL-Migrationen direkt per PDO SQL Server aus.
## Hinweise Zum Umbau
+35 -7
View File
@@ -7,7 +7,7 @@ Worker ausgelegt.
## Voraussetzungen
- PHP 8.2 oder neuer
- Composer
- Composer fuer den spaeteren Laravel-Bootstrap
- SQL Server oder eine kompatible Datenbank fuer das Zielsystem
- Webserver mit Document-Root auf `public/` des Zielprojekts
- Cron-Zugang
@@ -17,13 +17,41 @@ Worker ausgelegt.
## Installation lokal
1. Repository klonen.
2. In `saas-app/` wechseln.
3. Abhaengigkeiten mit Composer installieren.
4. `.env` aus `.env.example` ableiten.
5. Datenbankzugang, URL, Mail und Tenancy-Werte eintragen.
6. Migrations ausfuehren.
2. Mit `php scripts/check-prerequisites.php` die Voraussetzungen pruefen.
3. Mit `php scripts/prepare-saas-env.php` eine `.env` aus `.env.example` anlegen.
4. Datenbankzugang, URL, Mail und Tenancy-Werte in `saas-app/.env` eintragen.
5. Mit `php scripts/install-saas.php` das SQL-Bundle erzeugen.
6. Das SQL-Bundle manuell in SQL Server ausfuehren oder mit `php scripts/run-sql-migrations.php --server=<server> --database=<db> --username=<user> --password=<pass>` direkt einspielen.
7. Einen ersten Mandanten anlegen.
8. Einen ersten Benutzer und eine Mitgliedszuordnung anlegen.
9. Spaeter Composer und das eigentliche Laravel-Bootstrap nachziehen.
## Migrationen ausfuehren
Die aktuellen Dateien unter `saas-app/database/migrations/` sind SQL-Skizzen in
PHP-Dateien, keine Laravel-Migrationsklassen. Deshalb gibt es aktuell bewusst
kein `php artisan migrate`.
Stattdessen wird die SQL-Datei ueber Skripte erzeugt:
- Installationsvorbereitung inklusive SQL-Bundle:
`php scripts/install-saas.php`
- SQL direkt gegen SQL Server ausfuehren:
`php scripts/run-sql-migrations.php --server=localhost --database=kaffeeliste_saas --username=sa --password=<passwort>`
Die erzeugte SQL-Datei liegt anschliessend unter:
- `saas-app/database/migrations/generated/all-migrations.sql`
## Installationsskripte
Vorhandene Hilfsskripte:
- `scripts/check-prerequisites.php`
- `scripts/prepare-saas-env.php`
- `scripts/install-saas.php`
- `scripts/build-migration-bundle.php`
- `scripts/run-sql-migrations.php`
## Wichtige Umgebungswerte
@@ -42,7 +70,7 @@ eigene Umgebung angepasst werden:
1. Den Inhalt von `saas-app/` auf den Zielserver hochladen.
2. Das Document-Root auf `public/` zeigen lassen.
3. Schreibrechte fuer `storage/`, Cache und Queue-Tabellen sicherstellen.
3. Schreibrechte fuer spaetere Storage-, Cache- und Queue-Bereiche sicherstellen.
4. `.env` serverseitig hinterlegen.
5. Die Anwendung einmal per Browser aufrufen und die Grundseiten pruefen.
+15 -3
View File
@@ -19,15 +19,27 @@ Die komplette Installationsanleitung steht im Repo unter
Kurzfassung:
1. `saas-app/` als Projektrahmen bereitstellen.
2. PHP 8.2+ und Composer verwenden.
1. `php ../scripts/check-prerequisites.php`
2. `php ../scripts/install-saas.php`
3. `.env` aus `.env.example` ableiten und anpassen.
4. Datenbank und Tenancy-Werte konfigurieren.
5. Migrations ausfuehren.
5. SQL-Migrationen ueber das erzeugte Bundle ausfuehren.
6. Einen ersten Mandanten und erste Benutzer anlegen.
7. Den Webserver auf `public/` ausrichten.
8. Cron-Jobs fuer Queue, Import, Export und Benachrichtigungen einrichten.
## Migrationen
Aktuell gibt es in diesem Verzeichnis keine lauffaehige `artisan migrate`-Strecke.
Die Dateien unter `database/migrations/` liefern SQL und werden ueber folgende
Skripte verarbeitet:
- `..\scripts\build-migration-bundle.php`
- `..\scripts\run-sql-migrations.php`
Wenn `pdo_sqlsrv` lokal nicht verfuegbar ist, muss das erzeugte SQL-Bundle
manuell gegen SQL Server ausgefuehrt werden.
## Migration Aus Dem Legacy-System
Die fachliche Roadmap und der Uebergang aus dem alten Root-System sind in
@@ -0,0 +1,277 @@
-- Generated migration bundle for Kaffeeliste SaaS
-- Generated at 2026-03-21T18:18:09+00:00
SET XACT_ABORT ON;
BEGIN TRANSACTION;
-- Migration: 2026_03_20_000001_create_tenants_table.php
CREATE TABLE tenants (
id CHAR(36) NOT NULL PRIMARY KEY,
tenant_key VARCHAR(120) NOT NULL,
name VARCHAR(255) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'active',
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
UNIQUE (tenant_key)
);
-- Migration: 2026_03_20_000002_create_users_table.php
CREATE TABLE users (
id CHAR(36) NOT NULL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NULL,
display_name VARCHAR(255) NOT NULL,
is_platform_admin BIT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
UNIQUE (email)
);
-- Migration: 2026_03_20_000003_create_tenant_users_table.php
CREATE TABLE tenant_users (
id CHAR(36) NOT NULL PRIMARY KEY,
tenant_id CHAR(36) NOT NULL,
user_id CHAR(36) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'active',
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
UNIQUE (tenant_id, user_id),
FOREIGN KEY (tenant_id) REFERENCES tenants(id),
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- Migration: 2026_03_20_000004_create_roles_table.php
CREATE TABLE roles (
id CHAR(36) NOT NULL PRIMARY KEY,
role_key VARCHAR(100) NOT NULL,
name VARCHAR(255) NOT NULL,
scope VARCHAR(50) NOT NULL DEFAULT 'tenant',
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
UNIQUE (role_key, scope)
);
-- Migration: 2026_03_20_000005_create_tenant_user_roles_table.php
CREATE TABLE tenant_user_roles (
id CHAR(36) NOT NULL PRIMARY KEY,
tenant_user_id CHAR(36) NOT NULL,
role_id CHAR(36) NOT NULL,
created_at DATETIME NOT NULL,
UNIQUE (tenant_user_id, role_id),
FOREIGN KEY (tenant_user_id) REFERENCES tenant_users(id),
FOREIGN KEY (role_id) REFERENCES roles(id)
);
-- Migration: 2026_03_20_000006_create_members_table.php
CREATE TABLE members (
id CHAR(36) NOT NULL PRIMARY KEY,
tenant_id CHAR(36) NOT NULL,
tenant_user_id CHAR(36) NOT NULL,
display_name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'active',
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
UNIQUE (tenant_id, email),
UNIQUE (tenant_id, tenant_user_id),
FOREIGN KEY (tenant_id) REFERENCES tenants(id),
FOREIGN KEY (tenant_user_id) REFERENCES tenant_users(id)
);
-- Migration: 2026_03_20_000006_create_password_reset_tokens_table.php
CREATE TABLE password_reset_tokens (
email VARCHAR(255) NOT NULL,
token VARCHAR(255) NOT NULL,
created_at DATETIME NOT NULL,
PRIMARY KEY (email)
);
-- Migration: 2026_03_20_000007_create_coffee_entries_table.php
CREATE TABLE coffee_entries (
id CHAR(36) NOT NULL PRIMARY KEY,
tenant_id CHAR(36) NOT NULL,
member_id CHAR(36) NOT NULL,
strokes INT NOT NULL,
unit_price DECIMAL(10,2) NOT NULL,
total_cost DECIMAL(10,2) NOT NULL,
booking_source VARCHAR(50) NOT NULL DEFAULT 'manual',
booked_at DATETIME NOT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
FOREIGN KEY (tenant_id) REFERENCES tenants(id),
FOREIGN KEY (member_id) REFERENCES members(id)
);
-- Migration: 2026_03_20_000007_create_tenant_identity_providers_table.php
CREATE TABLE tenant_identity_providers (
id CHAR(36) NOT NULL PRIMARY KEY,
tenant_id CHAR(36) NOT NULL,
provider_key VARCHAR(120) NOT NULL,
driver VARCHAR(50) NOT NULL DEFAULT 'oidc',
display_name VARCHAR(255) NOT NULL,
client_id VARCHAR(255) NOT NULL,
client_secret_encrypted VARCHAR(1024) NULL,
discovery_url VARCHAR(1024) NULL,
authorization_url VARCHAR(1024) NULL,
token_url VARCHAR(1024) NULL,
userinfo_url VARCHAR(1024) NULL,
redirect_uri VARCHAR(1024) NOT NULL,
scopes TEXT NULL,
is_enabled BIT NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
UNIQUE (tenant_id, provider_key),
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);
-- Migration: 2026_03_20_000008_create_payment_entries_table.php
CREATE TABLE payment_entries (
id CHAR(36) NOT NULL PRIMARY KEY,
tenant_id CHAR(36) NOT NULL,
member_id CHAR(36) NOT NULL,
amount DECIMAL(10,2) NOT NULL,
payment_method VARCHAR(50) NOT NULL DEFAULT 'manual',
booked_at DATETIME NOT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
FOREIGN KEY (tenant_id) REFERENCES tenants(id),
FOREIGN KEY (member_id) REFERENCES members(id)
);
-- Migration: 2026_03_20_000008_create_user_identities_table.php
CREATE TABLE user_identities (
id CHAR(36) NOT NULL PRIMARY KEY,
user_id CHAR(36) NOT NULL,
tenant_id CHAR(36) NULL,
provider_key VARCHAR(120) NOT NULL,
external_subject VARCHAR(255) NOT NULL,
email VARCHAR(255) NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
UNIQUE (provider_key, external_subject),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);
-- Migration: 2026_03_20_000009_create_ledger_entries_table.php
CREATE TABLE ledger_entries (
id CHAR(36) NOT NULL PRIMARY KEY,
tenant_id CHAR(36) NOT NULL,
member_id CHAR(36) NOT NULL,
entry_type VARCHAR(50) NOT NULL,
amount DECIMAL(10,2) NOT NULL,
reference_type VARCHAR(100) NOT NULL,
reference_id CHAR(36) NULL,
booked_at DATETIME NOT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
FOREIGN KEY (tenant_id) REFERENCES tenants(id),
FOREIGN KEY (member_id) REFERENCES members(id)
);
-- Migration: 2026_03_20_000010_create_announcements_table.php
CREATE TABLE announcements (
id CHAR(36) NOT NULL PRIMARY KEY,
tenant_id CHAR(36) NOT NULL,
title VARCHAR(255) NOT NULL,
message TEXT NOT NULL,
visible_until DATETIME NULL,
is_active BIT NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);
-- Migration: 2026_03_20_000011_create_faq_items_table.php
CREATE TABLE faq_items (
id CHAR(36) NOT NULL PRIMARY KEY,
tenant_id CHAR(36) NOT NULL,
question VARCHAR(255) NOT NULL,
answer TEXT NOT NULL,
sort_order INT NOT NULL DEFAULT 0,
is_active BIT NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);
-- Migration: 2026_03_20_000012_create_surveys_table.php
CREATE TABLE surveys (
id CHAR(36) NOT NULL PRIMARY KEY,
tenant_id CHAR(36) NOT NULL,
title VARCHAR(255) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'draft',
starts_at DATETIME NULL,
ends_at DATETIME NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);
-- Migration: 2026_03_20_000013_create_survey_questions_table.php
CREATE TABLE survey_questions (
id CHAR(36) NOT NULL PRIMARY KEY,
survey_id CHAR(36) NOT NULL,
question TEXT NOT NULL,
question_type VARCHAR(50) NOT NULL,
is_required BIT NOT NULL DEFAULT 0,
sort_order INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
FOREIGN KEY (survey_id) REFERENCES surveys(id)
);
-- Migration: 2026_03_20_000014_create_survey_answers_table.php
CREATE TABLE survey_answers (
id CHAR(36) NOT NULL PRIMARY KEY,
survey_id CHAR(36) NOT NULL,
question_id CHAR(36) NOT NULL,
tenant_user_id CHAR(36) NOT NULL,
answer_text TEXT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
FOREIGN KEY (survey_id) REFERENCES surveys(id),
FOREIGN KEY (question_id) REFERENCES survey_questions(id),
FOREIGN KEY (tenant_user_id) REFERENCES tenant_users(id)
);
-- Migration: 2026_03_20_000015_create_import_jobs_table.php
CREATE TABLE import_jobs (
id CHAR(36) NOT NULL PRIMARY KEY,
tenant_id CHAR(36) NOT NULL,
import_type VARCHAR(100) NOT NULL,
source_path VARCHAR(255) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'pending',
scheduled_at DATETIME NULL,
processed_at DATETIME NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);
-- Migration: 2026_03_20_000016_create_export_jobs_table.php
CREATE TABLE export_jobs (
id CHAR(36) NOT NULL PRIMARY KEY,
tenant_id CHAR(36) NOT NULL,
export_type VARCHAR(100) NOT NULL,
target_path VARCHAR(255) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'pending',
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);
-- Migration: 2026_03_20_000017_create_notification_logs_table.php
CREATE TABLE notification_logs (
id CHAR(36) NOT NULL PRIMARY KEY,
tenant_id CHAR(36) NOT NULL,
channel VARCHAR(50) NOT NULL,
template_key VARCHAR(100) NOT NULL,
recipient VARCHAR(255) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'planned',
sent_at DATETIME NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);
COMMIT TRANSACTION;
+201 -46
View File
@@ -2,6 +2,8 @@
declare(strict_types=1);
$page = $_GET['page'] ?? 'home';
$modules = [
[
'title' => 'Dashboard',
@@ -35,6 +37,34 @@ $modules = [
],
];
$installSteps = [
'1. `php scripts/check-prerequisites.php` ausfuehren.',
'2. `php scripts/prepare-saas-env.php` fuer die lokale `.env` verwenden.',
'3. `saas-app\.env` mit echten DB-, Mail-, Tenancy- und OIDC-Werten fuellen.',
'4. `php scripts/install-saas.php` fuer die SQL-Bundle-Erzeugung verwenden.',
'5. Optional `php scripts/run-sql-migrations.php --server=<server> --database=<db> --username=<user> --password=<pass>` fuer die direkte SQL-Ausfuehrung nutzen.',
'6. Ersten Tenant, ersten Benutzer und erste Member-Zuordnung anlegen.',
];
$migrationSteps = [
'Die Dateien unter `saas-app/database/migrations/*.php` sind keine Laravel-Migrationsklassen, sondern SQL-Skizzen.',
'Das Bundle wird ueber `php scripts/build-migration-bundle.php` erzeugt.',
'Empfohlener Weg: `php scripts/install-saas.php`.',
'Direkte SQL-Server-Ausfuehrung: `php scripts/run-sql-migrations.php --server=<server> --database=<db> --username=<user> --password=<pass>`.',
'Ohne `pdo_sqlsrv` die erzeugte SQL-Datei manuell in SQL Server Management Studio oder einem kompatiblen Tool ausfuehren.',
];
function renderList(array $items): void
{
echo '<ul class="list">';
foreach ($items as $item) {
echo '<li>' . htmlspecialchars($item, ENT_QUOTES) . '</li>';
}
echo '</ul>';
}
?><!DOCTYPE html>
<html lang="de">
<head>
@@ -48,7 +78,7 @@ $modules = [
--muted: #6a5649;
--brand: #0f766e;
--accent: #b45309;
--card: rgba(255, 252, 247, 0.88);
--card: rgba(255, 252, 247, 0.9);
--line: rgba(36, 23, 15, 0.12);
--shadow: 0 24px 60px rgba(61, 38, 24, 0.12);
--radius: 28px;
@@ -71,6 +101,7 @@ $modules = [
margin: 24px auto 40px;
}
.nav,
.hero,
.card {
border: 1px solid var(--line);
@@ -79,6 +110,38 @@ $modules = [
box-shadow: var(--shadow);
}
.nav {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
justify-content: space-between;
padding: 18px 22px;
margin-bottom: 18px;
}
.nav-links {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.nav a,
.button {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
padding: 11px 16px;
text-decoration: none;
font-weight: 700;
}
.nav a {
color: var(--brand);
background: rgba(15, 118, 110, 0.08);
}
.hero {
padding: 34px;
background:
@@ -98,11 +161,11 @@ $modules = [
h1, h2 {
margin: 0 0 14px;
font-family: "Georgia", serif;
font-family: Georgia, serif;
line-height: 1.04;
}
h1 { font-size: clamp(2.2rem, 4vw, 4rem); }
h1 { font-size: clamp(2.1rem, 4vw, 3.8rem); }
h2 { font-size: 1.35rem; }
p {
@@ -111,6 +174,14 @@ $modules = [
line-height: 1.65;
}
code {
padding: 2px 6px;
border-radius: 8px;
background: rgba(15, 118, 110, 0.08);
color: var(--brand);
font-family: Consolas, monospace;
}
.actions,
.meta {
display: flex;
@@ -119,17 +190,6 @@ $modules = [
margin-top: 22px;
}
.button,
.pill {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
padding: 12px 18px;
text-decoration: none;
font-weight: 700;
}
.button {
background: linear-gradient(135deg, var(--brand), #115e59);
color: #fff;
@@ -142,10 +202,14 @@ $modules = [
}
.pill {
display: inline-flex;
align-items: center;
border-radius: 999px;
padding: 9px 14px;
background: rgba(15, 118, 110, 0.08);
color: var(--brand);
font-size: 0.9rem;
font-weight: 700;
}
.grid {
@@ -155,6 +219,13 @@ $modules = [
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.grid-2 {
display: grid;
gap: 18px;
margin-top: 22px;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.card {
padding: 22px;
}
@@ -175,6 +246,17 @@ $modules = [
border: 1px solid rgba(15, 118, 110, 0.16);
}
.list {
margin: 18px 0 0;
padding-left: 18px;
color: var(--muted);
line-height: 1.7;
}
.list li + li {
margin-top: 10px;
}
.footer {
margin-top: 18px;
text-align: center;
@@ -183,52 +265,125 @@ $modules = [
}
@media (max-width: 900px) {
.grid {
.grid,
.grid-2 {
grid-template-columns: 1fr;
}
.hero {
padding: 24px;
}
.nav {
align-items: flex-start;
flex-direction: column;
}
}
</style>
</head>
<body>
<main class="shell">
<section class="hero">
<div class="eyebrow">Kaffeeliste SaaS Preview</div>
<h1>Der Root ist jetzt SaaS-first aufgebaut.</h1>
<p>
Diese Preview-Seite markiert den neuen Document-Root unter `saas-app/public/`.
Der Legacy-Bestand wurde nach `legacy-app/` verschoben, die Zielarchitektur
und die neu gestalteten Produktseiten liegen unter `saas-app/`.
</p>
<div class="actions">
<a class="button" href="../README.md">Workspace lesen</a>
<a class="button secondary" href="../docs/installationshandbuch.md">Installation ansehen</a>
</div>
<div class="meta">
<span class="pill">Mandantenfaehig</span>
<span class="pill">Webspace-tauglich</span>
<span class="pill">Legacy archiviert</span>
</div>
<div class="note">
Die eigentliche Runtime bleibt der naechste Schritt nach Composer-Bootstrap.
Layout, Module, Dokumentation und Hosting-Zielbild sind bereits auf die
SaaS-Zielstruktur umgestellt.
<nav class="nav" aria-label="Preview Navigation">
<strong>Kaffeeliste SaaS Preview</strong>
<div class="nav-links">
<a href="?page=home">Start</a>
<a href="?page=install">Installation</a>
<a href="?page=migrations">Migrationen</a>
</div>
</nav>
<?php if ($page === 'install'): ?>
<section class="hero">
<div class="eyebrow">Installation</div>
<h1>Die Installationsschritte laufen jetzt ueber interne Preview-Seiten.</h1>
<p>
Der Link zeigt nicht mehr auf <code>docs/installationshandbuch.md</code>,
sondern auf eine im Public-Bereich erreichbare Anleitung. Das ist noetig,
weil Dateien ausserhalb von <code>saas-app/public/</code> im Browser nicht
direkt verlinkt werden sollten.
</p>
<div class="actions">
<a class="button" href="?page=migrations">Migrationen ansehen</a>
<a class="button secondary" href="?page=home">Zur Startseite</a>
</div>
<?php renderList($installSteps); ?>
</section>
<?php elseif ($page === 'migrations'): ?>
<section class="hero">
<div class="eyebrow">Migrationen</div>
<h1>Die Migrationen werden ueber ein SQL-Bundle erzeugt und ausgefuehrt.</h1>
<p>
Aktuell gibt es hier keine lauffaehige Laravel-<code>artisan migrate</code>-Strecke.
Die Migrationsdateien liefern SQL und werden deshalb ueber Skripte zu
einer ausfuehrbaren Datei zusammengefuehrt.
</p>
<div class="actions">
<a class="button" href="?page=install">Installationsschritte</a>
<a class="button secondary" href="?page=home">Zur Startseite</a>
</div>
<?php renderList($migrationSteps); ?>
<div class="note">
Ergebnisdatei: <code>saas-app/database/migrations/generated/all-migrations.sql</code>
</div>
</section>
<?php else: ?>
<section class="hero">
<div class="eyebrow">Kaffeeliste SaaS Preview</div>
<h1>Der Root ist jetzt SaaS-first aufgebaut.</h1>
<p>
Diese Preview-Seite markiert den neuen Document-Root unter
<code>saas-app/public/</code>. Der Legacy-Bestand wurde nach
<code>legacy-app/</code> verschoben, die Zielarchitektur und die neu
gestalteten Produktseiten liegen unter <code>saas-app/</code>.
</p>
<div class="actions">
<a class="button" href="?page=install">Installation ansehen</a>
<a class="button secondary" href="?page=migrations">Migrationen ansehen</a>
</div>
<div class="meta">
<span class="pill">Mandantenfaehig</span>
<span class="pill">Webspace-tauglich</span>
<span class="pill">Legacy archiviert</span>
</div>
<div class="note">
Die eigentliche Runtime bleibt der naechste Schritt nach Composer-Bootstrap.
Layout, Module, Dokumentation und Hosting-Zielbild sind bereits auf die
SaaS-Zielstruktur umgestellt.
</div>
</section>
<section class="grid">
<?php foreach ($modules as $module): ?>
<article class="card tone-<?= htmlspecialchars($module['tone'], ENT_QUOTES) ?>">
<h2><?= htmlspecialchars($module['title'], ENT_QUOTES) ?></h2>
<p><?= htmlspecialchars($module['copy'], ENT_QUOTES) ?></p>
</article>
<?php endforeach; ?>
</section>
<?php endif; ?>
<section class="grid-2">
<article class="card">
<h2>Verfuegbare Skripte</h2>
<ul class="list">
<li><code>scripts/check-prerequisites.php</code></li>
<li><code>scripts/prepare-saas-env.php</code></li>
<li><code>scripts/install-saas.php</code></li>
<li><code>scripts/build-migration-bundle.php</code></li>
<li><code>scripts/run-sql-migrations.php</code></li>
</ul>
</article>
<article class="card">
<h2>Technischer Stand</h2>
<ul class="list">
<li>Composer ist fuer den finalen Bootstrap weiterhin erforderlich.</li>
<li>Die Migrationen sind aktuell SQL-basiert, nicht <code>artisan</code>-basiert.</li>
<li>Fuer automatische Ausfuehrung wird die PHP-Erweiterung <code>pdo_sqlsrv</code> benoetigt.</li>
</ul>
</article>
</section>
<section class="grid">
<?php foreach ($modules as $module): ?>
<article class="card tone-<?= htmlspecialchars($module['tone'], ENT_QUOTES) ?>">
<h2><?= htmlspecialchars($module['title'], ENT_QUOTES) ?></h2>
<p><?= htmlspecialchars($module['copy'], ENT_QUOTES) ?></p>
</article>
<?php endforeach; ?>
</section>
<p class="footer">Kaffeeliste SaaS Preview · Einstieg fuer Hosting, Review und weitere Implementierung</p>
<p class="footer">Kaffeeliste SaaS Preview - Einstieg fuer Hosting, Installation und weitere Implementierung</p>
</main>
</body>
</html>
+55
View File
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
$projectRoot = dirname(__DIR__);
$migrationDir = $projectRoot . DIRECTORY_SEPARATOR . 'saas-app' . DIRECTORY_SEPARATOR . 'database' . DIRECTORY_SEPARATOR . 'migrations';
$outputPath = $argv[1] ?? (
$projectRoot . DIRECTORY_SEPARATOR . 'saas-app' . DIRECTORY_SEPARATOR . 'database' . DIRECTORY_SEPARATOR . 'migrations' . DIRECTORY_SEPARATOR . 'generated' . DIRECTORY_SEPARATOR . 'all-migrations.sql'
);
if (!is_dir($migrationDir)) {
fwrite(STDERR, "Migrationsverzeichnis nicht gefunden: {$migrationDir}" . PHP_EOL);
exit(1);
}
$files = glob($migrationDir . DIRECTORY_SEPARATOR . '*.php');
sort($files, SORT_STRING);
if ($files === []) {
fwrite(STDERR, "Keine Migrationsdateien gefunden." . PHP_EOL);
exit(1);
}
$outputDir = dirname($outputPath);
if (!is_dir($outputDir) && !mkdir($outputDir, 0777, true) && !is_dir($outputDir)) {
fwrite(STDERR, "Ausgabeverzeichnis konnte nicht erstellt werden: {$outputDir}" . PHP_EOL);
exit(1);
}
$bundle = [];
$bundle[] = '-- Generated migration bundle for Kaffeeliste SaaS';
$bundle[] = '-- Generated at ' . date('c');
$bundle[] = 'SET XACT_ABORT ON;';
$bundle[] = 'BEGIN TRANSACTION;';
foreach ($files as $file) {
$sql = require $file;
if (!is_string($sql) || trim($sql) === '') {
fwrite(STDERR, "Ungueltige Migration: {$file}" . PHP_EOL);
exit(1);
}
$bundle[] = '';
$bundle[] = '-- Migration: ' . basename($file);
$bundle[] = trim($sql);
}
$bundle[] = '';
$bundle[] = 'COMMIT TRANSACTION;';
$bundle[] = '';
file_put_contents($outputPath, implode(PHP_EOL, $bundle));
fwrite(STDOUT, $outputPath . PHP_EOL);
+34
View File
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/support.php';
$phpVersion = PHP_VERSION;
$composerExists = scripts_check_command('composer');
$gitExists = scripts_check_command('git');
scripts_stdout('Pruefe lokale Voraussetzungen fuer Kaffeeliste SaaS...');
scripts_stdout('Projektwurzel: ' . scripts_project_root());
scripts_stdout('');
$checks = [
['PHP', true, $phpVersion],
['Composer', $composerExists, $composerExists ? 'verfuegbar' : 'nicht gefunden'],
['Git', $gitExists, $gitExists ? 'verfuegbar' : 'nicht gefunden'],
['SaaS-App', is_dir(scripts_saas_app_path()), scripts_saas_app_path()],
['.env.example', is_file(scripts_env_example_path()), scripts_env_example_path()],
['.env', is_file(scripts_env_path()), is_file(scripts_env_path()) ? 'vorhanden' : 'noch nicht angelegt'],
];
foreach ($checks as [$label, $passed, $detail]) {
$prefix = $passed ? '[OK]' : '[FEHLT]';
scripts_stdout(sprintf('%s %s - %s', $prefix, $label, $detail));
}
scripts_stdout('');
scripts_stdout('Naechste Schritte:');
scripts_stdout('1. Falls Composer fehlt, zuerst Composer installieren.');
scripts_stdout('2. Mit `php scripts/prepare-saas-env.php` eine lokale .env anlegen.');
scripts_stdout('3. Mit `php scripts/install-saas.php` das SQL-Bundle erzeugen.');
scripts_stdout('4. Optional `php scripts/run-sql-migrations.php --server=... --database=...` fuer die direkte SQL-Ausfuehrung verwenden.');
-48
View File
@@ -1,48 +0,0 @@
param(
[string]$ProjectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
)
$ErrorActionPreference = 'Stop'
$saasAppPath = Join-Path $ProjectRoot 'saas-app'
$envExamplePath = Join-Path $saasAppPath '.env.example'
function Test-Command {
param([string]$Name)
return $null -ne (Get-Command $Name -ErrorAction SilentlyContinue)
}
function Write-Check {
param(
[string]$Label,
[bool]$Passed,
[string]$Detail
)
$status = if ($Passed) { '[OK]' } else { '[FEHLT]' }
Write-Host "$status $Label - $Detail"
}
Write-Host 'Pruefe lokale Voraussetzungen fuer Kaffeeliste SaaS...'
Write-Host "Projektwurzel: $ProjectRoot"
Write-Host ''
$phpExists = Test-Command 'php'
$composerExists = Test-Command 'composer'
$gitExists = Test-Command 'git'
Write-Check -Label 'PHP' -Passed $phpExists -Detail ($(if ($phpExists) { (php -r "echo PHP_VERSION;") } else { 'nicht gefunden' }))
Write-Check -Label 'Composer' -Passed $composerExists -Detail ($(if ($composerExists) { (composer --version | Select-Object -First 1) } else { 'nicht gefunden' }))
Write-Check -Label 'Git' -Passed $gitExists -Detail ($(if ($gitExists) { 'verfuegbar' } else { 'nicht gefunden' }))
Write-Check -Label 'SaaS-App' -Passed (Test-Path $saasAppPath) -Detail $saasAppPath
Write-Check -Label '.env.example' -Passed (Test-Path $envExamplePath) -Detail $envExamplePath
$envPath = Join-Path $saasAppPath '.env'
Write-Check -Label '.env' -Passed (Test-Path $envPath) -Detail ($(if (Test-Path $envPath) { 'vorhanden' } else { 'noch nicht angelegt' }))
Write-Host ''
Write-Host 'Naechste Schritte:'
Write-Host '1. Falls Composer fehlt, zuerst Composer installieren.'
Write-Host '2. Mit scripts/prepare-saas-env.ps1 eine lokale .env anlegen.'
Write-Host '3. Danach saas-app konfigurieren und die Installationsanleitung in docs/installationshandbuch.md befolgen.'
+42
View File
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/support.php';
$options = scripts_parse_options($argv);
$forceEnv = isset($options['force-env']);
$prepareEnv = isset($options['prepare-env']) || !is_file(scripts_env_path());
scripts_stdout('Starte SaaS-Installationsvorbereitung...');
scripts_stdout('');
require __DIR__ . '/check-prerequisites.php';
if ($prepareEnv) {
$command = 'php ' . escapeshellarg(__DIR__ . '/prepare-saas-env.php');
if ($forceEnv) {
$command .= ' --force';
}
passthru($command, $exitCode);
if ($exitCode !== 0) {
exit($exitCode);
}
}
$bundlePath = scripts_project_root() . DIRECTORY_SEPARATOR . 'saas-app' . DIRECTORY_SEPARATOR . 'database' . DIRECTORY_SEPARATOR . 'migrations' . DIRECTORY_SEPARATOR . 'generated' . DIRECTORY_SEPARATOR . 'all-migrations.sql';
$command = 'php ' . escapeshellarg(__DIR__ . '/build-migration-bundle.php') . ' ' . escapeshellarg($bundlePath);
passthru($command, $exitCode);
if ($exitCode !== 0) {
exit($exitCode);
}
scripts_stdout('');
scripts_stdout('Naechste Schritte:');
scripts_stdout('1. saas-app/.env mit echten DB-, Mail- und Tenancy-Werten fuellen.');
scripts_stdout('2. Das SQL-Bundle manuell importieren oder `php scripts/run-sql-migrations.php --server=... --database=...` verwenden.');
scripts_stdout('3. Danach ersten Tenant, ersten Benutzer und erste Member-Zuordnung anlegen.');
+26
View File
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/support.php';
$options = scripts_parse_options($argv);
$force = isset($options['force']);
$sourcePath = scripts_env_example_path();
$targetPath = scripts_env_path();
if (!is_file($sourcePath)) {
scripts_stderr('Quelle nicht gefunden: ' . $sourcePath);
}
if (is_file($targetPath) && !$force) {
scripts_stderr('.env existiert bereits. Nutze `php scripts/prepare-saas-env.php --force` fuer ein Ueberschreiben.');
}
if (!copy($sourcePath, $targetPath)) {
scripts_stderr('Die .env konnte nicht angelegt werden.');
}
scripts_stdout('Lokale .env wurde angelegt: ' . $targetPath);
scripts_stdout('Passe jetzt DB-, Mail-, Tenancy- und OIDC-Werte an.');
-23
View File
@@ -1,23 +0,0 @@
param(
[string]$ProjectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path,
[switch]$Force
)
$ErrorActionPreference = 'Stop'
$saasAppPath = Join-Path $ProjectRoot 'saas-app'
$sourcePath = Join-Path $saasAppPath '.env.example'
$targetPath = Join-Path $saasAppPath '.env'
if (-not (Test-Path $sourcePath)) {
throw "Quelle nicht gefunden: $sourcePath"
}
if ((Test-Path $targetPath) -and -not $Force) {
throw ".env existiert bereits. Nutze -Force, wenn sie neu erzeugt werden soll."
}
Copy-Item -Path $sourcePath -Destination $targetPath -Force
Write-Host "Lokale .env wurde angelegt: $targetPath"
Write-Host 'Passe jetzt DB-, Mail-, Tenancy- und OIDC-Werte an.'
+60
View File
@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/support.php';
$options = scripts_parse_options($argv);
$env = scripts_read_env_file(scripts_env_path());
$server = $options['server'] ?? $env['DB_HOST'] ?? null;
$database = $options['database'] ?? $env['DB_DATABASE'] ?? null;
$port = $options['port'] ?? $env['DB_PORT'] ?? '1433';
$username = $options['username'] ?? $env['DB_USERNAME'] ?? null;
$password = $options['password'] ?? $env['DB_PASSWORD'] ?? null;
if ($server === null || $database === null) {
scripts_stderr('Bitte --server und --database angeben oder eine passende saas-app/.env pflegen.');
}
if (!extension_loaded('pdo_sqlsrv')) {
scripts_stderr('Die PHP-Erweiterung pdo_sqlsrv ist nicht geladen. Fuehre das Bundle manuell aus oder aktiviere den Treiber.');
}
$migrationDir = scripts_project_root() . DIRECTORY_SEPARATOR . 'saas-app' . DIRECTORY_SEPARATOR . 'database' . DIRECTORY_SEPARATOR . 'migrations';
$files = glob($migrationDir . DIRECTORY_SEPARATOR . '*.php');
sort($files, SORT_STRING);
if ($files === []) {
scripts_stderr('Keine Migrationsdateien gefunden.');
}
$dsn = sprintf('sqlsrv:Server=%s,%s;Database=%s;TrustServerCertificate=1', $server, $port, $database);
try {
$pdo = new PDO($dsn, $username, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$pdo->beginTransaction();
foreach ($files as $file) {
$sql = require $file;
if (!is_string($sql) || trim($sql) === '') {
throw new RuntimeException('Ungueltige Migration: ' . basename($file));
}
scripts_stdout('Fuehre aus: ' . basename($file));
$pdo->exec($sql);
}
$pdo->commit();
scripts_stdout('Migrationen wurden erfolgreich ausgefuehrt.');
} catch (Throwable $exception) {
if (isset($pdo) && $pdo instanceof PDO && $pdo->inTransaction()) {
$pdo->rollBack();
}
scripts_stderr('Migration fehlgeschlagen: ' . $exception->getMessage());
}
+90
View File
@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
function scripts_project_root(): string
{
return dirname(__DIR__);
}
function scripts_saas_app_path(): string
{
return scripts_project_root() . DIRECTORY_SEPARATOR . 'saas-app';
}
function scripts_env_example_path(): string
{
return scripts_saas_app_path() . DIRECTORY_SEPARATOR . '.env.example';
}
function scripts_env_path(): string
{
return scripts_saas_app_path() . DIRECTORY_SEPARATOR . '.env';
}
function scripts_check_command(string $command): bool
{
$where = stripos(PHP_OS_FAMILY, 'Windows') === 0 ? 'where' : 'command -v';
$output = [];
$exitCode = 0;
$redirect = stripos(PHP_OS_FAMILY, 'Windows') === 0 ? ' 2>NUL' : ' 2>/dev/null';
@exec($where . ' ' . escapeshellarg($command) . $redirect, $output, $exitCode);
return $exitCode === 0;
}
function scripts_parse_options(array $argv): array
{
$options = [];
foreach (array_slice($argv, 1) as $arg) {
if (!str_starts_with($arg, '--')) {
continue;
}
$arg = substr($arg, 2);
if (str_contains($arg, '=')) {
[$key, $value] = explode('=', $arg, 2);
$options[$key] = $value;
continue;
}
$options[$arg] = true;
}
return $options;
}
function scripts_read_env_file(string $path): array
{
if (!is_file($path)) {
return [];
}
$values = [];
foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) {
$trimmed = trim($line);
if ($trimmed === '' || str_starts_with($trimmed, '#') || !str_contains($trimmed, '=')) {
continue;
}
[$key, $value] = explode('=', $trimmed, 2);
$values[trim($key)] = trim($value, " \t\n\r\0\x0B\"");
}
return $values;
}
function scripts_stdout(string $message): void
{
fwrite(STDOUT, $message . PHP_EOL);
}
function scripts_stderr(string $message, int $exitCode = 1): never
{
fwrite(STDERR, $message . PHP_EOL);
exit($exitCode);
}