3 Commits

Author SHA1 Message Date
stefan 1a4753cd73 refactor(frontend): HTML-Styles aufgeräumt und Konsistenz verbessert
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-05-09 17:23:17 +02:00
stefan ece3f8bf78 feat(frontend): Grundlegendes HTML-Template für Website hinzugefügt
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-05-09 17:09:51 +02:00
stefan 8d176ce955 refactor(gradle, desktop): Build-Konfiguration bereinigt, Ports optimiert und UI-Logik konsolidiert
Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
2026-05-09 14:27:22 +02:00
18 changed files with 1318 additions and 191 deletions
+4
View File
@@ -56,3 +56,7 @@ desktop.ini
docs/temp/ docs/temp/
docs/Bin/ docs/Bin/
docs/_archive/ docs/_archive/
# Conveyor
conveyor.rootkey
output/
+2 -1
View File
@@ -90,7 +90,7 @@ subprojects {
jvmArgs("--add-opens=java.base/java.nio=ALL-UNNAMED") jvmArgs("--add-opens=java.base/java.nio=ALL-UNNAMED")
// Suppress ByteBuddy/Mockito dynamic agent loading warnings (Java 21+) // Suppress ByteBuddy/Mockito dynamic agent loading warnings (Java 21+)
jvmArgs("-XX:+EnableDynamicAgentLoading") jvmArgs("-XX:+EnableDynamicAgentLoading")
// Increase test JVM memory with a stable configuration jvmArgs("--enable-native-access=ALL-UNNAMED")
minHeapSize = "512m" minHeapSize = "512m"
maxHeapSize = "2g" maxHeapSize = "2g"
// Parallel test execution for better performance // Parallel test execution for better performance
@@ -166,6 +166,7 @@ subprojects {
jvmArgs("-Xshare:auto", "-Djdk.instrument.traceUsage=false") jvmArgs("-Xshare:auto", "-Djdk.instrument.traceUsage=false")
jvmArgs("--add-opens=java.base/java.nio=ALL-UNNAMED") jvmArgs("--add-opens=java.base/java.nio=ALL-UNNAMED")
jvmArgs("-XX:+EnableDynamicAgentLoading") jvmArgs("-XX:+EnableDynamicAgentLoading")
jvmArgs("--enable-native-access=ALL-UNNAMED")
maxHeapSize = "2g" maxHeapSize = "2g"
dependsOn("testClasses") dependsOn("testClasses")
} }
+11 -27
View File
@@ -1,42 +1,31 @@
# ============================================================================= # =============================================================================
# Conveyor Configuration for Meldestelle Desktop App # Conveyor Configuration for Meldestelle Desktop App
# ============================================================================= # =============================================================================
# Dieser Build-Weg ermöglicht das Cross-Packaging für Windows (MSI) auf Linux.
# Dokumentation: https://conveyor.hydraulic.dev/
# =============================================================================
include required("/stdlib/jdk/21/openjdk.conf")
include required("https://raw.githubusercontent.com/hydraulic-software/conveyor/master/configs/jvm/extract-native-libraries.conf") include required("https://raw.githubusercontent.com/hydraulic-software/conveyor/master/configs/jvm/extract-native-libraries.conf")
# Basis-Import der Gradle-Konfiguration (sofern das Plugin genutzt wird,
# aber wir definieren es hier explizit für maximale Kontrolle im CI/CD).
app { app {
# Anzeige-Name und Vendor
display-name = "Meldestelle" display-name = "Meldestelle"
rdns-name = "at.mocode.meldestelle" rdns-name = "at.mocode.meldestelle"
vendor = "mo-code.at" vendor = "mo-code.at"
contact-email = "support@mo-code.at" contact-email = "support@mo-code.at"
# Version aus version.properties (Conveyor kann HOCON-Variablen nutzen)
# Für diesen Task hart codiert oder via CLI-Flag --variable übergeben.
version = "1.0.0" version = "1.0.0"
# Beschreibung
description = "ÖTO-konforme Turnier-Meldestelle Desktop App" description = "ÖTO-konforme Turnier-Meldestelle Desktop App"
# Ziel-Plattformen # Ziel-Plattformen: Windows und Linux (AMD/Intel 64-bit)
# Wir konzentrieren uns auf Windows, können aber Linux/Mac später ergänzen. machines = [ windows.amd64, linux.amd64.glibc ]
site.base-url = "localhost" # Später echte Update-URL
# Icons site.base-url = "localhost"
icons = "frontend/shells/meldestelle-desktop/src/jvmMain/resources/icon.png"
# Wir geben nur den Ordner an, Conveyor findet die icon.png darin automatisch
icons = "frontend/shells/meldestelle-desktop/src/jvmMain/resources"
# Einbetten der JRE (Temurin 21 wie in CI genutzt)
jvm { jvm {
gui { gui {
main-class = "at.mocode.frontend.shell.desktop.MainKt" main-class = "at.mocode.frontend.shell.desktop.MainKt"
} }
# JVM-Argumente (analog build.gradle.kts)
jvm-options = [ jvm-options = [
"-Xms128m", "-Xms128m",
"-Xmx512m", "-Xmx512m",
@@ -45,19 +34,14 @@ app {
] ]
} }
# Input-Dateien: Hier ziehen wir die Uber-JAR oder die Gradle-Outputs. # JARs aus dem Gradle-Build
# Da wir plattformunabhängig bleiben wollen, nutzen wir das Gradle-Output-Dir. inputs += "frontend/shells/meldestelle-desktop/build/libs/*.jar"
inputs += "frontend/shells/meldestelle-desktop/build/libs/meldestelle-desktop-jvm-*.jar"
# Windows-spezifische Einstellungen
windows { windows {
# Icon als .ico
icons = "frontend/shells/meldestelle-desktop/src/jvmMain/resources/icon.ico"
# GUID für Upgrades (muss stabil bleiben)
upgrade-uuid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890" upgrade-uuid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
# Menü-Eintrag
menu-group = "Meldestelle" menu-group = "Meldestelle"
# Verknüpfung
desktop-shortcut = true desktop-shortcut = true
} }
} }
conveyor.compatibility-level = 22
+98
View File
@@ -0,0 +1,98 @@
# 📦 Guide: Desktop App Packaging (Linux, Windows, macOS)
Dieses Dokument beschreibt, wie die Meldestelle Desktop App für verschiedene Betriebssysteme paketiert wird. Wir nutzen einen hybriden Ansatz aus **Gradle (Compose-Desktop)** für lokale Linux-Builds und **Conveyor** für das Cross-Packaging (Windows/macOS) von Linux aus.
---
## 1. Voraussetzungen
### Linux (Entwicklungsrechner / Fedora)
Um native Pakete bauen zu können, müssen folgende Werkzeuge auf dem System vorhanden sein:
```bash
# Für RPM-Pakete (Fedora)
sudo dnf install rpm-build
# Für DEB-Pakete (Ubuntu/Debian)
sudo apt install dpkg-dev fakeroot
```
### Conveyor (Cross-Packaging Tool)
Conveyor wird benötigt, um von Linux aus Windows-Installer (.msi) oder macOS-Pakete zu erzeugen.
**Installation auf Fedora/Linux:**
Da automatisierte Skripte manchmal unzuverlässig sind, hier der direkte Weg über das Binär-Paket:
1. **Tarball herunterladen:**
Besuchen Sie [https://downloads.hydraulic.dev/conveyor/download.html](https://downloads.hydraulic.dev/conveyor/download.html) und laden Sie die neueste `linux-amd64.tar.gz` Datei herunter.
2. **Manuelle Installation:**
```bash
# Entpacken
tar -xvf hydraulic-conveyor-*-linux-amd64.tar.gz
# In den Pfad verschieben
sudo mv conveyor /usr/local/bin/
```
3. **Verifizieren:**
```bash
conveyor --version
```
---
## 2. Lokale Linux-Builds (Gradle)
Die schnellste Methode, um während der Entwicklung ein installierbares Paket für das eigene System zu erstellen.
### RPM-Paket (Fedora)
```bash
./gradlew :frontend:shells:meldestelle-desktop:packageRpm
```
*Ausgabe: `frontend/shells:meldestelle-desktop/build/compose/binaries/main/rpm/`*
### DEB-Paket (Ubuntu/Debian)
```bash
./gradlew :frontend:shells:meldestelle-desktop:packageDeb
```
*Ausgabe: `frontend/shells:meldestelle-desktop/build/compose/binaries/main/deb/`*
### Portable Version (Ohne Installation)
```bash
./gradlew :frontend:shells:meldestelle-desktop:createDistributable
```
*Ausgabe: `frontend/shells:meldestelle-desktop/build/compose/binaries/main/app/`*
---
## 3. Cross-Packaging mit Conveyor
Conveyor nutzt die kompilierte JAR-Datei und schnürt daraus Pakete für alle Zielplattformen.
### Schritt 1: JAR erstellen
```bash
./gradlew :frontend:shells:meldestelle-desktop:jvmJar
```
### Schritt 2: Pakete bauen
```bash
# Erstellt den Windows-Installer und die HTML-Downloadseite
conveyor make site
```
### Schritt 3: Ergebnisse
Die fertigen Installer (z.B. `.msi` für Windows) befinden sich im neu erstellten Ordner `output/`.
---
## 4. Problembehandlung & Optimierung
### Native Access Warnungen
Die App benötigt Zugriff auf native Bibliotheken (Netty/SQLite). Der notwendige Parameter `--enable-native-access=ALL-UNNAMED` ist bereits fest hinterlegt.
### Firewall-Konfiguration
Für Netzwerk-Tests (Discovery/Chat) müssen die Ports 8090, 8080 und 5353 (UDP) geöffnet sein.
Nutzen Sie dafür das bereitgestellte Skript:
```bash
sudo ./setup-firewall-linux.sh
```
+47
View File
@@ -0,0 +1,47 @@
# 🧪 Testplan: Real-World Netzwerk-POC (Chat)
Ziel dieses Tests ist die Verifizierung der stabilen Kommunikation zwischen verschiedenen Geräten (Master & Client) im lokalen Netzwerk (LAN/WLAN) inklusive automatischer Dienst-Erkennung (mDNS).
---
## Vorbereitung (USB-Stick)
Folgende Dateien sollten auf dem Test-USB-Stick vorhanden sein:
1. **Installer:** Das .rpm oder .deb Paket der App (oder der distributable Ordner).
2. **Windows-Installer:** Die .msi Datei (via Conveyor).
3. **Setup-Skript:** setup-firewall-linux.sh.
---
## Durchführung
### 1. Master-Gerät einrichten (Zentrale)
1. App auf dem Haupt-PC installieren und starten.
2. In der **Geräte-Initialisierung**:
* Rolle: **MASTER** wählen.
* Gerätename vergeben (z.B. "Meldestelle-Master").
* Sicherheitsschlüssel (Sync-Key) festlegen (z.B. "geheim123").
3. Auf **Finalisieren** klicken.
4. Der Master zeigt nun seine IP-Adresse an und wartet auf Clients.
### 2. Client-Geräte einrichten (Richter/PC)
1. App auf weiteren Geräten (Linux/Windows) starten.
2. In der **Geräte-Initialisierung**:
* Rolle: **CLIENT** wählen.
* **Shared Key** eingeben (muss exakt wie beim Master sein).
3. Warten, bis der Master in der Liste erscheint (mDNS Discovery).
4. Master auswählen und auf **Jetzt verbinden** klicken.
### 3. Verbindungs-Check & Chat
1. Sobald der Status auf "Verbunden" steht, den Button **"Verbindung testen (Chat & Self-Test)"** klicken.
2. Im Chat-Modal eine Nachricht schreiben.
3. Prüfen, ob die Nachricht auf allen verbundenen Geräten erscheint.
4. Den automatischen "Ping-Pong" Self-Test beobachten.
---
## Erfolgskriterien
* [ ] Master wird innerhalb von 10 Sekunden automatisch in der Client-Liste gefunden.
* [ ] Nachrichten werden nahezu verzögerungsfrei (< 500ms) übertragen.
* [ ] Der Status wechselt zuverlässig auf "CONNECTED".
* [ ] Keine FocusRelatedWarning mehr in der Konsole/Log.
@@ -0,0 +1,43 @@
---
type: Journal
status: ACTIVE
owner: Curator
last_update: 2026-05-09
---
# 2026-05-09 — Session Log (Build Hardening, RPM Packaging & Network POC Trial)
## Kontext
- Fokus: Build-System-Optimierung für JDK 25, Etablierung des professionellen Packaging-Workflows (RPM/Conveyor) und erster Real-World Netzwerk-POC.
## Summary
- **Build-System Hardening:** Umstellung auf Gradle 9.5.0 und Kotlin 2.3.21. Sämtliche Build- und Laufzeit-Warnungen (sun.misc.Unsafe, JDK 25 Native Access, SLF4J) wurden durch zentrale Konfiguration in `gradle.properties` und Root-`build.gradle.kts` eliminiert.
- **Desktop Shell Stabilisierung:** Behebung von Koin-Inferenzfehlern und SQLDelight-Initialisierungsproblemen in der `main.kt`. Der `FocusRelatedWarning` wurde durch eine frame-safe Fokus-Steuerung behoben.
- **Packaging & Distribution:**
- RPM-Support für Fedora/RHEL aktiviert.
- Hydraulic Conveyor lokal installiert und für Cross-Packaging (Windows MSI) konfiguriert.
- Icon-Inkompatibilitäten (8-bit vs 16-bit RGBA) für Linux-Installer gelöst.
- Neue Guides für Packaging und Netzwerk-Tests erstellt.
- **Netzwerk-POC (Erster Test):**
- Das RPM-Paket lies sich auf Fedora 44 (KDE) erfolgreich installieren und starten.
- Der Discovery-Mechanismus (mDNS) konnte im ersten Versuch keine Verbindung zwischen IDEA-Instanz und installiertem Gerät herstellen.
## Changes
- `gradle.properties` & `build.gradle.kts`: Globale JVM-Flags für JDK 25.
- `frontend/shells/meldestelle-desktop/main.kt`: Robuste Initialisierung & Koin-Fix.
- `DeviceInitializationScreen.kt` & Configs: Frame-safe Focus-Handling.
- `conveyor.conf`: Korrektur der JDK- und Icon-Pfads.
- `docs/02_Guides/Desktop-Packaging-Guide.md`: Neue Anleitung für Installer-Builds.
- `docs/90_Reports/Network-POC-Testplan.md`: Neuer Testplan für die Vernetzung.
- `setup-firewall-linux.sh`: Hilfsskript für Netzwerk-Ports.
## Verification
- **Build:** SUCCESSFUL (Gradle 9.5.0 / JDK 25) ✓.
- **UI:** Keine Fokus-Warnungen mehr beim Start ✓.
- **Packaging:** RPM-Build erfolgreich und lauffähig ✓.
- **Netzwerk:** Discovery fehlgeschlagen (Untersuchung morgen) ❌.
## Nächste Schritte
1. Debugging der mDNS-Discovery (mögliche Ursache: Fedora 44 KDE Firewall-Besonderheiten oder IPv6-Konflikte).
2. Analyse des Startup-Fehlers des Conveyor `tar.gz` Pakets.
3. Wiederaufnahme der physischen Turnier-Hierarchie (Meilenstein 1), sobald die Vernetzung steht.
@@ -2,12 +2,6 @@ package at.mocode.frontend.core.localdb
import org.koin.dsl.module import org.koin.dsl.module
/**
* Thin wrapper around SQLDelight `AppDatabase` creation.
*
* The platform-specific part is the `DatabaseDriverFactory` (expect/actual),
* which provides the appropriate SQLDelight driver (JVM sqlite driver, JS WebWorkerDriver, ...).
*/
class DatabaseProvider( class DatabaseProvider(
private val driverFactory: DatabaseDriverFactory private val driverFactory: DatabaseDriverFactory
) { ) {
@@ -17,9 +11,6 @@ class DatabaseProvider(
} }
} }
/**
* Koin module to provide the SQLDelight database for all frontend targets.
*/
val localDbModule = module { val localDbModule = module {
single<DatabaseDriverFactory> { DatabaseDriverFactory() } single<DatabaseDriverFactory> { DatabaseDriverFactory() }
single<DatabaseProvider> { DatabaseProvider(get()) } single<DatabaseProvider> { DatabaseProvider(get()) }
@@ -45,6 +45,7 @@ class JvmP2pSyncService : P2pSyncService {
} }
// Prozessweiter, portbasierter Guard // Prozessweiter, portbasierter Guard
println("[P2P Server] Versuche Port $port zu reservieren...")
if (!startedPorts.add(port)) { if (!startedPorts.add(port)) {
println("[P2P Server] Bereits gestartet (Prozess) auf Port $port idempotent, kein neuer Bind") println("[P2P Server] Bereits gestartet (Prozess) auf Port $port idempotent, kein neuer Bind")
return return
@@ -79,7 +80,6 @@ class JvmP2pSyncService : P2pSyncService {
} }
}.start(wait = false) }.start(wait = false)
currentPort = port currentPort = port
println("[P2P Server] Gestartet auf Port $port")
} catch (e: Exception) { } catch (e: Exception) {
// Start fehlgeschlagen -> Port-Lock wieder freigeben // Start fehlgeschlagen -> Port-Lock wieder freigeben
startedPorts.remove(port) startedPorts.remove(port)
@@ -33,6 +33,8 @@ import androidx.compose.ui.unit.sp
import at.mocode.frontend.features.device.initialization.domain.DeviceInitializationValidator import at.mocode.frontend.features.device.initialization.domain.DeviceInitializationValidator
import at.mocode.frontend.features.device.initialization.domain.model.AppThemeSetting import at.mocode.frontend.features.device.initialization.domain.model.AppThemeSetting
import at.mocode.frontend.features.device.initialization.domain.model.NetworkRole import at.mocode.frontend.features.device.initialization.domain.model.NetworkRole
import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.milliseconds
@Composable @Composable
private fun DiscoveryRadar( private fun DiscoveryRadar(
@@ -94,7 +96,7 @@ fun DeviceInitializationScreen(
// Automatische Discovery starten // Automatische Discovery starten
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.startDiscovery() viewModel.startDiscovery()
roleSelectorFocus.requestFocus() delay(100.milliseconds); withFrameMillis { roleSelectorFocus.requestFocus() }
} }
Surface( Surface(
@@ -48,12 +48,6 @@ actual fun DeviceInitializationConfig(
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val (_, sharedKeyFocus, backupPathFocus, clientNameFocus, clientRoleFocus) = remember { FocusRequester.createRefs() } val (_, sharedKeyFocus, backupPathFocus, clientNameFocus, clientRoleFocus) = remember { FocusRequester.createRefs() }
LaunchedEffect(Unit) {
if (settings.deviceName.isEmpty()) {
deviceNameFocus.requestFocus()
}
}
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.large, shape = MaterialTheme.shapes.large,
@@ -66,7 +60,7 @@ actual fun DeviceInitializationConfig(
value = settings.deviceName, value = settings.deviceName,
onValueChange = { viewModel.updateSettings { s -> s.copy(deviceName = it) } }, onValueChange = { viewModel.updateSettings { s -> s.copy(deviceName = it) } },
label = "Gerätename", label = "Gerätename",
helpDescription = "Ein eindeutiger Name für diesen PC (z.B. 'Richter-Springplatz').", helpDescription = "Ein eindeutiger Name für diesen PC (z.B. Richter-Springplatz).",
placeholder = "z.B. Meldestelle-PC-1", placeholder = "z.B. Meldestelle-PC-1",
isError = settings.deviceName.isNotEmpty() && !DeviceInitializationValidator.isNameValid(settings.deviceName), isError = settings.deviceName.isNotEmpty() && !DeviceInitializationValidator.isNameValid(settings.deviceName),
errorMessage = "Mindestens ${DeviceInitializationValidator.MIN_NAME_LENGTH} Zeichen erforderlich.", errorMessage = "Mindestens ${DeviceInitializationValidator.MIN_NAME_LENGTH} Zeichen erforderlich.",
@@ -77,24 +71,43 @@ actual fun DeviceInitializationConfig(
compact = true compact = true
) )
// NETZWERK-INTERFACES (EXPERTEN-MODUS)
val interfaces = remember { val interfaces = remember {
NetworkInterface.getNetworkInterfaces().toList() NetworkInterface.getNetworkInterfaces().toList()
.filter { it.isUp && !it.isLoopback && it.inetAddresses.hasMoreElements() } .filter { it.isUp && !it.isLoopback && it.inetAddresses.hasMoreElements() && !it.name.startsWith("br-") && !it.name.startsWith("docker") && !it.name.startsWith("veth") }
.map { ni -> .map { ni ->
val friendlyName = when { val friendlyName = when {
ni.displayName.contains("wlan", ignoreCase = true) || ni.displayName.contains("wi-fi", ignoreCase = true) || ni.name.contains("wlan", ignoreCase = true) -> "🌐 WLAN" ni.displayName.contains("wlan", ignoreCase = true) || ni.displayName.contains(
ni.displayName.contains("eth", ignoreCase = true) || ni.displayName.contains("ethernet", ignoreCase = true) || ni.name.contains("eth", ignoreCase = true) || ni.name.contains("en", ignoreCase = true) -> "🔌 Ethernet" "wi-fi",
ignoreCase = true
) || ni.name.contains("wlan", ignoreCase = true) -> "🌐 WLAN"
ni.displayName.contains("eth", ignoreCase = true) || ni.displayName.contains(
"ethernet",
ignoreCase = true
) || ni.name.contains("eth", ignoreCase = true) || ni.name.contains(
"en",
ignoreCase = true
) -> "🔌 Ethernet"
else -> "💻 " + ni.displayName else -> "💻 " + ni.displayName
} }
val address = ni.inetAddresses.asSequence().firstOrNull { !it.isLinkLocalAddress && it.hostAddress.indexOf(':') == -1 }?.hostAddress val address = ni.inetAddresses.asSequence()
.firstOrNull { !it.isLinkLocalAddress && it.hostAddress.indexOf(":") == -1 }?.hostAddress
?: ni.inetAddresses.nextElement().hostAddress ?: ni.inetAddresses.nextElement().hostAddress
val isConnected = !ni.isLoopback && ni.isUp && ni.interfaceAddresses.any { val isConnected = !ni.isLoopback && ni.isUp && ni.interfaceAddresses.any {
it.address.isSiteLocalAddress || it.address.hostAddress.startsWith("192.168") || it.address.hostAddress.startsWith("10.") it.address.isSiteLocalAddress || it.address.hostAddress.startsWith("192.168") || it.address.hostAddress.startsWith(
"10."
)
} }
InterfaceInfo(id = "$friendlyName ($address)", name = friendlyName, address = address, hardwareName = ni.name, isConnected = isConnected) InterfaceInfo(
id = "$friendlyName ($address)",
name = friendlyName,
address = address,
hardwareName = ni.name,
isConnected = isConnected
)
} }
} }
@@ -120,12 +133,17 @@ actual fun DeviceInitializationConfig(
Surface( Surface(
onClick = { if (!uiState.isLocked) viewModel.updateSettings { s -> s.copy(networkInterface = info.id) } }, onClick = { if (!uiState.isLocked) viewModel.updateSettings { s -> s.copy(networkInterface = info.id) } },
shape = MaterialTheme.shapes.medium, shape = MaterialTheme.shapes.medium,
color = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f), color = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant.copy(
alpha = 0.3f
),
border = if (isSelected) BorderStroke(2.dp, MaterialTheme.colorScheme.primary) else null, border = if (isSelected) BorderStroke(2.dp, MaterialTheme.colorScheme.primary) else null,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) { Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
Box(Modifier.size(10.dp).background(if (info.isConnected) Color(0xFF4CAF50) else Color(0xFFF44336), CircleShape)) Box(
Modifier.size(10.dp)
.background(if (info.isConnected) Color(0xFF4CAF50) else Color(0xFFF44336), CircleShape)
)
Spacer(Modifier.width(12.dp)) Spacer(Modifier.width(12.dp))
Column(Modifier.weight(1f)) { Column(Modifier.weight(1f)) {
Text(info.name, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold) Text(info.name, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold)
@@ -137,13 +155,12 @@ actual fun DeviceInitializationConfig(
} }
} }
// SICHERHEITSSCHLÜSSEL
var passwordVisible by remember { mutableStateOf(false) } var passwordVisible by remember { mutableStateOf(false) }
MsTextField( MsTextField(
value = settings.sharedKey, value = settings.sharedKey,
onValueChange = { viewModel.updateSettings { s -> s.copy(sharedKey = it) } }, onValueChange = { viewModel.updateSettings { s -> s.copy(sharedKey = it) } },
label = "Sicherheitsschlüssel (Sync-Key)", label = "Sicherheitsschlüssel (Sync-Key)",
helpDescription = "Das 'Turnier-Passwort'. Muss auf allen Geräten gleich sein.", helpDescription = "Das Turnier-Passwort. Muss auf allen Geräten gleich sein.",
placeholder = "Mindestens 8 Zeichen", placeholder = "Mindestens 8 Zeichen",
isError = settings.sharedKey.isNotEmpty() && !DeviceInitializationValidator.isKeyValid(settings.sharedKey), isError = settings.sharedKey.isNotEmpty() && !DeviceInitializationValidator.isKeyValid(settings.sharedKey),
errorMessage = "Mindestens ${DeviceInitializationValidator.MIN_KEY_LENGTH} Zeichen erforderlich.", errorMessage = "Mindestens ${DeviceInitializationValidator.MIN_KEY_LENGTH} Zeichen erforderlich.",
@@ -157,7 +174,6 @@ actual fun DeviceInitializationConfig(
compact = true compact = true
) )
// CLIENT-VERBINDUNG-FEEDBACK
if (settings.networkRole == NetworkRole.CLIENT && !uiState.isLocked) { if (settings.networkRole == NetworkRole.CLIENT && !uiState.isLocked) {
val masterSelected = uiState.selectedMaster != null val masterSelected = uiState.selectedMaster != null
val canConnect = masterSelected && settings.sharedKey.isNotBlank() val canConnect = masterSelected && settings.sharedKey.isNotBlank()
@@ -170,13 +186,19 @@ actual fun DeviceInitializationConfig(
else -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.1f) else -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.1f)
}, },
shape = MaterialTheme.shapes.medium, shape = MaterialTheme.shapes.medium,
border = BorderStroke(1.dp, when (uiState.connectionStatus) { border = BorderStroke(
1.dp, when (uiState.connectionStatus) {
ConnectionStatus.CONNECTED -> Color(0xFF4CAF50) ConnectionStatus.CONNECTED -> Color(0xFF4CAF50)
ConnectionStatus.FAILED -> Color(0xFFF44336) ConnectionStatus.FAILED -> Color(0xFFF44336)
else -> MaterialTheme.colorScheme.outlineVariant else -> MaterialTheme.colorScheme.outlineVariant
}) }
)
) {
Column(
Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
Column(Modifier.padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
when (uiState.connectionStatus) { when (uiState.connectionStatus) {
ConnectionStatus.CONNECTING -> CircularProgressIndicator(Modifier.size(20.dp), strokeWidth = 2.dp) ConnectionStatus.CONNECTING -> CircularProgressIndicator(Modifier.size(20.dp), strokeWidth = 2.dp)
@@ -210,7 +232,6 @@ actual fun DeviceInitializationConfig(
} }
} }
// BACKUP & DRUCKER
MsFilePicker( MsFilePicker(
label = "Backup-Verzeichnis (Plan-USB)", label = "Backup-Verzeichnis (Plan-USB)",
selectedPath = settings.backupPath, selectedPath = settings.backupPath,
@@ -246,10 +267,13 @@ actual fun DeviceInitializationConfig(
) )
} }
// MASTER: ERWARTETE CLIENTS
if (settings.networkRole == NetworkRole.MASTER && !uiState.isLocked) { if (settings.networkRole == NetworkRole.MASTER && !uiState.isLocked) {
HorizontalDivider(Modifier.padding(vertical = 8.dp)) HorizontalDivider(Modifier.padding(vertical = 8.dp))
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text("👥 Erwartete Clients", style = MaterialTheme.typography.titleSmall) Text("👥 Erwartete Clients", style = MaterialTheme.typography.titleSmall)
TextButton(onClick = { viewModel.addExpectedClient() }) { TextButton(onClick = { viewModel.addExpectedClient() }) {
Icon(Icons.Default.Add, null, Modifier.size(18.dp)) Icon(Icons.Default.Add, null, Modifier.size(18.dp))
@@ -295,7 +319,12 @@ actual fun DeviceInitializationConfig(
}, },
trailingContent = { trailingContent = {
IconButton(onClick = { viewModel.removeExpectedClient(index) }) { IconButton(onClick = { viewModel.removeExpectedClient(index) }) {
Icon(Icons.Default.Delete, null, tint = MaterialTheme.colorScheme.error, modifier = Modifier.size(20.dp)) Icon(
Icons.Default.Delete,
null,
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(20.dp)
)
} }
}, },
colors = ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)), colors = ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)),
@@ -307,4 +336,10 @@ actual fun DeviceInitializationConfig(
} }
} }
private data class InterfaceInfo(val id: String, val name: String, val address: String, val hardwareName: String, val isConnected: Boolean) private data class InterfaceInfo(
val id: String,
val name: String,
val address: String,
val hardwareName: String,
val isConnected: Boolean
)
@@ -2,9 +2,6 @@
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
/**
* Dieses Modul kapselt die gesamte UI und Logik für das Ping-Feature.
*/
plugins { plugins {
alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeMultiplatform)
@@ -17,14 +14,9 @@ version = "1.0.0"
kotlin { kotlin {
jvm() jvm()
wasmJs { wasmJs {
binaries.library() binaries.library()
browser { browser { testTask { enabled = false } }
testTask {
enabled = false
}
}
} }
sourceSets { sourceSets {
@@ -33,12 +25,13 @@ kotlin {
implementation(projects.frontend.core.designSystem) implementation(projects.frontend.core.designSystem)
implementation(projects.frontend.core.sync) implementation(projects.frontend.core.sync)
implementation(projects.frontend.core.localDb) implementation(projects.frontend.core.localDb)
implementation(projects.frontend.core.auth) // Added auth module for AuthTokenManager implementation(projects.frontend.core.auth)
implementation(libs.sqldelight.coroutines) implementation(libs.sqldelight.coroutines)
implementation(projects.frontend.core.domain) implementation(projects.frontend.core.domain)
implementation(compose.foundation) // Explizite Compose-Abhängigkeiten zur Vermeidung von Gradle 10 Warnungen
implementation(compose.runtime) implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3) implementation(compose.material3)
implementation(compose.ui) implementation(compose.ui)
implementation(compose.components.resources) implementation(compose.components.resources)
@@ -49,7 +42,7 @@ kotlin {
implementation(libs.bundles.compose.common) implementation(libs.bundles.compose.common)
implementation(libs.koin.core) implementation(libs.koin.core)
implementation(libs.koin.compose) // Added koin.compose for koinInject implementation(libs.koin.compose)
} }
commonTest.dependencies { commonTest.dependencies {
@@ -72,6 +65,5 @@ kotlin {
wasmJsMain.dependencies { wasmJsMain.dependencies {
implementation(libs.kotlin.stdlib.wasm.js) implementation(libs.kotlin.stdlib.wasm.js)
} }
} }
} }
@@ -1,21 +1,6 @@
import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import java.util.* import java.util.*
/**
* Shell-Modul: Meldestelle Desktop App
* Reines JVM/Compose-Desktop-Modul Desktop-First gemäß MASTER_ROADMAP.
* Setzt alle Core- und Feature-Module zu einer lauffähigen Desktop-Anwendung zusammen.
*
* Packaging:
* ./gradlew :frontend:shells:meldestelle-desktop:packageDeb → Linux .deb
* ./gradlew :frontend:shells:meldestelle-desktop:packageMsi → Windows .msi
* ./gradlew :frontend:shells:meldestelle-desktop:packageDmg → macOS .dmg
* ./gradlew :frontend:shells:meldestelle-desktop:packageReleaseDistributables → alle Plattformen
*
* Version: Wird automatisch aus version.properties im Root-Projekt gelesen (SemVer).
* Icons: src/jvmMain/resources/icon.png / icon.ico / icon.icns
* → siehe ICONS_PLACEHOLDER.md für Anforderungen
*/
plugins { plugins {
alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.composeCompiler) alias(libs.plugins.composeCompiler)
@@ -26,16 +11,12 @@ plugins {
group = "at.mocode.frontend.shell" group = "at.mocode.frontend.shell"
version = "1.0.0" version = "1.0.0"
// ---------------------------------------------------------------
// Version aus root version.properties lesen (SemVer)
// ---------------------------------------------------------------
val versionProps = Properties().also { props -> val versionProps = Properties().also { props ->
rootProject.file("version.properties").inputStream().use { props.load(it) } rootProject.file("version.properties").inputStream().use { props.load(it) }
} }
val vMajor: String? = versionProps.getProperty("VERSION_MAJOR", "1") val vMajor: String? = versionProps.getProperty("VERSION_MAJOR", "1")
val vMinor: String? = versionProps.getProperty("VERSION_MINOR", "0") val vMinor: String? = versionProps.getProperty("VERSION_MINOR", "0")
val vPatch: String? = versionProps.getProperty("VERSION_PATCH", "0") val vPatch: String? = versionProps.getProperty("VERSION_PATCH", "0")
// nativeDistributions erwartet reines "MAJOR.MINOR.PATCH" (kein Qualifier)
val packageVer = "$vMajor.$vMinor.$vPatch" val packageVer = "$vMajor.$vMinor.$vPatch"
kotlin { kotlin {
@@ -43,7 +24,6 @@ kotlin {
sourceSets { sourceSets {
jvmMain.dependencies { jvmMain.dependencies {
// Core-Module
implementation(projects.frontend.core.domain) implementation(projects.frontend.core.domain)
implementation(projects.core.coreDomain) implementation(projects.core.coreDomain)
implementation(projects.frontend.core.designSystem) implementation(projects.frontend.core.designSystem)
@@ -54,10 +34,8 @@ kotlin {
implementation(projects.frontend.core.auth) implementation(projects.frontend.core.auth)
implementation(projects.core.znsParser) implementation(projects.core.znsParser)
// Feature-Module
implementation(projects.frontend.features.pingFeature) implementation(projects.frontend.features.pingFeature)
implementation(projects.frontend.features.nennungFeature) implementation(projects.frontend.features.nennungFeature)
implementation(projects.frontend.features.znsImportFeature) implementation(projects.frontend.features.znsImportFeature)
implementation(projects.frontend.features.veranstalterFeature) implementation(projects.frontend.features.veranstalterFeature)
implementation(projects.frontend.features.veranstaltungFeature) implementation(projects.frontend.features.veranstaltungFeature)
@@ -70,7 +48,6 @@ kotlin {
implementation(projects.frontend.features.billingFeature) implementation(projects.frontend.features.billingFeature)
implementation(projects.frontend.features.deviceInitialization) implementation(projects.frontend.features.deviceInitialization)
// Compose Desktop
implementation(compose.desktop.currentOs) implementation(compose.desktop.currentOs)
implementation(compose.runtime) implementation(compose.runtime)
implementation(compose.foundation) implementation(compose.foundation)
@@ -80,17 +57,13 @@ kotlin {
implementation(compose.uiTooling) implementation(compose.uiTooling)
implementation(libs.composeHotReloadApi) implementation(libs.composeHotReloadApi)
// DI (Koin)
implementation(libs.koin.core) implementation(libs.koin.core)
implementation(libs.koin.compose) implementation(libs.koin.compose)
implementation(libs.koin.compose.viewmodel) implementation(libs.koin.compose.viewmodel)
// Coroutines
implementation(libs.kotlinx.coroutines.swing) implementation(libs.kotlinx.coroutines.swing)
// Bundles
implementation(libs.bundles.kmp.common) implementation(libs.bundles.kmp.common)
implementation(libs.bundles.compose.common) implementation(libs.bundles.compose.common)
implementation(libs.logback.classic)
} }
jvmTest.dependencies { jvmTest.dependencies {
@@ -104,12 +77,8 @@ compose.desktop {
mainClass = "at.mocode.frontend.shell.desktop.MainKt" mainClass = "at.mocode.frontend.shell.desktop.MainKt"
nativeDistributions { nativeDistributions {
// Ziel-Formate: Linux .deb, Windows .msi, macOS .dmg targetFormats(TargetFormat.Deb, TargetFormat.Rpm, TargetFormat.Msi, TargetFormat.Dmg)
targetFormats(TargetFormat.Deb, TargetFormat.Msi, TargetFormat.Dmg)
// -------------------------------------------------------
// Gemeinsame App-Metadaten
// -------------------------------------------------------
packageName = "meldestelle" packageName = "meldestelle"
packageVersion = packageVer packageVersion = packageVer
description = "ÖTO-konforme Turnier-Meldestelle Desktop App" description = "ÖTO-konforme Turnier-Meldestelle Desktop App"
@@ -117,53 +86,30 @@ compose.desktop {
copyright = "© 20242026 mo-code.at. Alle Rechte vorbehalten." copyright = "© 20242026 mo-code.at. Alle Rechte vorbehalten."
licenseFile.set(rootProject.file("LICENSE")) licenseFile.set(rootProject.file("LICENSE"))
// -------------------------------------------------------
// Linux (.deb)
// -------------------------------------------------------
linux { linux {
// PNG 512×512 px — siehe src/jvmMain/resources/ICONS_PLACEHOLDER.md
iconFile.set(project.file("src/jvmMain/resources/icon.png")) iconFile.set(project.file("src/jvmMain/resources/icon.png"))
packageName = "meldestelle" packageName = "meldestelle"
// Debian-Kategorie
appCategory = "misc" appCategory = "misc"
// Menü-Eintrag
menuGroup = "Meldestelle" menuGroup = "Meldestelle"
shortcut = true shortcut = true
debMaintainer = "support@mo-code.at" debMaintainer = "support@mo-code.at"
} }
// -------------------------------------------------------
// Windows (.msi)
// -------------------------------------------------------
windows { windows {
// ICO Multi-Size — siehe src/jvmMain/resources/ICONS_PLACEHOLDER.md
iconFile.set(project.file("src/jvmMain/resources/icon.ico")) iconFile.set(project.file("src/jvmMain/resources/icon.ico"))
// Eindeutige GUID für Windows Installer Upgrade-Erkennung
// WICHTIG: Diese UUID darf sich NIE ändern!
upgradeUuid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890" upgradeUuid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
menuGroup = "Meldestelle" menuGroup = "Meldestelle"
// Startmenü-Verknüpfung
shortcut = true shortcut = true
// Desktop-Verknüpfung
dirChooser = true dirChooser = true
perUserInstall = false perUserInstall = false
} }
// -------------------------------------------------------
// macOS (.dmg)
// -------------------------------------------------------
macOS { macOS {
// ICNS 1024×1024 px — siehe src/jvmMain/resources/ICONS_PLACEHOLDER.md
iconFile.set(project.file("src/jvmMain/resources/icon.icns")) iconFile.set(project.file("src/jvmMain/resources/icon.icns"))
bundleID = "at.mocode.meldestelle" bundleID = "at.mocode.meldestelle"
appCategory = "public.app-category.productivity" appCategory = "public.app-category.productivity"
// Für notarisierten Release: signing-Konfiguration hier ergänzen
// signing { sign.set(true); identity.set("Developer ID Application: ...") }
} }
// -------------------------------------------------------
// JVM-Laufzeit-Konfiguration (eingebettetes JRE)
// -------------------------------------------------------
modules( modules(
"java.base", "java.base",
"java.desktop", "java.desktop",
@@ -176,8 +122,8 @@ compose.desktop {
) )
} }
// JVM-Argumente für die gepackte Anwendung
jvmArgs( jvmArgs(
"--enable-native-access=ALL-UNNAMED",
"-Xms128m", "-Xms128m",
"-Xmx512m", "-Xmx512m",
"-Dfile.encoding=UTF-8", "-Dfile.encoding=UTF-8",
@@ -8,7 +8,6 @@ import at.mocode.frontend.core.auth.di.authModule
import at.mocode.frontend.core.localdb.AppDatabase import at.mocode.frontend.core.localdb.AppDatabase
import at.mocode.frontend.core.localdb.DatabaseProvider import at.mocode.frontend.core.localdb.DatabaseProvider
import at.mocode.frontend.core.localdb.localDbModule import at.mocode.frontend.core.localdb.localDbModule
import at.mocode.frontend.core.network.NetworkConfig
import at.mocode.frontend.core.network.chat.KtorWebSocketServerService import at.mocode.frontend.core.network.chat.KtorWebSocketServerService
import at.mocode.frontend.core.network.discovery.NetworkDiscoveryService import at.mocode.frontend.core.network.discovery.NetworkDiscoveryService
import at.mocode.frontend.core.network.networkModule import at.mocode.frontend.core.network.networkModule
@@ -62,42 +61,32 @@ fun main() = application {
desktopModule, desktopModule,
) )
} }
println("[DesktopApp] KOIN initialisiert")
// Base URL Log für schnelle Fehlerdiagnose
println("[Network] baseUrl=${NetworkConfig.baseUrl}")
// Starte Netzwerk-Dienste für den POC // Datenbank EAGER initialisieren (JVM-safe via runBlocking)
val koin = GlobalContext.get() val koin = GlobalContext.get()
val dbProvider = koin.get<DatabaseProvider>()
val database = runBlocking { dbProvider.createDatabase() }
loadKoinModules(module {
single<AppDatabase> { database }
})
println("[DesktopApp] KOIN & DB initialisiert")
// Start POC Netzwerk-Dienste
try { try {
val wsServer = koin.get<KtorWebSocketServerService>() val wsServer = koin.get<KtorWebSocketServerService>()
wsServer.start() wsServer.start()
val discovery = koin.get<NetworkDiscoveryService>() val discovery = koin.get<NetworkDiscoveryService>()
discovery.startDiscovery() discovery.startDiscovery()
// Im Host-Modus würden wir hier registerService aufrufen.
// Für den POC registrieren wir den lokalen Host-Dienst immer mit dem WS-Port
try {
discovery.registerService(wsServer.getPort()) discovery.registerService(wsServer.getPort())
println("[DesktopApp] Discovery-Registrierung durchgeführt (Port ${wsServer.getPort()})")
} catch (e: Exception) {
println("[DesktopApp] Discovery-Registrierung fehlgeschlagen: ${e.message}")
}
} catch(e: Exception) { } catch(e: Exception) {
println("[DesktopApp] POC-Dienste konnten nicht gestartet werden: ${e.message}") println("[DesktopApp] Netzwerk-Dienste Fehler: %s".format(e.message))
} }
// Testdaten für Prototyp laden
at.mocode.frontend.shell.desktop.data.Store.seed() at.mocode.frontend.shell.desktop.data.Store.seed()
} catch (e: Exception) { } catch (e: Exception) {
println("[DesktopApp] Koin-Warnung: ${e.message}") println("[DesktopApp] Startup-Fehler: %s".format(e.message))
}
try {
val provider = GlobalContext.get().get<DatabaseProvider>()
val db = runBlocking { provider.createDatabase() }
loadKoinModules(module { single<AppDatabase> { db } })
println("[DesktopApp] Lokale DB bereit")
} catch (e: Exception) {
println("[DesktopApp] DB-Warnung: ${e.message}")
} }
Window( Window(
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 13 KiB

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
<!-- JmDNS ist extrem gesprächig auf DEBUG/TRACE -->
<logger name="javax.jmdns" level="INFO"/>
<logger name="io.netty" level="INFO"/>
</configuration>
+14 -26
View File
@@ -4,31 +4,29 @@ android.nonTransitiveRClass=true
# Kotlin Configuration # Kotlin Configuration
kotlin.code.style=official kotlin.code.style=official
# Increased Kotlin Daemon Heap for JS Compilation # Increased Kotlin Daemon Heap for JS Compilation + JDK 25 Warning Suppression
kotlin.daemon.jvmargs=-Xmx8g -XX:+UseParallelGC -XX:MaxMetaspaceSize=2g kotlin.daemon.jvmargs=-Xmx8g -XX:+UseParallelGC -XX:MaxMetaspaceSize=2g --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --enable-native-access=ALL-UNNAMED
kotlin.js.compiler.sourcemaps=false kotlin.js.compiler.sourcemaps=false
# Kotlin Compiler Optimizations (Phase 5) # Kotlin Compiler Optimizations
kotlin.incremental=true kotlin.incremental=true
kotlin.incremental.multiplatform=true kotlin.incremental.multiplatform=true
kotlin.incremental.js=true kotlin.incremental.js=true
kotlin.caching.enabled=true kotlin.caching.enabled=true
kotlin.compiler.execution.strategy=in-process kotlin.compiler.execution.strategy=in-process
# kotlin.compiler.preciseCompilationResultsBackup=true
kotlin.stdlib.default.dependency=true kotlin.stdlib.default.dependency=true
# Gradle Configuration # Gradle Configuration
# Increased Gradle Daemon Heap # Optimized for JDK 25: Added --add-opens and --enable-native-access for compiler tools
org.gradle.jvmargs=-Xmx8g -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx6g" -XX:+UseParallelGC -XX:MaxMetaspaceSize=2g -XX:+HeapDumpOnOutOfMemoryError -Xshare:off -Djava.awt.headless=true org.gradle.jvmargs=-Xmx8g -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=2g -XX:+HeapDumpOnOutOfMemoryError -Xshare:off -Djava.awt.headless=true --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED --enable-native-access=ALL-UNNAMED -Djdk.instrument.traceUsage=false
org.gradle.workers.max=8 org.gradle.workers.max=8
org.gradle.vfs.watch=true org.gradle.vfs.watch=true
# Configuration Cache optimieren - TEMPORÄR DEAKTIVIERT wegen JS-Test Serialisierungsproblemen # Configuration Cache (JS-Test workaround)
org.gradle.configuration-cache=false org.gradle.configuration-cache=false
org.gradle.configuration-cache.problems=warn org.gradle.configuration-cache.problems=warn
# Build Performance verbessern # Build Performance
org.gradle.parallel=true org.gradle.parallel=true
org.gradle.caching=true org.gradle.caching=true
@@ -46,7 +44,7 @@ org.jetbrains.kotlin.wasm.check.wasm.binary.format=false
kotlin.native.ignoreDisabledTargets=true kotlin.native.ignoreDisabledTargets=true
idea.project.settings.delegate.build.run.actions.to.gradle=true idea.project.settings.delegate.build.run.actions.to.gradle=true
# Enable NPM/Yarn lifecycle scripts for Kotlin/JS (required for sql.js & worker setup) # NPM/Yarn lifecycle
kotlin.js.yarn.ignoreScripts=false kotlin.js.yarn.ignoreScripts=false
org.jetbrains.kotlin.js.yarn.ignoreScripts=false org.jetbrains.kotlin.js.yarn.ignoreScripts=false
kotlin.js.npm.ignoreScripts=false kotlin.js.npm.ignoreScripts=false
@@ -56,31 +54,21 @@ org.jetbrains.kotlin.js.npm.ignoreScripts=false
org.gradle.logging.level=lifecycle org.gradle.logging.level=lifecycle
kotlin.build.report.single_file=false kotlin.build.report.single_file=false
# Compose Experimental Features # Compose Experimental
org.jetbrains.compose.experimental.jscanvas.enabled=true org.jetbrains.compose.experimental.jscanvas.enabled=true
org.jetbrains.compose.experimental.wasm.enabled=true org.jetbrains.compose.experimental.wasm.enabled=true
# Java Toolchain: ensure Gradle auto-downloads a full JDK when needed # Java Toolchain
org.gradle.java.installations.auto-download=true org.gradle.java.installations.auto-download=true
org.gradle.java.installations.auto-detect=true org.gradle.java.installations.auto-detect=true
# Development Environment Support # Feature Toggles
dev.port.offset=0
# Set dev.port.offset=100 for second developer
# Set dev.port.offset=200 for the third developer
# ------------------------------------------------------------------
# Wasm/JS Feature Toggle
# ------------------------------------------------------------------
# Setze enableWasm=true, um die Web-App zu bauen oder Web-spezifische
# Module zu testen. Default=false spart massiv Zeit beim Desktop-Build.
enableWasm=true enableWasm=true
enableDesktop=true enableDesktop=true
dev.port.offset=0
# Dokka Gradle plugin V2 mode (with helpers for V1 compatibility) # Dokka V2
# See https://kotl.in/dokka-gradle-migration
# org.jetbrains.dokka.experimental.gradle.pluginMode=V2EnabledWithHelpers
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
# Workaround for Gradle 9 / KMP "Plugin loaded multiple times" error in Docker/CI # Gradle 9 Workaround
# This allows subprojects to re-declare plugins even if they are already on the classpath
kotlin.mpp.allowMultiplePluginDeclarations=true kotlin.mpp.allowMultiplePluginDeclarations=true
+962
View File
@@ -0,0 +1,962 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
<meta name="csrf-token" content="pTASJlt042Vo3XAFbvffgDFlRJIGLAVOJ9LcEFQe"/>
<meta name="theme-color" content="#ea580c">
<meta name="title" content="CHANGELOG.md - Realease notes &amp; Feedback Management Tool">
<meta name="description"
content="Product feedback collection, analyze insights, build actionable roadmaps, and keep users informed with CHANGELOG.md">
<meta name="keywords"
content="product feedback, SaaS feedback platform, product roadmap tool, product updates, changelog software">
<link rel="alternate" hreflang="x-default" href="http://changelog.md"/>
<meta name="language" content="en">
<meta name="author" content="CHANGELOG.md - Realease notes &amp; Feedback Management Tool">
<meta property="og:url" content="http://changelog.md">
<meta property="og:image" content="https://changelog.md/storage/logo/social_share.jpg">
<meta property="og:site_name" content="CHANGELOG.md - Realease notes &amp; Feedback Management Tool">
<meta property="og:type" content="website">
<meta property="og:title" content="CHANGELOG.md - Realease notes &amp; Feedback Management Tool">
<meta property="og:description"
content="Product feedback collection, analyze insights, build actionable roadmaps, and keep users informed with CHANGELOG.md">
<meta property="og:image:width" content="600">
<meta property="og:image:height" content="315">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="CHANGELOG.md - Realease notes &amp; Feedback Management Tool">
<meta name="twitter:image:src" content="https://changelog.md/storage/logo/social_share.jpg">
<meta name="twitter:description"
content="Product feedback collection, analyze insights, build actionable roadmaps, and keep users informed with CHANGELOG.md">
<meta name="theme" content="classic">
<title>CHANGELOG.md - Realease notes & Feedback Management Tool</title>
<link rel="shortcut icon" type="image/x-icon" href="https://changelog.md/storage/logo/favicon.png">
<style>
:root {
--color-primary: #ea580c !important;
--theme-color-rgb: 234, 88, 12 !important;
--color-primary-l: rgba(var(--theme-color-rgb), 0.08) !important;
}
</style>
<link rel="stylesheet" href="https://changelog.md/assets/global/fonts/css/fontawesome.css">
<link rel="stylesheet" href="https://changelog.md/assets/templates/classic/plugin/owl-carousel/owl.carousel.min.css">
<link rel="stylesheet"
href="https://changelog.md/assets/templates/classic/plugin/owl-carousel/owl.theme.default.min.css">
<link rel="stylesheet" href="https://changelog.md/assets/templates/classic/plugin/magnific-poupup/magnific-popup.css">
<link rel="stylesheet" href="https://changelog.md/assets/templates/classic/plugin/simple-bar/simplebar.min.css">
<link rel="stylesheet" href="https://changelog.md/assets/templates/classic/plugin/text-typer/typing-text.css">
<link rel="stylesheet" href="https://changelog.md/assets/templates/classic/plugin/wow-animate/animate.css">
<link rel="stylesheet" href="https://changelog.md/assets/templates/classic/plugin/swiper/swiper-bundle.min.css">
<link rel="stylesheet" href="https://changelog.md/assets/templates/classic/plugin/snackbar/snackbar.min.css">
<link rel="stylesheet" href="https://changelog.md/assets/global/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="https://changelog.md/assets/templates/classic/css/style.css?ver=3.2.1">
<link rel="stylesheet" href="https://changelog.md/assets/templates/classic/css/responsive.css?ver=3.2.1">
<link rel="stylesheet" href="https://changelog.md/assets/templates/classic/css/app.css?ver=3.2.1">
<link rel="stylesheet" href="https://changelog.md/assets/templates/classic/css/custom.css?ver=3.2.1">
<link rel="stylesheet" href="https://changelog.md/assets/templates/classic/css/dark.css?ver=3.2.1">
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-157578943-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'UA-157578943-1');
</script>
<script type="text/javascript" class="flasher-js">(function () {
var rootScript = 'https://cdn.jsdelivr.net/npm/@flasher/flasher@1.3.1/dist/flasher.min.js';
var FLASHER_FLASH_BAG_PLACE_HOLDER = {};
var options = mergeOptions([], FLASHER_FLASH_BAG_PLACE_HOLDER);
function mergeOptions(first, second) {
return {
context: merge(first.context || {}, second.context || {}),
envelopes: merge(first.envelopes || [], second.envelopes || []),
options: merge(first.options || {}, second.options || {}),
scripts: merge(first.scripts || [], second.scripts || []),
styles: merge(first.styles || [], second.styles || []),
};
}
function merge(first, second) {
if (Array.isArray(first) && Array.isArray(second)) {
return first.concat(second).filter(function (item, index, array) {
return array.indexOf(item) === index;
});
}
return Object.assign({}, first, second);
}
function renderOptions(options) {
if (!window.hasOwnProperty('flasher')) {
console.error('Flasher is not loaded');
return;
}
requestAnimationFrame(function () {
window.flasher.render(options);
});
}
function render(options) {
if ('loading' !== document.readyState) {
renderOptions(options);
return;
}
document.addEventListener('DOMContentLoaded', function () {
renderOptions(options);
});
}
if (1 === document.querySelectorAll('script.flasher-js').length) {
document.addEventListener('flasher:render', function (event) {
render(event.detail);
});
}
if (window.hasOwnProperty('flasher') || !rootScript || document.querySelector('script[src="' + rootScript + '"]')) {
render(options);
} else {
var tag = document.createElement('script');
tag.setAttribute('src', rootScript);
tag.setAttribute('type', 'text/javascript');
tag.onload = function () {
render(options);
};
document.head.appendChild(tag);
}
})();</script>
</head>
<body>
<header>
<div class="navbar-area nav-light position-absolute">
<div class="desktop-nav">
<nav class="navbar navbar-expand-lg navbar-light">
<a class="navbar-brand" href="http://changelog.md">
<img class="white-logo" src="https://changelog.md/storage/logo/classic-theme_footer_logo.png"
alt="CHANGELOG.md - Realease notes &amp; Feedback Management Tool"/>
<img class="main-logo" src="https://changelog.md/storage/logo/classic-theme_logo.png"
alt="CHANGELOG.md - Realease notes &amp; Feedback Management Tool"/>
</a>
<div class="navbar-collapse offcanvas offcanvas-nav offcanvas-start" tabindex="-1" id="offcanvasExample"
aria-labelledby="offcanvasExampleLabel">
<div class="offcanvas-header d-lg-none">
<h3 class="navbar-brand offcanvas-title mb-0 font-24" id="offcanvasExampleLabel">
<img class="white-logo" src="https://changelog.md/storage/logo/classic-theme_footer_logo.png"
alt="CHANGELOG.md - Realease notes &amp; Feedback Management Tool"/>
<img class="main-logo" src="https://changelog.md/storage/logo/classic-theme_logo.png"
alt="CHANGELOG.md - Realease notes &amp; Feedback Management Tool"/>
</h3>
<button type="button" class="icon-group -secondary" data-bs-dismiss="offcanvas"
aria-label="Close">
<i class="fa-solid fa-xmark"></i></button>
</div>
<div class="offcanvas-body me-auto d-flex flex-column h-100">
<ul class="navbar-nav">
<li class="nav-item">
<a href="http://changelog.md" class="nav-link">
Home
</a>
</li>
<li class="nav-item">
<a href="http://changelog.md/faq" class="nav-link">
FAQs
</a>
</li>
<li class="nav-item">
<a href="http://changelog.md/blog" class="nav-link">
Blog
</a>
</li>
<li class="nav-item">
<a href="http://changelog.md/contact" class="nav-link">
Contact Us
</a>
</li>
<li class="nav-item d-block d-sm-none">
<a href="http://changelog.md/login" class="nav-link">
Log in
</a>
</li>
<li class="nav-item d-block d-sm-none">
<a href="http://changelog.md/signup" class="nav-link">
Sign up
</a>
</li>
<!--/ # When user logout or new user login signup button-->
</ul>
</div>
</div>
<div class="nav-others d-flex align-items-center">
<!--/ # When user logout or new user login signup button-->
<div class="d-flex justify-content-center">
<a href="http://changelog.md/login"
class="ml-16 button -secondary text-dark-1 px-15 rounded-pill fw-semibold font-16">Log in
</a>
<a href="http://changelog.md/signup"
class="ml-16 button bg-primary text-white px-15 rounded-pill fw-semibold font-16 d-none d-md-flex">Sign
up
</a>
</div>
<!--/ # When user logout or new user login signup button-->
<!--/ # On responsive hamburger menu button for offcanvas desktop nav-->
<div class="sidemenu-header ml-16 d-none">
<div class="responsive-burger-menu icon-group -secondary" data-bs-toggle="offcanvas"
data-bs-target="#offcanvasExample" aria-controls="offcanvasExample">
<i class="fa-solid fa-bars-staggered"></i>
</div>
</div>
<!--/ # On responsive hamburger menu button for offcanvas desktop nav-->
</div>
</nav>
</div>
</div>
</header>
<main class="pt-70">
<!--Hero section start-->
<section class="container py-60" data-cue="fadeIn">
<div class="row justify-content-center">
<div class="col-xl-8 col-lg-10 col-12" data-cues="zoomIn" data-group="page-title" data-delay="700">
<div class="text-center d-flex flex-column gap-4">
<div class="d-flex justify-content-center">
<span
class="bg-primary-l text-primary border-primary border px-3 py-2 font-14 rounded-pill lh-1 align-items-center d-flex">
<i class="fa-regular fa-bolt"></i>
<span class="ms-2 fw-semibold">Effortlessly collect feedback</span>
</span>
</div>
<div class="d-flex flex-column gap-3 mx-lg-5">
<h1 class="mb-0 display-4 fw-bold">Create better products driven by customer feedback</h1>
<p class="mb-0 lead">Simplify feedback collection, lighten support tasks, and share product updates—all in
one powerful tool.</p>
</div>
<div class="d-flex flex-row align-items-center gap-4 justify-content-center">
<a href="http://changelog.md/login" class="button -primary">Get Started</a>
<a href="https://changelog.md"
class="push-right text-primary d-flex align-items-center">
<span class="me-1">Explore product</span>
<i class="fa-light fa-arrow-right push-this"></i>
</a>
</div>
</div>
</div>
</div>
</section>
<div class="pattern-square"
style="background-image: url(https://changelog.md/assets/templates/classic/images/home/bg-pattern.png)"></div>
<section class="container py-60 xl-py-32">
<div class="row justify-content-center">
<div class="col-md-10 col-12">
<div class="text-center position-relative" data-cue="zoomIn">
<img src="https://changelog.md/assets/templates/classic/images/home/hero-app-screen.png"
class="img-fluid bg-light p-3 rounded-3 border" alt=""/>
<div class="position-absolute top-50 d-none d-lg-block ms-n5" data-cue="slideInLeft">
<img src="https://changelog.md/assets/templates/classic/images/home/hero-frame-left.svg" alt=""/>
</div>
<div class="position-absolute top-50 end-0 translate-middle me-n9 d-none d-lg-block">
<img src="https://changelog.md/assets/templates/classic/images/home/hero-frame-right-1.svg"
class="me-n9 mb-4" alt=""
data-cue="slideInRight"/>
<br/>
<img src="https://changelog.md/assets/templates/classic/images/home/hero-frame-right-2.svg" alt=""
data-cue="slideInRight"/>
</div>
</div>
</div>
</div>
</section>
<!--Hero section close-->
<!--Trusted worldwide start-->
<div class="py-30" data-cue="fadeIn">
<div class="container py-2">
<div class="row">
<div class="col-lg-10 offset-lg-1">
<div class="swiper-container swiper"
id="swiper-1"
data-pagination-type=""
data-speed="400"
data-space-between="100"
data-pagination="true"
data-navigation="false"
data-autoplay="true"
data-autoplay-delay="3000"
data-breakpoints='{"480": {"slidesPerView": 2}, "768": {"slidesPerView": 3}, "1024": {"slidesPerView": 5}}'>
<div class="swiper-wrapper pb-40">
<div class="swiper-slide">
<figure class="text-center">
<img src="https://changelog.md/storage/partner/clients-logo-1.svg"
alt="clients-logo-1.svg"/>
</figure>
</div>
<div class="swiper-slide">
<figure class="text-center">
<img src="https://changelog.md/storage/partner/clients-logo-2.svg"
alt="clients-logo-2.svg"/>
</figure>
</div>
<div class="swiper-slide">
<figure class="text-center">
<img src="https://changelog.md/storage/partner/clients-logo-3.svg"
alt="clients-logo-3.svg"/>
</figure>
</div>
<div class="swiper-slide">
<figure class="text-center">
<img src="https://changelog.md/storage/partner/clients-logo-4.svg"
alt="clients-logo-4.svg"/>
</figure>
</div>
<div class="swiper-slide">
<figure class="text-center">
<img src="https://changelog.md/storage/partner/clients-logo-5.svg"
alt="clients-logo-5.svg"/>
</figure>
</div>
</div>
<!-- Add Pagination -->
<div class="swiper-pagination"></div>
<!-- Add Navigation -->
<div class="swiper-navigation">
<div class="swiper-button-next"></div>
<div class="swiper-button-prev"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<!--Trusted worldwide end-->
<!--Feature to boost Start-->
<section class="features container py-60 xl-py-32" data-cue="fadeIn">
<div class="row justify-content-center mb-45 lg-mb-24">
<div class="col-xl-6 col-lg-10 col-12">
<div class="text-center d-flex flex-column gap-4">
<div class="d-flex justify-content-center">
<span
class="bg-primary-l text-primary border-primary border px-3 py-2 font-14 rounded-pill lh-1 align-items-center d-flex">
<i class="fa-regular fa-bolt"></i>
<span class="ms-1 text-uppercase ls-md fw-semibold">Features</span>
</span>
</div>
<div class="d-flex flex-column gap-3 mx-60 lg-mx-0">
<h1 class="mb-0">All-in-One Platform for Customer Feedback</h1>
<p class="mb-0 font-18">Centralize your feedback, prioritize your next steps, and keep everyone
informed.</p>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Tabs -->
<div class="col-lg-10 col-xl-8 col-xxl-6 mx-auto mb-70">
<div class="bg-light-3 rounded-pill p-2">
<ul class="nav nav-pills style-2 flex-nowrap justify-content-between" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link rounded-pill active" id="pills-analytics-tab"
data-bs-toggle="pill"
data-bs-target="#pills-analytics" type="button" role="tab"
aria-controls="pills-analytics" aria-selected="true"
tabindex="-1">Analyze feedback
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link rounded-pill" id="pills-roadmap-tab" data-bs-toggle="pill"
data-bs-target="#pills-roadmap" type="button" role="tab"
aria-controls="pills-roadmap" aria-selected="false">Build roadmap
</button>
</li>
<li class="nav-item position-relative" role="presentation">
<button class="nav-link rounded-pill" id="pills-prioritize-tab" data-bs-toggle="pill"
data-bs-target="#pills-prioritize" aria-controls="pills-prioritize"
aria-selected="false" tabindex="-1" role="tab">Prioritize requests
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link rounded-pill" id="pills-changelog-tab" data-bs-toggle="pill"
data-bs-target="#pills-changelog" aria-controls="pills-changelog"
aria-selected="false" tabindex="-1" role="tab">Share updates
</button>
</li>
</ul>
</div>
</div>
<!-- Tabs content -->
<div class="tab-content" id="pills-tabContent">
<!-- Content item 1-->
<div class="tab-pane active show fade" id="pills-analytics" role="tabpanel"
aria-labelledby="pills-analytics-tab" tabindex="0">
<div class="row align-items-center gy-5">
<!-- Info -->
<div class="col-lg-6 pr-120 xl-pr-0">
<div class="d-flex align-items-center">
<div class="icon-group size-24 bg-dark-3 text-white rounded-2 font-14"><i
class="fa-light fa-bolt"></i></div>
<span
class="text-uppercase font-14 ms-3 fw-bold ls-lg">Powerful SaaS solutions.</span>
</div>
<h1 class="my-24">Feedback Management</h1>
<p>Dont let valuable ideas fall through the cracks. Use a single tool to collect, analyze, and organize
feedback and feature requests efficiently.</p>
<ul>
<li>- Capture customer input seamlessly from conversations with Autopilot.</li>
<li>- Detect and merge duplicate requests to better quantify user needs.</li>
</ul>
<!-- List -->
</div>
<!-- image and decoration -->
<div class="col-lg-6 position-relative">
<figure class="w-100">
<img src="https://changelog.md/assets/templates/classic/images/home/feature/screen1.png"
class="rounded position-relative w-100" alt="feature-img">
</figure>
</div>
</div>
</div>
<!-- Content item 2-->
<div class="tab-pane fade" id="pills-roadmap" role="tabpanel" aria-labelledby="pills-roadmap-tab"
tabindex="0">
<div class="row align-items-center">
<!-- Info -->
<div class="col-lg-6 pr-120 xl-pr-0">
<div class="d-flex align-items-center">
<div class="icon-group size-24 bg-dark-3 text-white rounded-2 font-14"><i
class="fa-light fa-bolt"></i></div>
<span
class="text-uppercase font-14 ms-3 fw-bold ls-lg">Define Your Product Vision.</span>
</div>
<h1 class="my-24">Build your roadmap</h1>
<p>Keep users and stakeholders informed about current projects and upcoming plans.</p>
</div>
<!-- image and decoration -->
<div class="col-lg-6 position-relative ms-auto">
<figure class="w-100">
<img src="https://changelog.md/assets/templates/classic/images/home/feature/screen2.png"
class="rounded position-relative w-100" alt="feature-img">
</figure>
</div>
</div>
</div>
<!-- Content item 2-->
<div class="tab-pane fade" id="pills-prioritize" role="tabpanel"
aria-labelledby="pills-prioritize-tab"
tabindex="0">
<div class="row align-items-center">
<!-- Info -->
<div class="col-lg-6 pr-120 xl-pr-0">
<div class="d-flex align-items-center">
<div class="icon-group size-24 bg-dark-3 text-white rounded-2 font-14"><i
class="fa-light fa-bolt"></i></div>
<span
class="text-uppercase font-14 ms-3 fw-bold ls-lg">Management &amp; prioritization</span>
</div>
<h1 class="my-24">Prioritize feature requests</h1>
<p>Create a prioritization formula to score feedback and feature requests, ensuring you focus on the most
impactful features.</p>
<ul>
<li>- Adjust impact and effort factors to fit your needs.</li>
<li>- Include business-specific post fields for greater flexibility.</li>
<li>- Prioritize features based on user demand.</li>
</ul>
</div>
<!-- image and decoration -->
<div class="col-lg-6 position-relative ms-auto">
<figure class="w-100">
<img src="https://changelog.md/assets/templates/classic/images/home/feature/screen1.png"
class="rounded position-relative w-100" alt="feature-img">
</figure>
</div>
</div>
</div>
<!-- Content item 2-->
<div class="tab-pane fade" id="pills-changelog" role="tabpanel"
aria-labelledby="pills-changelog-tab"
tabindex="0">
<div class="row align-items-center">
<!-- Info -->
<div class="col-lg-6 pr-120 xl-pr-0">
<div class="d-flex align-items-center">
<div class="icon-group size-24 bg-dark-3 text-white rounded-2 font-14"><i
class="fa-light fa-bolt"></i></div>
<span
class="text-uppercase font-14 ms-3 fw-bold ls-lg">CHANGELOG.md</span>
</div>
<h1 class="my-24">Share updates</h1>
<p>Create a changelog that keeps everyone informed and engaged.</p>
<ul>
<li>- Publish Detailed Release Notes.</li>
<li>- Notify users who voted on specific feature requests.</li>
<li>- Drive customer retention, engagement and feature adoption.</li>
</ul>
</div>
<!-- image and decoration -->
<div class="col-lg-6 position-relative ms-auto">
<figure class="w-100">
<img src="https://changelog.md/assets/templates/classic/images/home/feature/screen4.png"
class="rounded position-relative w-100" alt="feature-img">
</figure>
</div>
</div>
</div>
</div>
</div>
</section>
<!--Feature to boost end-->
<!--More focus start-->
<section class="more-focus container py-60 xl-py-32" data-cue="fadeIn">
<div class="row mb-60">
<div class="col-12">
<div class="d-flex flex-column gap-4">
<div class="d-flex">
<span
class="bg-primary-l text-primary border-primary border px-3 py-2 font-14 rounded-pill lh-1 align-items-center d-flex">
<i class="fa-regular fa-bolt"></i>
<span class="ms-1 text-uppercase ls-md fw-semibold">Capture feedback</span>
</span>
</div>
<div class="d-flex justify-content-end gap-5">
<h1 class="mb-0 w-50">Easy to set up and use</h1>
<p class="mb-0 w-50 text-md-end font-18">Simplify feedback collection, lighten support workloads, and
announce product updates—all with a single tool.</p>
</div>
</div>
</div>
</div>
<div class="row gy-4 mb-3">
<div class="col-lg-4 col-12" data-cue="zoomIn">
<div class="card overflow-hidden border card-lift shadow-none p-0">
<div class="card-body me-xl-5 px-30 py-30">
<h4>Feedback Board</h4>
<p class="mb-0">Gather, analyze, and organize feedback in a centralized location</p>
</div>
<div class="text-end ms-4">
<figure class="mb-0">
<img src="https://changelog.md/assets/templates/classic/images/home/feature/feedback.png"
class="img-fluid" alt=""/>
</figure>
</div>
</div>
</div>
<div class="col-lg-4 col-12" data-cue="zoomIn">
<div class="card overflow-hidden border card-lift shadow-none p-0">
<div class="card-body me-xl-5 px-30 py-30">
<h4>Product Roadmap</h4>
<p class="mb-0">Create public/private roadmaps to keep everyone updated on your progress</p>
</div>
<div class="text-end ms-4">
<figure class="mb-0">
<img src="https://changelog.md/assets/templates/classic/images/home/feature/roadmap.png"
class="img-fluid" alt=""/>
</figure>
</div>
</div>
</div>
<div class="col-lg-4 col-12" data-cue="zoomIn">
<div class="card overflow-hidden border card-lift shadow-none p-0">
<div class="card-body me-xl-5 px-30 py-30">
<h4>Changelog</h4>
<p class="mb-0">Increase transparency with detailed change logs</p>
</div>
<div class="text-end ms-4">
<figure class="mb-0">
<img src="https://changelog.md/assets/templates/classic/images/home/feature/changelog.png"
class="img-fluid" alt=""/>
</figure>
</div>
</div>
</div>
</div>
<div class="row gy-4">
<div class="col-md-4" data-cue="zoomIn" data-duration="1500">
<div class="card border card-lift shadow-none h-100">
<div class="card-body">
<div class="d-flex">
<div class="feature-icon">
<i class="fa-light fa-sun-alt icon-group size-50 bg-light-2 font-24"></i>
</div>
<div class="ms-4">
<h4>Feature Request</h4>
<p class="mb-0">Organize feature requests to identify the most in-demand improvements.</p>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4" data-cue="zoomIn" data-duration="1500">
<div class="card border card-lift shadow-none h-100">
<div class="card-body">
<div class="d-flex">
<div class="feature-icon">
<i class="fa-light fa-dashboard icon-group size-50 bg-light-2 font-24"></i>
</div>
<div class="ms-4">
<h4>Customer Satisfaction</h4>
<p class="mb-0">Collect ongoing feedback to track and improve customer satisfaction over time.</p>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4" data-cue="zoomIn" data-duration="1500">
<div class="card border card-lift shadow-none h-100">
<div class="card-body">
<div class="d-flex">
<div class="feature-icon">
<i class="fa-light fa-search icon-group size-50 bg-light-2 font-24"></i>
</div>
<div class="ms-4">
<h4>Analyze feedback</h4>
<p class="mb-0">
Uncover valuable customer insights to make better product decisions.
</p>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6" data-cue="zoomIn" data-duration="1500">
<div class="card border card-lift shadow-none h-100">
<div class="card-body">
<div class="d-flex">
<div class="feature-icon">
<i class="fa-light fa-bug icon-group size-50 bg-light-2 font-24"></i>
</div>
<div class="ms-4">
<h4>Bug Reporting</h4>
<p class="mb-0">Receive instant notifications when users report bugs, keeping you ahead of critical
issues.</p>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6" data-cue="zoomIn" data-duration="1500">
<div class="card border card-lift shadow-none h-100">
<div class="card-body">
<div class="d-flex">
<div class="feature-icon">
<i class="fa-light fa-heart-pulse icon-group size-50 bg-light-2 font-24"></i>
</div>
<div class="ms-4">
<h4>Uptime monitoring service</h4>
<p class="mb-0">Create beautiful status pages &amp; incident management reports and keep your visitors
updated.(Soon)</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!--More focus end-->
<section class="container my-60" data-cue="zoomIn">
<div class="container theme-gradient-dark rounded-4 shadow-3">
<div class="p-5 py-50">
<div class="row g-4 align-items-center text-center text-xl-start">
<div class="col-xl-5">
<div class="text-white">
<span class="text-uppercase">Join Our Newsletter</span>
<h2 class="text-white mb-0">Subscribe Now</h2>
</div>
</div>
<div class="col-xl-7">
<form action="http://changelog.md/newsletter" method="post" class="d-flex">
<input type="hidden" name="_token" value="pTASJlt042Vo3XAFbvffgDFlRJIGLAVOJ9LcEFQe"> <input name="email"
class="form-control rounded-5 h-48-px me-3 px-20"
placeholder="Enter your email address"
type="email"
value="">
<button class="button -primary -lg rounded-5">Subscribe</button>
</form>
</div>
</div>
</div>
</div>
</section>
<!--FAQ-->
<section class="container py-60 xl-py-32" data-cue="fadeIn">
<div class="row justify-content-center mb-60">
<div class="col-xl-6 col-lg-10 col-12">
<div class="text-center d-flex flex-column gap-4">
<div class="d-flex justify-content-center">
<span
class="bg-primary-l text-primary border-primary border px-3 py-2 font-14 rounded-pill lh-1 align-items-center d-flex">
<i class="fa-regular fa-bolt"></i>
<span class="ms-1 text-uppercase ls-md fw-semibold">Help Center</span>
</span>
</div>
<div class="d-flex flex-column gap-3 mx-70 xl-mx-0">
<h1 class="mb-0">Frequently Asked Questions</h1>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="ui-content">
<div class="accordion -style2 faq-page mb-4 mb-lg-5">
<div class="accordion" id="accordionExample">
<div class="accordion-item active ">
<h2 class="accordion-header" id="heading1">
<button class="accordion-button " type="button" data-bs-toggle="collapse" data-bs-target="#collapse1"
aria-expanded="" aria-controls="collapse1">
What is CHANGELOG.md?
</button>
</h2>
<div id="collapse1" class="accordion-collapse collapse show " aria-labelledby="heading1"
data-bs-parent="#accordionExample">
<div class="accordion-body"><p>CHANGELOG.md is a SaaS platform designed to help you collect, analyze,
and act on customer feedback to uncover valuable insights and make informed product decisions.</p>
</div>
</div>
</div>
<div class="accordion-item ">
<h2 class="accordion-header" id="heading2">
<button class="accordion-button collapsed " type="button" data-bs-toggle="collapse"
data-bs-target="#collapse2" aria-expanded="" aria-controls="collapse2">
Can I use CHANGELOG.md to prioritize feature requests?
</button>
</h2>
<div id="collapse2" class="accordion-collapse collapse " aria-labelledby="heading2"
data-bs-parent="#accordionExample">
<div class="accordion-body"><p>Absolutely! With our prioritization tools, you can score feedback and
feature requests based on factors like impact and effort, helping you focus on what matters
most.</p></div>
</div>
</div>
<div class="accordion-item ">
<h2 class="accordion-header" id="heading3">
<button class="accordion-button collapsed " type="button" data-bs-toggle="collapse"
data-bs-target="#collapse3" aria-expanded="" aria-controls="collapse3">
How does CHANGELOG.md help with roadmapping?
</button>
</h2>
<div id="collapse3" class="accordion-collapse collapse " aria-labelledby="heading3"
data-bs-parent="#accordionExample">
<div class="accordion-body"><p>CHANGELOG.md allows you to build a clear and actionable roadmap by
organizing feedback and aligning it with your product vision.</p></div>
</div>
</div>
<div class="accordion-item ">
<h2 class="accordion-header" id="heading4">
<button class="accordion-button collapsed " type="button" data-bs-toggle="collapse"
data-bs-target="#collapse4" aria-expanded="" aria-controls="collapse4">
Can I share updates with my users?
</button>
</h2>
<div id="collapse4" class="accordion-collapse collapse " aria-labelledby="heading4"
data-bs-parent="#accordionExample">
<div class="accordion-body"><p>Yes! CHANGELOG.md includes a changelog feature where you can publish
detailed release notes, link them to specific feature requests, and notify users who requested those
features automatically.</p></div>
</div>
</div>
<div class="accordion-item ">
<h2 class="accordion-header" id="heading5">
<button class="accordion-button collapsed " type="button" data-bs-toggle="collapse"
data-bs-target="#collapse5" aria-expanded="" aria-controls="collapse5">
Does CHANGELOG.md integrate with other tools?
</button>
</h2>
<div id="collapse5" class="accordion-collapse collapse " aria-labelledby="heading5"
data-bs-parent="#accordionExample">
<div class="accordion-body"><p>Yes, CHANGELOG.md integrates with popular customer support and project
management tools, allowing your team to seamlessly capture and manage feedback within their existing
workflows.</p></div>
</div>
</div>
<div class="accordion-item ">
<h2 class="accordion-header" id="heading6">
<button class="accordion-button collapsed " type="button" data-bs-toggle="collapse"
data-bs-target="#collapse6" aria-expanded="" aria-controls="collapse6">
Who can benefit from using CHANGELOG.md?
</button>
</h2>
<div id="collapse6" class="accordion-collapse collapse " aria-labelledby="heading6"
data-bs-parent="#accordionExample">
<div class="accordion-body"><p>Product managers, customer success teams, and anyone involved in
building and improving products can benefit from CHANGELOG.md. Its perfect for startups, SaaS
companies, and organizations looking to make data-driven product decisions.</p></div>
</div>
</div>
<div class="accordion-item ">
<h2 class="accordion-header" id="heading7">
<button class="accordion-button collapsed " type="button" data-bs-toggle="collapse"
data-bs-target="#collapse7" aria-expanded="" aria-controls="collapse7">
How do I get started with CHANGELOG.md?
</button>
</h2>
<div id="collapse7" class="accordion-collapse collapse " aria-labelledby="heading7"
data-bs-parent="#accordionExample">
<div class="accordion-body"><p>Getting started is simple! Sign up for a free trial, set up your
feedback portal, and start collecting insights to drive your product decisions.</p></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!--FAQ end-->
<!--Call to action start-->
<section class="container lg-mb-auto mb-60 py-60 xl-py-32" data-cue="zoomIn">
<div class="container theme-gradient-dark rounded-4 shadow-3">
<div class="p-5 py-100 text-center">
<div class="d-flex flex-column gap-4">
<div class="d-flex justify-content-center">
<span
class="bg-primary-l text-primary border-primary border px-3 py-2 font-14 rounded-pill lh-1 align-items-center d-flex">
<i class="fa-regular fa-bolt"></i>
<span class="ms-1 text-uppercase ls-md fw-semibold">More features. More power.</span>
</span>
</div>
<div class="d-flex flex-column gap-3 mx-70 xl-mx-0">
<h1 class="mb-0 text-white">We bring companies and customers even closer</h1>
<p class="mb-0 text-white">Ready to start building the right things?</p>
</div>
</div>
<div class="d-flex align-items-center justify-content-center gap-3 mt-24">
<a href="http://changelog.md/login" class="button -primary">Join now</a>
<a href="https://changelog.md" class="button bg-white">Try demo</a>
</div>
</div>
</div>
</section>
<!--Call to action end-->
</main>
<!-- Footer -->
<footer>
<div class="container">
<div class="d-flex justify-content-center align-items-center">
<div class="d-flex flex-column align-items-center">
<a href="http://changelog.md">
<img class="white-logo mb-16 w-130-px"
src="https://changelog.md/storage/logo/classic-theme_footer_logo.png"
alt="CHANGELOG.md - Realease notes &amp; Feedback Management Tool">
<img class="main-logo mb-16 w-130-px" src="https://changelog.md/storage/logo/classic-theme_logo.png"
alt="CHANGELOG.md - Realease notes &amp; Feedback Management Tool">
</a>
<div class="d-flex align-items-center font-16 gap-3">
<div><a href="http://changelog.md" class="-underline fw-semibold">Home</a></div>
<div><a href="http://changelog.md/page/terms-conditions"
class="-underline fw-semibold">Terms &amp; Conditions</a></div>
<div><a href="http://changelog.md/page/privacy"
class="-underline fw-semibold">Privacy Policy</a></div>
</div>
</div>
</div>
<div class="separator-1px-op-l my-32"></div>
<div class="row align-items-center g-4 g-lg-0 pb-32">
<div class="col-lg-4 order-lg-first order-last text-lg-start text-center">
Copyright © 2026 Changelog.md. All Rights Reserved.
</div>
<div class="col-lg-4">
<div class="d-flex font-16 gap-3 justify-content-center">
<div>
<a href="http://changelog.md/faq"
class="text-dark-1 text-decoration -underline fw-semibold">FAQs</a>
</div>
<div>
<a href="http://changelog.md/feedback"
class="text-dark-1 text-decoration -underline fw-semibold">Feedback</a>
</div>
<div>
<a href="http://changelog.md/contact"
class="text-dark-1 text-decoration -underline fw-semibold">Contact</a>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="d-flex font-16 gap-3 justify-content-lg-end justify-content-center">
<div>
<a class="icon-group -outlined -light shadow-3 rounded-3"
href="https://x.com/changelogmd" rel="nofollow">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 14 14"
style="fill: currentcolor; height: .9em; overflow: visible; width: 1em;">
<path
d="M12.6.75h2.454l-5.36 6.142L16 15.25h-4.937l-3.867-5.07-4.425 5.07H.316l5.733-6.57L0 .75h5.063l3.495 4.633L12.601.75Zm-.86 13.028h1.36L4.323 2.145H2.865z"/>
</svg>
</a>
</div>
</div>
</div>
</div>
</div>
</footer>
<script>
"use strict";
var themecolor = "#ea580c";
var siteurl = "http:\/\/changelog.md";
var ajax_url = "http:\/\/changelog.md\/ajax";
var LANG_LOGGED_IN_SUCCESS = "Logged in successfully";
var LANG_DEVELOPED_BY = "Developed by";
var DEVELOPER_CREDIT = 1;
var LIVE_CHAT = null;
var DARK_MODE_SWITCH = 0;
var DEFAULT_THEME_MODE = "light";
</script>
<script src="https://changelog.md/assets/global/js/jquery.min.js"></script>
<script src="https://changelog.md/assets/global/js/jquery.form.js"></script>
<script src="https://changelog.md/assets/global/bootstrap/js/bootstrap.min.js"></script>
<script src="https://changelog.md/assets/templates/classic/plugin/owl-carousel/owl.carousel.min.js"></script>
<script src="https://changelog.md/assets/templates/classic/plugin/owl-carousel/carousel-thumbs.min.js"></script>
<script src="https://changelog.md/assets/templates/classic/plugin/magnific-poupup/jquery.magnific-popup.js"></script>
<script src="https://changelog.md/assets/templates/classic/plugin/mixitup/mixitup.min.js"></script>
<script src="https://changelog.md/assets/templates/classic/plugin/simple-bar/simplebar.min.js"></script>
<script src="https://changelog.md/assets/templates/classic/plugin/appear/appear.min.js"></script>
<script src="https://changelog.md/assets/templates/classic/plugin/text-typer/typing-text.js"></script>
<script src="https://changelog.md/assets/templates/classic/plugin/wow-animate/wow.min.js"></script>
<script src="https://changelog.md/assets/templates/classic/plugin/swiper/swiper-bundle.min.js"></script>
<script src="https://changelog.md/assets/templates/classic/plugin/swiper/swiper.js"></script>
<script src="https://changelog.md/assets/templates/classic/plugin/snackbar/snackbar.min.js"></script>
<!--Custom JS-->
<script src="https://changelog.md/assets/templates/classic/js/user-ajax.js?ver=3.2.1"></script>
<script src="https://changelog.md/assets/templates/classic/js/custom.js?ver=3.2.1"></script>
<script src="https://changelog.md/assets/templates/classic/js/script.js?ver=3.2.1"></script>
</body>
</html>
+29
View File
@@ -0,0 +1,29 @@
#!/bin/bash
echo "==========================================="
echo "Meldestelle - Netzwerk-Setup für POC"
echo "==========================================="
if [ "$EUID" -ne 0 ]; then
echo "Bitte mit sudo ausführen: sudo ./setup-firewall-linux.sh"
exit
fi
# Erkennung der Firewall (firewalld für Fedora/KDE, ufw für Ubuntu)
if command -v firewall-cmd &> /dev/null; then
echo "[Fedora/firewalld] Öffne Ports 8090 (TCP), 8080 (TCP) und 5353 (UDP)..."
firewall-cmd --permanent --add-port=8090/tcp
firewall-cmd --permanent --add-port=8080/tcp
firewall-cmd --permanent --add-service=mdns
firewall-cmd --reload
echo "Fertig!"
elif command -v ufw &> /dev/null; then
echo "[Ubuntu/ufw] Öffne Ports 8090 (TCP), 8080 (TCP) und 5353 (UDP)..."
ufw allow 8090/tcp
ufw allow 8080/tcp
ufw allow 5353/udp
echo "Fertig!"
else
echo "Keine bekannte Firewall (ufw/firewalld) gefunden. Bitte Ports manuell prüfen."
fi
echo "Das System ist nun bereit für den Meldestelle-POC."