chore(frontend): remove custom Webpack sqlite-wasm integration plugins and cleanup config
- Deleted `dummy.js` and its usage for sqlite-wasm integration as custom Webpack adjustments are no longer necessary. - Removed outdated Webpack configuration files: `ignore-sqlite-wasm.js`, `ignore-sqlite-wasm-critical-dependency.js`, and `sqljs-fix.js`. - Introduced `sqlite-config.js` for simplified and streamlined sqlite-wasm and Skiko Webpack configuration. - Minor code formatting adjustments across frontend modules for improved consistency.
This commit is contained in:
+1
-1
@@ -3,4 +3,4 @@
|
||||
Dieses Modul enthält den gesamten Code für das Kotlin Multiplatform (KMP) Frontend "Meldestelle Portal".
|
||||
|
||||
**Die vollständige Dokumentation befindet sich hier:**
|
||||
[**-> docs/06_Frontend/README.md**](../docs/06_Frontend/README.md)
|
||||
[**→ docs/06_Frontend/README.md**](../docs/06_Frontend/README.md)
|
||||
|
||||
+18
-18
@@ -22,23 +22,23 @@ import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||
*/
|
||||
@Composable
|
||||
fun DenseButton(
|
||||
text: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
containerColor: Color = MaterialTheme.colorScheme.primary
|
||||
text: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
containerColor: Color = MaterialTheme.colorScheme.primary
|
||||
) {
|
||||
Button(
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
modifier = modifier.height(32.dp), // Fixe, kompakte Höhe
|
||||
shape = MaterialTheme.shapes.small, // Nutzt unsere 4dp Rundung
|
||||
colors = ButtonDefaults.buttonColors(containerColor = containerColor),
|
||||
contentPadding = PaddingValues(horizontal = Dimens.SpacingM, vertical = 0.dp) // Wenig Padding
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.labelMedium // Kleinere Schrift
|
||||
)
|
||||
}
|
||||
Button(
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
modifier = modifier.height(32.dp), // Fixe, kompakte Höhe
|
||||
shape = MaterialTheme.shapes.small, // Nutzt unsere 4dp Rundung
|
||||
colors = ButtonDefaults.buttonColors(containerColor = containerColor),
|
||||
contentPadding = PaddingValues(horizontal = Dimens.SpacingM, vertical = 0.dp) // Wenig Padding
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.labelMedium // Kleinere Schrift
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+15
-15
@@ -21,22 +21,22 @@ import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||
*/
|
||||
@Composable
|
||||
fun DashboardCard(
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
),
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), // Dünner Rahmen
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) // Kein Schatten
|
||||
Card(
|
||||
modifier = modifier,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
),
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), // Dünner Rahmen
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) // Kein Schatten
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(Dimens.SpacingS) // Kompaktes Padding innen
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(Dimens.SpacingS) // Kompaktes Padding innen
|
||||
) {
|
||||
content()
|
||||
}
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+60
-60
@@ -18,89 +18,89 @@ import androidx.compose.ui.unit.sp
|
||||
// Blau steht für Aktion/Information, Grau für Struktur.
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Color(0xFF0052CC), // Enterprise Blue (stark)
|
||||
onPrimary = Color.White,
|
||||
primaryContainer = Color(0xFFDEEBFF),
|
||||
onPrimaryContainer = Color(0xFF0052CC),
|
||||
primary = Color(0xFF0052CC), // Enterprise Blue (stark)
|
||||
onPrimary = Color.White,
|
||||
primaryContainer = Color(0xFFDEEBFF),
|
||||
onPrimaryContainer = Color(0xFF0052CC),
|
||||
|
||||
secondary = Color(0xFF2684FF), // Helleres Blau für Akzente
|
||||
onSecondary = Color.White,
|
||||
secondary = Color(0xFF2684FF), // Helleres Blau für Akzente
|
||||
onSecondary = Color.White,
|
||||
|
||||
background = Color(0xFFF4F5F7), // Helles Grau (nicht hartes Weiß)
|
||||
surface = Color.White,
|
||||
onBackground = Color(0xFF172B4D), // Fast Schwarz (besser lesbar als #000)
|
||||
onSurface = Color(0xFF172B4D),
|
||||
background = Color(0xFFF4F5F7), // Helles Grau (nicht hartes Weiß)
|
||||
surface = Color.White,
|
||||
onBackground = Color(0xFF172B4D), // Fast Schwarz (besser lesbar als #000)
|
||||
onSurface = Color(0xFF172B4D),
|
||||
|
||||
error = Color(0xFFDE350B),
|
||||
onError = Color.White
|
||||
error = Color(0xFFDE350B),
|
||||
onError = Color.White
|
||||
)
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Color(0xFF4C9AFF), // Helleres Blau auf Dunkel
|
||||
onPrimary = Color(0xFF091E42),
|
||||
primaryContainer = Color(0xFF0052CC),
|
||||
onPrimaryContainer = Color.White,
|
||||
primary = Color(0xFF4C9AFF), // Helleres Blau auf Dunkel
|
||||
onPrimary = Color(0xFF091E42),
|
||||
primaryContainer = Color(0xFF0052CC),
|
||||
onPrimaryContainer = Color.White,
|
||||
|
||||
secondary = Color(0xFF2684FF),
|
||||
onSecondary = Color.White,
|
||||
secondary = Color(0xFF2684FF),
|
||||
onSecondary = Color.White,
|
||||
|
||||
background = Color(0xFF1E1E1E), // Dunkles Grau (angenehmer als #000)
|
||||
surface = Color(0xFF2C2C2C), // Panels heben sich leicht ab
|
||||
onBackground = Color(0xFFEBECF0),
|
||||
onSurface = Color(0xFFEBECF0),
|
||||
background = Color(0xFF1E1E1E), // Dunkles Grau (angenehmer als #000)
|
||||
surface = Color(0xFF2C2C2C), // Panels heben sich leicht ab
|
||||
onBackground = Color(0xFFEBECF0),
|
||||
onSurface = Color(0xFFEBECF0),
|
||||
|
||||
error = Color(0xFFFF5630),
|
||||
onError = Color.Black
|
||||
error = Color(0xFFFF5630),
|
||||
onError = Color.Black
|
||||
)
|
||||
|
||||
// --- 2. Formen (Shapes) ---
|
||||
// Enterprise Apps nutzen oft weniger Rundung als Consumer Apps (seriöser).
|
||||
private val AppShapes = Shapes(
|
||||
small = RoundedCornerShape(Dimens.CornerRadiusS), // Buttons, Inputs
|
||||
medium = RoundedCornerShape(Dimens.CornerRadiusM), // Cards, Dialogs
|
||||
large = RoundedCornerShape(Dimens.CornerRadiusM)
|
||||
small = RoundedCornerShape(Dimens.CornerRadiusS), // Buttons, Inputs
|
||||
medium = RoundedCornerShape(Dimens.CornerRadiusM), // Cards, Dialogs
|
||||
large = RoundedCornerShape(Dimens.CornerRadiusM)
|
||||
)
|
||||
|
||||
// --- 3. Typografie ---
|
||||
// wir setzen auf klare Hierarchie.
|
||||
private val AppTypography = Typography(
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp
|
||||
),
|
||||
titleMedium = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp
|
||||
),
|
||||
bodyMedium = TextStyle( // Standard Text
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp
|
||||
),
|
||||
labelSmall = TextStyle( // Für dichte Tabellen/Labels
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp
|
||||
)
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp
|
||||
),
|
||||
titleMedium = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp
|
||||
),
|
||||
bodyMedium = TextStyle( // Standard Text
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp
|
||||
),
|
||||
labelSmall = TextStyle( // Für dichte Tabellen/Labels
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp
|
||||
)
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun AppTheme(
|
||||
darkTheme: Boolean = false, // Kann später via Settings gesteuert werden
|
||||
content: @Composable () -> Unit
|
||||
darkTheme: Boolean = false, // Kann später via Settings gesteuert werden
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
|
||||
val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
shapes = AppShapes,
|
||||
typography = AppTypography,
|
||||
content = content
|
||||
)
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
shapes = AppShapes,
|
||||
typography = AppTypography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
+13
-13
@@ -8,20 +8,20 @@ import androidx.compose.ui.unit.dp
|
||||
* Wenn wir den Abstand global ändern wollen, machen wir das nur hier.
|
||||
*/
|
||||
object Dimens {
|
||||
// Spacing (Abstände)
|
||||
val SpacingXS = 4.dp // Sehr eng (für Tabellen, dichte Listen)
|
||||
val SpacingS = 8.dp // Standard Abstand zwischen Elementen
|
||||
val SpacingM = 16.dp // Abstand für Sektionen
|
||||
val SpacingL = 24.dp // Außenabstand für Screens
|
||||
// Spacing (Abstände)
|
||||
val SpacingXS = 4.dp // Sehr eng (für Tabellen, dichte Listen)
|
||||
val SpacingS = 8.dp // Standard Abstand zwischen Elementen
|
||||
val SpacingM = 16.dp // Abstand für Sektionen
|
||||
val SpacingL = 24.dp // Außenabstand für Screens
|
||||
|
||||
// Sizes (Größen)
|
||||
val IconSizeS = 16.dp
|
||||
val IconSizeM = 24.dp
|
||||
// Sizes (Größen)
|
||||
val IconSizeS = 16.dp
|
||||
val IconSizeM = 24.dp
|
||||
|
||||
// Borders
|
||||
val BorderThin = 1.dp
|
||||
// Borders
|
||||
val BorderThin = 1.dp
|
||||
|
||||
// Corner Radius (Ecken)
|
||||
val CornerRadiusS = 4.dp // Leicht abgerundet (Enterprise Look)
|
||||
val CornerRadiusM = 8.dp
|
||||
// Corner Radius (Ecken)
|
||||
val CornerRadiusS = 4.dp // Leicht abgerundet (Enterprise Look)
|
||||
val CornerRadiusM = 8.dp
|
||||
}
|
||||
|
||||
+1
-1
@@ -4,5 +4,5 @@ import app.cash.sqldelight.db.SqlDriver
|
||||
|
||||
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
|
||||
expect class DatabaseDriverFactory() {
|
||||
suspend fun createDriver(): SqlDriver
|
||||
suspend fun createDriver(): SqlDriver
|
||||
}
|
||||
|
||||
+14
-12
@@ -6,23 +6,25 @@ import org.w3c.dom.Worker
|
||||
|
||||
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
|
||||
actual class DatabaseDriverFactory {
|
||||
actual suspend fun createDriver(): SqlDriver {
|
||||
// Wir nutzen eine Helper-Funktion, um den Worker zu erstellen.
|
||||
// Dies ermöglicht uns, 'new URL(..., import.meta.url)' in JS zu verwenden,
|
||||
// was Webpack dazu bringt, den Pfad korrekt aufzulösen.
|
||||
val worker = createWorker()
|
||||
val driver = WebWorkerDriver(worker)
|
||||
actual suspend fun createDriver(): SqlDriver {
|
||||
// Wir nutzen eine Helper-Funktion, um den Worker zu erstellen.
|
||||
// Dies ermöglicht uns, 'new URL(..., import.meta.url)' in JS zu verwenden,
|
||||
// was Webpack dazu bringt, den Pfad korrekt aufzulösen.
|
||||
val worker = createWorker()
|
||||
val driver = WebWorkerDriver(worker)
|
||||
|
||||
// Initialize schema asynchronously
|
||||
AppDatabase.Schema.create(driver).await()
|
||||
// Initialize schema asynchronously
|
||||
AppDatabase.Schema.create(driver).await()
|
||||
|
||||
return driver
|
||||
}
|
||||
return driver
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to create the worker using proper URL resolution
|
||||
private fun createWorker(): Worker {
|
||||
return js("""
|
||||
return js(
|
||||
"""
|
||||
new Worker(new URL('sqlite.worker.js', import.meta.url), { type: 'module' })
|
||||
""")
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,103 +1,109 @@
|
||||
import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
|
||||
// We do NOT import from node_modules anymore to avoid Webpack bundling issues.
|
||||
// import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
|
||||
|
||||
console.log("Worker: sqlite.worker.js loaded. Starting initialization...");
|
||||
|
||||
// Minimal worker protocol compatible with SQLDelight's `web-worker-driver`.
|
||||
// Mirrors the message format used by SQLDelight's `sqljs.worker.js` implementation.
|
||||
function runWorker({ driver }) {
|
||||
console.log("Worker: runWorker called");
|
||||
let db = null;
|
||||
const open = (name) => {
|
||||
console.log("Worker: Opening database", name);
|
||||
db = driver.open(name);
|
||||
};
|
||||
function runWorker({driver}) {
|
||||
console.log("Worker: runWorker called");
|
||||
let db = null;
|
||||
const open = (name) => {
|
||||
console.log("Worker: Opening database", name);
|
||||
db = driver.open(name);
|
||||
};
|
||||
|
||||
// Open once with the default database name expected by SQLDelight.
|
||||
open('app.db');
|
||||
// Open once with the default database name expected by SQLDelight.
|
||||
open('app.db');
|
||||
|
||||
self.onmessage = (event) => {
|
||||
const data = event.data;
|
||||
try {
|
||||
switch (data && data.action) {
|
||||
case 'exec': {
|
||||
if (!data.sql) throw new Error('exec: Missing query string');
|
||||
// sqlite-wasm oo1 DB supports `.exec(...)`.
|
||||
// We intentionally return only `values` which is sufficient for SQLDelight.
|
||||
const rows = [];
|
||||
db.exec({
|
||||
sql: data.sql,
|
||||
bind: data.params ?? [],
|
||||
rowMode: 'array',
|
||||
callback: (row) => rows.push(row)
|
||||
});
|
||||
return postMessage({ id: data.id, results: { values: rows } });
|
||||
}
|
||||
case 'begin_transaction':
|
||||
db.exec('BEGIN TRANSACTION;');
|
||||
return postMessage({ id: data.id, results: [] });
|
||||
case 'end_transaction':
|
||||
db.exec('END TRANSACTION;');
|
||||
return postMessage({ id: data.id, results: [] });
|
||||
case 'rollback_transaction':
|
||||
db.exec('ROLLBACK TRANSACTION;');
|
||||
return postMessage({ id: data.id, results: [] });
|
||||
default:
|
||||
throw new Error(`Unsupported action: ${data && data.action}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Worker: Error processing message", err);
|
||||
return postMessage({ id: data && data.id, error: err?.message ?? String(err) });
|
||||
self.onmessage = (event) => {
|
||||
const data = event.data;
|
||||
try {
|
||||
switch (data && data.action) {
|
||||
case 'exec': {
|
||||
if (!data.sql) throw new Error('exec: Missing query string');
|
||||
const rows = [];
|
||||
db.exec({
|
||||
sql: data.sql,
|
||||
bind: data.params ?? [],
|
||||
rowMode: 'array',
|
||||
callback: (row) => rows.push(row)
|
||||
});
|
||||
return postMessage({id: data.id, results: {values: rows}});
|
||||
}
|
||||
};
|
||||
case 'begin_transaction':
|
||||
db.exec('BEGIN TRANSACTION;');
|
||||
return postMessage({id: data.id, results: []});
|
||||
case 'end_transaction':
|
||||
db.exec('END TRANSACTION;');
|
||||
return postMessage({id: data.id, results: []});
|
||||
case 'rollback_transaction':
|
||||
db.exec('ROLLBACK TRANSACTION;');
|
||||
return postMessage({id: data.id, results: []});
|
||||
default:
|
||||
throw new Error(`Unsupported action: ${data && data.action}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Worker: Error processing message", err);
|
||||
return postMessage({id: data && data.id, error: err?.message ?? String(err)});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Error handling wrapper
|
||||
self.onerror = function(event) {
|
||||
console.error("Error in Web Worker (onerror):", event.message, event.filename, event.lineno);
|
||||
// Optionally, send the error back to the main thread
|
||||
self.postMessage({ type: 'error', message: event.message, filename: event.filename, lineno: event.lineno });
|
||||
self.onerror = function (event) {
|
||||
console.error("Error in Web Worker (onerror):", event.message, event.filename, event.lineno);
|
||||
self.postMessage({type: 'error', message: event.message, filename: event.filename, lineno: event.lineno});
|
||||
};
|
||||
|
||||
// Manually fetch the WASM file to bypass Webpack/sqlite-wasm loading issues
|
||||
async function init() {
|
||||
try {
|
||||
console.log("Worker: Fetching sqlite3.wasm manually...");
|
||||
const response = await fetch('/sqlite3.wasm');
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch sqlite3.wasm: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const wasmBinary = await response.arrayBuffer();
|
||||
console.log("Worker: sqlite3.wasm fetched successfully, size:", wasmBinary.byteLength);
|
||||
try {
|
||||
// 1. Load the sqlite3.js library manually via importScripts.
|
||||
// This file is copied to the root by Webpack (CopyWebpackPlugin).
|
||||
// This bypasses Webpack's module resolution for the library itself.
|
||||
console.log("Worker: Loading sqlite3.js via importScripts...");
|
||||
importScripts('sqlite3.js');
|
||||
|
||||
console.log("Worker: Calling sqlite3InitModule with wasmBinary...");
|
||||
const sqlite3 = await sqlite3InitModule({
|
||||
print: console.log,
|
||||
printErr: console.error,
|
||||
wasmBinary: wasmBinary // Provide the binary directly!
|
||||
});
|
||||
|
||||
console.log("Worker: sqlite3InitModule resolved successfully");
|
||||
const opfsAvailable = 'opfs' in sqlite3;
|
||||
console.log("Worker: OPFS available:", opfsAvailable);
|
||||
|
||||
runWorker({
|
||||
driver: {
|
||||
open: (name) => {
|
||||
if (opfsAvailable) {
|
||||
console.log("Initialisiere persistente OPFS Datenbank: " + name);
|
||||
return new sqlite3.oo1.OpfsDb(name);
|
||||
} else {
|
||||
console.warn("OPFS nicht verfügbar, Fallback auf In-Memory");
|
||||
return new sqlite3.oo1.DB(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.error("Database initialization error in worker:", e);
|
||||
self.postMessage({ type: 'error', message: 'Database initialization failed: ' + e.message });
|
||||
// After importScripts, `sqlite3InitModule` should be available globally.
|
||||
if (typeof self.sqlite3InitModule !== 'function') {
|
||||
throw new Error("sqlite3InitModule is not defined after importScripts. Check if sqlite3.js was loaded correctly.");
|
||||
}
|
||||
|
||||
console.log("Worker: Fetching sqlite3.wasm manually...");
|
||||
const response = await fetch('sqlite3.wasm');
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch sqlite3.wasm: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const wasmBinary = await response.arrayBuffer();
|
||||
console.log("Worker: sqlite3.wasm fetched successfully, size:", wasmBinary.byteLength);
|
||||
|
||||
console.log("Worker: Calling sqlite3InitModule with wasmBinary...");
|
||||
const sqlite3 = await self.sqlite3InitModule({
|
||||
print: console.log,
|
||||
printErr: console.error,
|
||||
wasmBinary: wasmBinary
|
||||
});
|
||||
|
||||
console.log("Worker: sqlite3InitModule resolved successfully");
|
||||
const opfsAvailable = 'opfs' in sqlite3;
|
||||
console.log("Worker: OPFS available:", opfsAvailable);
|
||||
|
||||
runWorker({
|
||||
driver: {
|
||||
open: (name) => {
|
||||
if (opfsAvailable) {
|
||||
console.log("Initialisiere persistente OPFS Datenbank: " + name);
|
||||
return new sqlite3.oo1.OpfsDb(name);
|
||||
} else {
|
||||
console.warn("OPFS nicht verfügbar, Fallback auf In-Memory");
|
||||
return new sqlite3.oo1.DB(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.error("Database initialization error in worker:", e);
|
||||
self.postMessage({type: 'error', message: 'Database initialization failed: ' + e.message});
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
+23
-22
@@ -4,31 +4,32 @@ import app.cash.sqldelight.db.SqlDriver
|
||||
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
|
||||
import java.io.File
|
||||
|
||||
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
|
||||
actual class DatabaseDriverFactory {
|
||||
actual suspend fun createDriver(): SqlDriver {
|
||||
// For desktop, we use a persistent file database
|
||||
// In dev mode, we might want to use a temporary file or user home
|
||||
val dbFile = File(System.getProperty("user.home"), ".meldestelle/app_database.db")
|
||||
dbFile.parentFile.mkdirs()
|
||||
actual suspend fun createDriver(): SqlDriver {
|
||||
// For desktop, we use a persistent file database
|
||||
// In dev mode, we might want to use a temporary file or user home
|
||||
val dbFile = File(System.getProperty("user.home"), ".meldestelle/app_database.db")
|
||||
dbFile.parentFile.mkdirs()
|
||||
|
||||
val driver = JdbcSqliteDriver("jdbc:sqlite:${dbFile.absolutePath}")
|
||||
val driver = JdbcSqliteDriver("jdbc:sqlite:${dbFile.absolutePath}")
|
||||
|
||||
// Schema creation/migration needs to be handled carefully.
|
||||
// For now, we just create it if it doesn't exist.
|
||||
// In a real app, we'd check version and migrate.
|
||||
// Since generateAsync=true, the Schema.create signature might be suspend or return AsyncResult.
|
||||
// However, JdbcSqliteDriver is synchronous. We might need to wrap or await.
|
||||
// But wait! Schema.create(driver) returns void or Unit usually.
|
||||
// Let's check the generated code later. For now, we assume standard behavior.
|
||||
// Schema creation/migration needs to be handled carefully.
|
||||
// For now, we just create it if it doesn't exist.
|
||||
// In a real app, we'd check version and migrate.
|
||||
// Since generateAsync=true, the Schema.create signature might be suspend or return AsyncResult.
|
||||
// However, JdbcSqliteDriver is synchronous. We might need to wrap or await.
|
||||
// But wait! Schema.create(driver) returns void or Unit usually.
|
||||
// Let's check the generated code later. For now, we assume standard behavior.
|
||||
|
||||
try {
|
||||
AppDatabase.Schema.create(driver).await()
|
||||
} catch (e: Exception) {
|
||||
// Schema might already exist.
|
||||
// SQLDelight doesn't have "createIfNotExists" built-in easily without version check.
|
||||
// We'll leave this simple for now and refine with proper migration logic later.
|
||||
}
|
||||
|
||||
return driver
|
||||
try {
|
||||
AppDatabase.Schema.create(driver).await()
|
||||
} catch (e: Exception) {
|
||||
// Schema might already exist.
|
||||
// SQLDelight doesn't have "createIfNotExists" built-in easily without version check.
|
||||
// We'll leave this simple for now and refine with proper migration logic later.
|
||||
}
|
||||
|
||||
return driver
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,82 +2,82 @@ import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
|
||||
|
||||
// Minimal worker protocol compatible with SQLDelight's `web-worker-driver`.
|
||||
// Mirrors the message format used by SQLDelight's `sqljs.worker.js` implementation.
|
||||
function runWorker({ driver }) {
|
||||
let db = null;
|
||||
const open = (name) => {
|
||||
db = driver.open(name);
|
||||
};
|
||||
function runWorker({driver}) {
|
||||
let db = null;
|
||||
const open = (name) => {
|
||||
db = driver.open(name);
|
||||
};
|
||||
|
||||
// Open once with the default database name expected by SQLDelight.
|
||||
open('app.db');
|
||||
// Open once with the default database name expected by SQLDelight.
|
||||
open('app.db');
|
||||
|
||||
self.onmessage = (event) => {
|
||||
const data = event.data;
|
||||
try {
|
||||
switch (data && data.action) {
|
||||
case 'exec': {
|
||||
if (!data.sql) throw new Error('exec: Missing query string');
|
||||
const rows = [];
|
||||
db.exec({
|
||||
sql: data.sql,
|
||||
bind: data.params ?? [],
|
||||
rowMode: 'array',
|
||||
callback: (row) => rows.push(row)
|
||||
});
|
||||
return postMessage({ id: data.id, results: { values: rows } });
|
||||
}
|
||||
case 'begin_transaction':
|
||||
db.exec('BEGIN TRANSACTION;');
|
||||
return postMessage({ id: data.id, results: [] });
|
||||
case 'end_transaction':
|
||||
db.exec('END TRANSACTION;');
|
||||
return postMessage({ id: data.id, results: [] });
|
||||
case 'rollback_transaction':
|
||||
db.exec('ROLLBACK TRANSACTION;');
|
||||
return postMessage({ id: data.id, results: [] });
|
||||
default:
|
||||
throw new Error(`Unsupported action: ${data && data.action}`);
|
||||
}
|
||||
} catch (err) {
|
||||
return postMessage({ id: data && data.id, error: err?.message ?? String(err) });
|
||||
self.onmessage = (event) => {
|
||||
const data = event.data;
|
||||
try {
|
||||
switch (data && data.action) {
|
||||
case 'exec': {
|
||||
if (!data.sql) throw new Error('exec: Missing query string');
|
||||
const rows = [];
|
||||
db.exec({
|
||||
sql: data.sql,
|
||||
bind: data.params ?? [],
|
||||
rowMode: 'array',
|
||||
callback: (row) => rows.push(row)
|
||||
});
|
||||
return postMessage({id: data.id, results: {values: rows}});
|
||||
}
|
||||
};
|
||||
case 'begin_transaction':
|
||||
db.exec('BEGIN TRANSACTION;');
|
||||
return postMessage({id: data.id, results: []});
|
||||
case 'end_transaction':
|
||||
db.exec('END TRANSACTION;');
|
||||
return postMessage({id: data.id, results: []});
|
||||
case 'rollback_transaction':
|
||||
db.exec('ROLLBACK TRANSACTION;');
|
||||
return postMessage({id: data.id, results: []});
|
||||
default:
|
||||
throw new Error(`Unsupported action: ${data && data.action}`);
|
||||
}
|
||||
} catch (err) {
|
||||
return postMessage({id: data && data.id, error: err?.message ?? String(err)});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Error handling wrapper
|
||||
self.onerror = function(event) {
|
||||
console.error("Error in Web Worker:", event.message, event.filename, event.lineno);
|
||||
// Optionally, send the error back to the main thread
|
||||
self.postMessage({ type: 'error', message: event.message, filename: event.filename, lineno: event.lineno });
|
||||
self.onerror = function (event) {
|
||||
console.error("Error in Web Worker:", event.message, event.filename, event.lineno);
|
||||
// Optionally, send the error back to the main thread
|
||||
self.postMessage({type: 'error', message: event.message, filename: event.filename, lineno: event.lineno});
|
||||
};
|
||||
|
||||
try {
|
||||
sqlite3InitModule({
|
||||
print: console.log,
|
||||
printErr: console.error,
|
||||
}).then((sqlite3) => {
|
||||
try {
|
||||
const opfsAvailable = 'opfs' in sqlite3;
|
||||
sqlite3InitModule({
|
||||
print: console.log,
|
||||
printErr: console.error,
|
||||
}).then((sqlite3) => {
|
||||
try {
|
||||
const opfsAvailable = 'opfs' in sqlite3;
|
||||
|
||||
runWorker({
|
||||
driver: {
|
||||
open: (name) => {
|
||||
if (opfsAvailable) {
|
||||
console.log("Initialisiere persistente OPFS Datenbank: " + name);
|
||||
return new sqlite3.oo1.OpfsDb(name);
|
||||
} else {
|
||||
console.warn("OPFS nicht verfügbar, Fallback auf In-Memory");
|
||||
return new sqlite3.oo1.DB(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Database initialization error in worker (inner):", e);
|
||||
self.postMessage({ type: 'error', message: 'Database initialization failed (inner): ' + e.message });
|
||||
runWorker({
|
||||
driver: {
|
||||
open: (name) => {
|
||||
if (opfsAvailable) {
|
||||
console.log("Initialisiere persistente OPFS Datenbank: " + name);
|
||||
return new sqlite3.oo1.OpfsDb(name);
|
||||
} else {
|
||||
console.warn("OPFS nicht verfügbar, Fallback auf In-Memory");
|
||||
return new sqlite3.oo1.DB(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Database initialization error in worker (inner):", e);
|
||||
self.postMessage({type: 'error', message: 'Database initialization failed (inner): ' + e.message});
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Database initialization error in worker (outer):", e);
|
||||
self.postMessage({ type: 'error', message: 'Database initialization failed (outer): ' + e.message });
|
||||
console.error("Database initialization error in worker (outer):", e);
|
||||
self.postMessage({type: 'error', message: 'Database initialization failed (outer): ' + e.message});
|
||||
}
|
||||
|
||||
+3
-1
@@ -8,7 +8,9 @@ import kotlin.test.assertTrue
|
||||
|
||||
private class FakeNav : NavigationPort {
|
||||
var last: String? = null
|
||||
override fun navigateTo(route: String) { last = route }
|
||||
override fun navigateTo(route: String) {
|
||||
last = route
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeUserProvider(private val user: User?) : CurrentUserProvider {
|
||||
|
||||
+7
-3
@@ -11,7 +11,7 @@ import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
/**
|
||||
* Simple token provider interface so core network module does not depend on auth-feature.
|
||||
* Simple token provider interface so the core network module does not depend on auth-feature.
|
||||
*/
|
||||
interface TokenProvider {
|
||||
fun getAccessToken(): String?
|
||||
@@ -41,7 +41,11 @@ val networkModule = module {
|
||||
|
||||
// 2. API Client (Configured for Gateway & Auth Header)
|
||||
single(named("apiClient")) {
|
||||
val tokenProvider: TokenProvider? = try { get<TokenProvider>() } catch (_: Throwable) { null }
|
||||
val tokenProvider: TokenProvider? = try {
|
||||
get<TokenProvider>()
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
HttpClient {
|
||||
// JSON (kotlinx) configuration
|
||||
install(ContentNegotiation) {
|
||||
@@ -79,7 +83,7 @@ val networkModule = module {
|
||||
// Inject Authorization header if token is present
|
||||
val token = tokenProvider?.getAccessToken()
|
||||
if (token != null) {
|
||||
header("Authorization", "Bearer $token")
|
||||
header("Authorization", "Bearer $token")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-4
@@ -13,9 +13,9 @@ actual object PlatformConfig {
|
||||
// In Wasm, we can access a window directly if we are in the browser main thread.
|
||||
// However, we need to be careful about exceptions.
|
||||
val origin = try {
|
||||
window.location.origin
|
||||
window.location.origin
|
||||
} catch (e: Throwable) {
|
||||
null
|
||||
null
|
||||
}
|
||||
|
||||
if (!origin.isNullOrBlank()) return origin.removeSuffix("/")
|
||||
@@ -28,9 +28,11 @@ actual object PlatformConfig {
|
||||
// Helper function for JS interop in Wasm
|
||||
// Kotlin/Wasm does not support 'dynamic' type or complex js() blocks inside functions.
|
||||
// We must use top-level external functions or simple js() expressions.
|
||||
private fun getGlobalApiBaseUrl(): String = js("""
|
||||
private fun getGlobalApiBaseUrl(): String = js(
|
||||
"""
|
||||
(function() {
|
||||
var global = typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : (typeof self !== 'undefined' ? self : {}));
|
||||
return (global.API_BASE_URL && typeof global.API_BASE_URL === 'string') ? global.API_BASE_URL : "";
|
||||
})()
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
+213
-213
@@ -23,274 +23,274 @@ import at.mocode.frontend.core.designsystem.theme.Dimens
|
||||
|
||||
@Composable
|
||||
fun PingScreen(
|
||||
viewModel: PingViewModel,
|
||||
onBack: () -> Unit = {}
|
||||
viewModel: PingViewModel,
|
||||
onBack: () -> Unit = {}
|
||||
) {
|
||||
val uiState = viewModel.uiState
|
||||
val uiState = viewModel.uiState
|
||||
|
||||
// Wir nutzen jetzt das globale Theme (Hintergrund kommt vom Theme)
|
||||
Column(
|
||||
// Wir nutzen jetzt das globale Theme (Hintergrund kommt vom Theme)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(Dimens.SpacingS) // Globales Spacing
|
||||
) {
|
||||
// 1. Header
|
||||
PingHeader(
|
||||
onBack = onBack,
|
||||
isSyncing = uiState.isSyncing,
|
||||
isLoading = uiState.isLoading
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(Dimens.SpacingS))
|
||||
|
||||
// 2. Main Dashboard Area (Split View)
|
||||
Row(modifier = Modifier.weight(1f)) {
|
||||
// Left Panel: Controls & Status Grid (60%)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(Dimens.SpacingS) // Globales Spacing
|
||||
) {
|
||||
// 1. Header
|
||||
PingHeader(
|
||||
onBack = onBack,
|
||||
isSyncing = uiState.isSyncing,
|
||||
isLoading = uiState.isLoading
|
||||
)
|
||||
|
||||
.weight(0.6f)
|
||||
.fillMaxHeight()
|
||||
.padding(end = Dimens.SpacingS)
|
||||
) {
|
||||
ActionToolbar(viewModel)
|
||||
Spacer(Modifier.height(Dimens.SpacingS))
|
||||
StatusGrid(uiState)
|
||||
}
|
||||
|
||||
// 2. Main Dashboard Area (Split View)
|
||||
Row(modifier = Modifier.weight(1f)) {
|
||||
// Left Panel: Controls & Status Grid (60%)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(0.6f)
|
||||
.fillMaxHeight()
|
||||
.padding(end = Dimens.SpacingS)
|
||||
) {
|
||||
ActionToolbar(viewModel)
|
||||
Spacer(Modifier.height(Dimens.SpacingS))
|
||||
StatusGrid(uiState)
|
||||
}
|
||||
|
||||
// Right Panel: Terminal Log (40%)
|
||||
// Hier nutzen wir bewusst einen dunklen "Terminal"-Look, unabhängig vom Theme
|
||||
DashboardCard(
|
||||
modifier = Modifier
|
||||
.weight(0.4f)
|
||||
.fillMaxHeight()
|
||||
) {
|
||||
LogHeader(onClear = { viewModel.clearLogs() })
|
||||
LogConsole(uiState.logs)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(Dimens.SpacingXS))
|
||||
|
||||
// 3. Footer
|
||||
PingStatusBar(uiState.lastSyncResult)
|
||||
// Right Panel: Terminal Log (40%)
|
||||
// Hier nutzen wir bewusst einen dunklen "Terminal"-Look, unabhängig vom Theme
|
||||
DashboardCard(
|
||||
modifier = Modifier
|
||||
.weight(0.4f)
|
||||
.fillMaxHeight()
|
||||
) {
|
||||
LogHeader(onClear = { viewModel.clearLogs() })
|
||||
LogConsole(uiState.logs)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(Dimens.SpacingXS))
|
||||
|
||||
// 3. Footer
|
||||
PingStatusBar(uiState.lastSyncResult)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PingHeader(
|
||||
onBack: () -> Unit,
|
||||
isSyncing: Boolean,
|
||||
isLoading: Boolean
|
||||
onBack: () -> Unit,
|
||||
isSyncing: Boolean,
|
||||
isLoading: Boolean
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth().height(40.dp)
|
||||
) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back", tint = MaterialTheme.colorScheme.onBackground)
|
||||
}
|
||||
Text(
|
||||
"PING SERVICE // DASHBOARD",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.weight(1f).padding(start = Dimens.SpacingS)
|
||||
)
|
||||
|
||||
if (isLoading) {
|
||||
StatusBadge("BUSY", Color(0xFFFFA000)) // Amber
|
||||
Spacer(Modifier.width(Dimens.SpacingS))
|
||||
}
|
||||
|
||||
if (isSyncing) {
|
||||
StatusBadge("SYNCING", MaterialTheme.colorScheme.primary)
|
||||
Spacer(Modifier.width(Dimens.SpacingS))
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(16.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
} else {
|
||||
StatusBadge("IDLE", Color(0xFF388E3C)) // Green
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth().height(40.dp)
|
||||
) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back", tint = MaterialTheme.colorScheme.onBackground)
|
||||
}
|
||||
Text(
|
||||
"PING SERVICE // DASHBOARD",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = Modifier.weight(1f).padding(start = Dimens.SpacingS)
|
||||
)
|
||||
|
||||
if (isLoading) {
|
||||
StatusBadge("BUSY", Color(0xFFFFA000)) // Amber
|
||||
Spacer(Modifier.width(Dimens.SpacingS))
|
||||
}
|
||||
|
||||
if (isSyncing) {
|
||||
StatusBadge("SYNCING", MaterialTheme.colorScheme.primary)
|
||||
Spacer(Modifier.width(Dimens.SpacingS))
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(16.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
} else {
|
||||
StatusBadge("IDLE", Color(0xFF388E3C)) // Green
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatusBadge(text: String, color: Color) {
|
||||
Surface(
|
||||
color = color.copy(alpha = 0.1f),
|
||||
contentColor = color,
|
||||
shape = MaterialTheme.shapes.small,
|
||||
border = androidx.compose.foundation.BorderStroke(1.dp, color)
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp)
|
||||
)
|
||||
}
|
||||
Surface(
|
||||
color = color.copy(alpha = 0.1f),
|
||||
contentColor = color,
|
||||
shape = MaterialTheme.shapes.small,
|
||||
border = androidx.compose.foundation.BorderStroke(1.dp, color)
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActionToolbar(viewModel: PingViewModel) {
|
||||
// Wrap buttons to avoid overflow on small screens
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingXS)
|
||||
) {
|
||||
DenseButton(text = "Simple", onClick = { viewModel.performSimplePing() })
|
||||
DenseButton(text = "Enhanced", onClick = { viewModel.performEnhancedPing() })
|
||||
DenseButton(text = "Secure", onClick = { viewModel.performSecurePing() })
|
||||
DenseButton(text = "Health", onClick = { viewModel.performHealthCheck() })
|
||||
DenseButton(
|
||||
text = "Sync",
|
||||
onClick = { viewModel.triggerSync() },
|
||||
containerColor = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
}
|
||||
// Wrap buttons to avoid overflow on small screens
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingXS)
|
||||
) {
|
||||
DenseButton(text = "Simple", onClick = { viewModel.performSimplePing() })
|
||||
DenseButton(text = "Enhanced", onClick = { viewModel.performEnhancedPing() })
|
||||
DenseButton(text = "Secure", onClick = { viewModel.performSecurePing() })
|
||||
DenseButton(text = "Health", onClick = { viewModel.performHealthCheck() })
|
||||
DenseButton(
|
||||
text = "Sync",
|
||||
onClick = { viewModel.triggerSync() },
|
||||
containerColor = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatusGrid(uiState: PingUiState) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(Dimens.SpacingS)) {
|
||||
// Row 1
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS)) {
|
||||
DashboardCard(modifier = Modifier.weight(1f)) {
|
||||
StatusHeader("SIMPLE / SECURE PING")
|
||||
if (uiState.simplePingResponse != null) {
|
||||
KeyValueRow("Status", uiState.simplePingResponse.status)
|
||||
KeyValueRow("Service", uiState.simplePingResponse.service)
|
||||
KeyValueRow("Time", uiState.simplePingResponse.timestamp)
|
||||
} else {
|
||||
EmptyStateText()
|
||||
}
|
||||
}
|
||||
|
||||
DashboardCard(modifier = Modifier.weight(1f)) {
|
||||
StatusHeader("HEALTH CHECK")
|
||||
if (uiState.healthResponse != null) {
|
||||
KeyValueRow("Status", uiState.healthResponse.status)
|
||||
KeyValueRow("Healthy", uiState.healthResponse.healthy.toString())
|
||||
KeyValueRow("Service", uiState.healthResponse.service)
|
||||
} else {
|
||||
EmptyStateText()
|
||||
}
|
||||
}
|
||||
Column(verticalArrangement = Arrangement.spacedBy(Dimens.SpacingS)) {
|
||||
// Row 1
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(Dimens.SpacingS)) {
|
||||
DashboardCard(modifier = Modifier.weight(1f)) {
|
||||
StatusHeader("SIMPLE / SECURE PING")
|
||||
if (uiState.simplePingResponse != null) {
|
||||
KeyValueRow("Status", uiState.simplePingResponse.status)
|
||||
KeyValueRow("Service", uiState.simplePingResponse.service)
|
||||
KeyValueRow("Time", uiState.simplePingResponse.timestamp)
|
||||
} else {
|
||||
EmptyStateText()
|
||||
}
|
||||
}
|
||||
|
||||
// Row 2
|
||||
DashboardCard(modifier = Modifier.fillMaxWidth()) {
|
||||
StatusHeader("ENHANCED PING (RESILIENCE)")
|
||||
if (uiState.enhancedPingResponse != null) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
KeyValueRow("Status", uiState.enhancedPingResponse.status)
|
||||
KeyValueRow("Timestamp", uiState.enhancedPingResponse.timestamp)
|
||||
}
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
KeyValueRow("Circuit Breaker", uiState.enhancedPingResponse.circuitBreakerState)
|
||||
KeyValueRow("Latency", "${uiState.enhancedPingResponse.responseTime}ms")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
EmptyStateText()
|
||||
}
|
||||
DashboardCard(modifier = Modifier.weight(1f)) {
|
||||
StatusHeader("HEALTH CHECK")
|
||||
if (uiState.healthResponse != null) {
|
||||
KeyValueRow("Status", uiState.healthResponse.status)
|
||||
KeyValueRow("Healthy", uiState.healthResponse.healthy.toString())
|
||||
KeyValueRow("Service", uiState.healthResponse.service)
|
||||
} else {
|
||||
EmptyStateText()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Row 2
|
||||
DashboardCard(modifier = Modifier.fillMaxWidth()) {
|
||||
StatusHeader("ENHANCED PING (RESILIENCE)")
|
||||
if (uiState.enhancedPingResponse != null) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
KeyValueRow("Status", uiState.enhancedPingResponse.status)
|
||||
KeyValueRow("Timestamp", uiState.enhancedPingResponse.timestamp)
|
||||
}
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
KeyValueRow("Circuit Breaker", uiState.enhancedPingResponse.circuitBreakerState)
|
||||
KeyValueRow("Latency", "${uiState.enhancedPingResponse.responseTime}ms")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
EmptyStateText()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatusHeader(title: String) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(bottom = Dimens.SpacingXS)
|
||||
)
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant, thickness = 1.dp)
|
||||
Spacer(Modifier.height(Dimens.SpacingXS))
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(bottom = Dimens.SpacingXS)
|
||||
)
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant, thickness = 1.dp)
|
||||
Spacer(Modifier.height(Dimens.SpacingXS))
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyStateText() {
|
||||
Text("No Data", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Text("No Data", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KeyValueRow(key: String, value: String) {
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp)) {
|
||||
Text(
|
||||
text = "$key:",
|
||||
modifier = Modifier.width(100.dp),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp)) {
|
||||
Text(
|
||||
text = "$key:",
|
||||
modifier = Modifier.width(100.dp),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Log Components (Terminal Style - intentionally distinct) ---
|
||||
|
||||
@Composable
|
||||
private fun LogHeader(onClear: () -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = Dimens.SpacingXS),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = Dimens.SpacingXS),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text("EVENT LOG", style = MaterialTheme.typography.labelSmall, fontWeight = FontWeight.Bold)
|
||||
TextButton(
|
||||
onClick = onClear,
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
modifier = Modifier.height(24.dp)
|
||||
) {
|
||||
Text("EVENT LOG", style = MaterialTheme.typography.labelSmall, fontWeight = FontWeight.Bold)
|
||||
TextButton(
|
||||
onClick = onClear,
|
||||
contentPadding = PaddingValues(0.dp),
|
||||
modifier = Modifier.height(24.dp)
|
||||
) {
|
||||
Text("CLEAR", style = MaterialTheme.typography.labelSmall)
|
||||
}
|
||||
Text("CLEAR", style = MaterialTheme.typography.labelSmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LogConsole(logs: List<LogEntry>) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFF1E1E1E)) // Always dark for terminal
|
||||
.padding(Dimens.SpacingXS),
|
||||
reverseLayout = false
|
||||
) {
|
||||
items(logs) { log ->
|
||||
val color = if (log.isError) Color(0xFFFF5555) else Color(0xFF55FF55)
|
||||
Text(
|
||||
text = "[${log.timestamp}] [${log.source}] ${log.message}",
|
||||
color = color,
|
||||
fontSize = 11.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
lineHeight = 14.sp
|
||||
)
|
||||
}
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFF1E1E1E)) // Always dark for terminal
|
||||
.padding(Dimens.SpacingXS),
|
||||
reverseLayout = false
|
||||
) {
|
||||
items(logs) { log ->
|
||||
val color = if (log.isError) Color(0xFFFF5555) else Color(0xFF55FF55)
|
||||
Text(
|
||||
text = "[${log.timestamp}] [${log.source}] ${log.message}",
|
||||
color = color,
|
||||
fontSize = 11.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
lineHeight = 14.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PingStatusBar(lastSync: String?) {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.primaryContainer,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = lastSync ?: "Ready",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
modifier = Modifier.padding(horizontal = Dimens.SpacingS, vertical = 2.dp)
|
||||
)
|
||||
}
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.primaryContainer,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = lastSync ?: "Ready",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
modifier = Modifier.padding(horizontal = Dimens.SpacingS, vertical = 2.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+7
-5
@@ -16,10 +16,10 @@ import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
|
||||
data class LogEntry(
|
||||
val timestamp: String,
|
||||
val source: String,
|
||||
val message: String,
|
||||
val isError: Boolean = false
|
||||
val timestamp: String,
|
||||
val source: String,
|
||||
val message: String,
|
||||
val isError: Boolean = false
|
||||
)
|
||||
|
||||
data class PingUiState(
|
||||
@@ -43,7 +43,9 @@ class PingViewModel(
|
||||
|
||||
private fun addLog(source: String, message: String, isError: Boolean = false) {
|
||||
val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
|
||||
val timeString = "${now.hour.toString().padStart(2, '0')}:${now.minute.toString().padStart(2, '0')}:${now.second.toString().padStart(2, '0')}"
|
||||
val timeString = "${now.hour.toString().padStart(2, '0')}:${now.minute.toString().padStart(2, '0')}:${
|
||||
now.second.toString().padStart(2, '0')
|
||||
}"
|
||||
val entry = LogEntry(timeString, source, message, isError)
|
||||
uiState = uiState.copy(logs = listOf(entry) + uiState.logs) // Prepend for newest first
|
||||
}
|
||||
|
||||
@@ -1,93 +1,109 @@
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
alias(libs.plugins.composeMultiplatform)
|
||||
alias(libs.plugins.composeCompiler)
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
alias(libs.plugins.composeMultiplatform)
|
||||
alias(libs.plugins.composeCompiler)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvm("desktop")
|
||||
jvm("desktop")
|
||||
|
||||
js(IR) {
|
||||
browser()
|
||||
binaries.executable()
|
||||
js(IR) {
|
||||
// WICHTIG: Als Library kompilieren für Webpack Federation
|
||||
binaries.library()
|
||||
generateTypeScriptDefinitions()
|
||||
browser {
|
||||
commonWebpackConfig {
|
||||
cssSupport {
|
||||
enabled.set(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wasm vorerst deaktiviert
|
||||
/*
|
||||
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||
wasmJs {
|
||||
browser()
|
||||
binaries.executable()
|
||||
}
|
||||
*/
|
||||
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
implementation(projects.frontend.core.domain)
|
||||
// implementation(projects.frontend.core.designSystem) // REMOVED: Circular dependency
|
||||
implementation(projects.frontend.core.navigation)
|
||||
implementation(projects.frontend.core.network)
|
||||
implementation(projects.frontend.core.localDb)
|
||||
|
||||
// Features - REMOVED: Circular dependency. Shared should NOT depend on features.
|
||||
// implementation(projects.frontend.features.authFeature)
|
||||
// implementation(projects.frontend.features.pingFeature)
|
||||
|
||||
// KMP Bundles
|
||||
implementation(libs.bundles.kmp.common)
|
||||
implementation(libs.bundles.compose.common)
|
||||
|
||||
// Ktor (used directly in shared/di and shared/network)
|
||||
implementation(libs.ktor.client.core)
|
||||
implementation(libs.ktor.client.contentNegotiation)
|
||||
implementation(libs.ktor.client.logging)
|
||||
implementation(libs.ktor.client.serialization.kotlinx.json)
|
||||
|
||||
// Serialization
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
// implementation(libs.sqldelight.coroutines) // Wird transitiv über core:localDb geladen
|
||||
|
||||
// Compose
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.material3)
|
||||
implementation(compose.ui)
|
||||
implementation(compose.components.resources)
|
||||
implementation(compose.components.uiToolingPreview)
|
||||
|
||||
// Koin
|
||||
implementation(libs.koin.core)
|
||||
implementation(libs.koin.compose)
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
}
|
||||
}
|
||||
|
||||
commonTest {
|
||||
dependencies {
|
||||
implementation(libs.kotlin.test)
|
||||
}
|
||||
}
|
||||
|
||||
val desktopMain by getting {
|
||||
dependencies {
|
||||
implementation(compose.desktop.currentOs)
|
||||
implementation(libs.kotlinx.coroutines.swing)
|
||||
// implementation(libs.sqldelight.driver.sqlite) // Wird transitiv über core:localDb geladen
|
||||
}
|
||||
}
|
||||
|
||||
val jsMain by getting {
|
||||
dependencies {
|
||||
implementation(libs.ktor.client.js)
|
||||
// implementation(libs.sqldelight.driver.web) // Wird transitiv über core:localDb geladen
|
||||
|
||||
// Webpack Plugin für Federation Support (falls benötigt)
|
||||
implementation(devNpm("copy-webpack-plugin", "12.0.0"))
|
||||
}
|
||||
}
|
||||
|
||||
// Wasm vorerst deaktiviert
|
||||
/*
|
||||
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||
wasmJs {
|
||||
browser()
|
||||
binaries.executable()
|
||||
val wasmJsMain by getting {
|
||||
dependencies {
|
||||
implementation(libs.ktor.client.js)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
implementation(projects.frontend.core.domain)
|
||||
// implementation(projects.frontend.core.designSystem) // REMOVED: Circular dependency
|
||||
implementation(projects.frontend.core.navigation)
|
||||
implementation(projects.frontend.core.network)
|
||||
implementation(projects.frontend.core.localDb)
|
||||
|
||||
// Features - REMOVED: Circular dependency. Shared should NOT depend on features.
|
||||
// implementation(projects.frontend.features.authFeature)
|
||||
// implementation(projects.frontend.features.pingFeature)
|
||||
|
||||
// KMP Bundles
|
||||
implementation(libs.bundles.kmp.common)
|
||||
implementation(libs.bundles.compose.common)
|
||||
|
||||
// Ktor (used directly in shared/di and shared/network)
|
||||
implementation(libs.ktor.client.core)
|
||||
implementation(libs.ktor.client.contentNegotiation)
|
||||
implementation(libs.ktor.client.logging)
|
||||
implementation(libs.ktor.client.serialization.kotlinx.json)
|
||||
|
||||
// Serialization
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
// Compose
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.material3)
|
||||
implementation(compose.ui)
|
||||
implementation(compose.components.resources)
|
||||
implementation(compose.components.uiToolingPreview)
|
||||
|
||||
// Koin
|
||||
implementation(libs.koin.core)
|
||||
implementation(libs.koin.compose)
|
||||
implementation(libs.koin.compose.viewmodel)
|
||||
}
|
||||
}
|
||||
|
||||
commonTest {
|
||||
dependencies {
|
||||
implementation(libs.kotlin.test)
|
||||
}
|
||||
}
|
||||
|
||||
val desktopMain by getting {
|
||||
dependencies {
|
||||
implementation(compose.desktop.currentOs)
|
||||
implementation(libs.kotlinx.coroutines.swing)
|
||||
}
|
||||
}
|
||||
|
||||
val jsMain by getting {
|
||||
dependencies {
|
||||
implementation(libs.ktor.client.js)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
val wasmJsMain by getting {
|
||||
dependencies {
|
||||
implementation(libs.ktor.client.js)
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package at.mocode.shared.network
|
||||
|
||||
|
||||
import at.mocode.shared.domain.model.ApiError
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
|
||||
@@ -51,8 +51,6 @@ kotlin {
|
||||
}
|
||||
// Browser-Tests komplett deaktivieren (Configuration Cache kompatibel)
|
||||
testTask {
|
||||
// enabled = false
|
||||
|
||||
useKarma {
|
||||
useChromeHeadless()
|
||||
environment("CHROME_BIN", "/usr/bin/google-chrome-stable")
|
||||
@@ -62,15 +60,6 @@ kotlin {
|
||||
binaries.executable()
|
||||
}
|
||||
|
||||
// Wasm vorerst deaktiviert
|
||||
/*
|
||||
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
|
||||
wasmJs {
|
||||
browser()
|
||||
binaries.executable()
|
||||
}
|
||||
*/
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
// Shared modules
|
||||
@@ -114,18 +103,6 @@ kotlin {
|
||||
implementation(devNpm("copy-webpack-plugin", "11.0.0"))
|
||||
}
|
||||
|
||||
/*
|
||||
val wasmJsMain = getByName("wasmJsMain")
|
||||
wasmJsMain.dependencies {
|
||||
implementation(libs.ktor.client.js) // WASM verwendet JS-Client [cite: 7]
|
||||
|
||||
// Compose für shared UI components für WASM
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.material3)
|
||||
}
|
||||
*/
|
||||
|
||||
commonTest.dependencies {
|
||||
implementation(libs.kotlin.test)
|
||||
}
|
||||
@@ -136,104 +113,28 @@ kotlin {
|
||||
// SQLDelight WebWorker (OPFS) resource
|
||||
// ---------------------------------------------------------------------------
|
||||
// `:frontend:core:local-db` ships `sqlite.worker.js` as a JS resource.
|
||||
// When bundling the final JS app, webpack resolves `new URL("sqlite.worker.js", import.meta.url)`
|
||||
// relative to the Kotlin JS package folder (root build dir). We therefore copy the worker into
|
||||
// that folder before webpack runs.
|
||||
// We need to ensure this worker file is available in the output directory so the browser can load it.
|
||||
// The WASM file itself is handled by Webpack (via CopyWebpackPlugin in webpack.config.d/sqlite-config.js).
|
||||
|
||||
// HACK: Overwrite sqlite3.wasm in node_modules with a dummy JS file to fool Webpack
|
||||
val patchSqliteWasmInNodeModules by tasks.registering(Copy::class) {
|
||||
dependsOn(rootProject.tasks.named("kotlinNpmInstall"))
|
||||
val copySqliteWorkerToWebpackSource by tasks.registering(Copy::class) {
|
||||
val localDb = project(":frontend:core:local-db")
|
||||
dependsOn(localDb.tasks.named("jsProcessResources"))
|
||||
|
||||
// We take our dummy.js
|
||||
from(localDb.layout.buildDirectory.file("processedResources/js/main/dummy.js")) {
|
||||
rename { "sqlite3.wasm" } // Rename it to sqlite3.wasm
|
||||
}
|
||||
|
||||
// And copy it OVER the original wasm file in node_modules
|
||||
into(rootProject.layout.buildDirectory.dir("js/node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm"))
|
||||
|
||||
// Force overwrite
|
||||
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||
}
|
||||
|
||||
val copySqliteAssetsToWebpackSource by tasks.registering(Copy::class) {
|
||||
val localDb = project(":frontend:core:local-db")
|
||||
dependsOn(localDb.tasks.named("jsProcessResources"), rootProject.tasks.named("kotlinNpmInstall"))
|
||||
|
||||
// Explicit dependency on the patch task to ensure we copy the REAL wasm file before it gets patched?
|
||||
// NO! We want to copy the REAL wasm file to the output, but PATCH the one in node_modules.
|
||||
// So we must copy the real one BEFORE patching.
|
||||
// But wait, copySqliteAssetsToWebpackSource copies FROM node_modules.
|
||||
// If we patch node_modules first, we copy the dummy file!
|
||||
|
||||
// So: copySqliteAssetsToWebpackSource must run BEFORE patchSqliteWasmInNodeModules?
|
||||
// Or we copy from a different source (e.g. the original npm package cache? No access).
|
||||
|
||||
// Better: We copy the real wasm file from node_modules to a temporary location FIRST,
|
||||
// then patch node_modules, then copy from temp to output.
|
||||
|
||||
// Actually, we can just copy from node_modules BEFORE the patch task runs.
|
||||
// But Gradle task ordering is tricky.
|
||||
|
||||
// Let's change the source of the copy. We can't easily access the original npm package.
|
||||
// But we know that `kotlinNpmInstall` restores the original files.
|
||||
|
||||
// So the order must be:
|
||||
// 1. kotlinNpmInstall (restores original sqlite3.wasm)
|
||||
// 2. copySqliteAssetsToWebpackSource (copies original sqlite3.wasm to output)
|
||||
// 3. patchSqliteWasmInNodeModules (overwrites sqlite3.wasm with dummy.js)
|
||||
// 4. webpack (uses dummy.js)
|
||||
|
||||
mustRunAfter(rootProject.tasks.named("kotlinNpmInstall"))
|
||||
|
||||
from(localDb.layout.buildDirectory.file("processedResources/js/main/sqlite.worker.js"))
|
||||
from(rootProject.layout.buildDirectory.file("js/node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3.wasm"))
|
||||
|
||||
// Root build directory where Kotlin JS packages are assembled.
|
||||
// This is one of the directories served by webpack-dev-server for static content.
|
||||
into(rootProject.layout.buildDirectory.dir("js/packages/${rootProject.name}-frontend-shells-meldestelle-portal/kotlin"))
|
||||
}
|
||||
|
||||
// Additional task to copy the worker and its wasm dependency to the distribution folder (for production build)
|
||||
val copySqliteAssetsToDist by tasks.registering(Copy::class) {
|
||||
val localDb = project(":frontend:core:local-db")
|
||||
dependsOn(localDb.tasks.named("jsProcessResources"), rootProject.tasks.named("kotlinNpmInstall"))
|
||||
|
||||
// Same logic here: copy before patch
|
||||
mustRunAfter(rootProject.tasks.named("kotlinNpmInstall"))
|
||||
|
||||
from(localDb.layout.buildDirectory.file("processedResources/js/main/sqlite.worker.js"))
|
||||
from(rootProject.layout.buildDirectory.file("js/node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3.wasm"))
|
||||
|
||||
// Copy to the distribution directory where index.html resides
|
||||
into(layout.buildDirectory.dir("dist/js/productionExecutable"))
|
||||
}
|
||||
|
||||
// Ensure the assets are present for the production bundle.
|
||||
tasks.named("jsBrowserProductionWebpack") {
|
||||
dependsOn(copySqliteAssetsToWebpackSource)
|
||||
dependsOn(patchSqliteWasmInNodeModules)
|
||||
|
||||
// Enforce order: Copy real assets first, then patch node_modules
|
||||
// patchSqliteWasmInNodeModules must run AFTER copySqliteAssetsToWebpackSource
|
||||
// But wait, dependsOn doesn't guarantee order.
|
||||
// We need to configure the tasks themselves.
|
||||
|
||||
finalizedBy(copySqliteAssetsToDist)
|
||||
}
|
||||
|
||||
// Configure task ordering
|
||||
patchSqliteWasmInNodeModules {
|
||||
mustRunAfter(copySqliteAssetsToWebpackSource)
|
||||
// Removed circular dependency: mustRunAfter(copySqliteAssetsToDist)
|
||||
}
|
||||
|
||||
// Ensure the assets are present for the development bundle.
|
||||
// Ensure the worker is present for the development bundle.
|
||||
tasks.named("jsBrowserDevelopmentWebpack") {
|
||||
dependsOn(copySqliteAssetsToWebpackSource)
|
||||
dependsOn(patchSqliteWasmInNodeModules)
|
||||
dependsOn(copySqliteWorkerToWebpackSource)
|
||||
}
|
||||
|
||||
// Ensure the worker is present for the production bundle.
|
||||
tasks.named("jsBrowserProductionWebpack") {
|
||||
dependsOn(copySqliteWorkerToWebpackSource)
|
||||
}
|
||||
|
||||
// KMP Compile-Optionen
|
||||
@@ -253,9 +154,6 @@ tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||
// Kotlin/JS source maps
|
||||
// ---------------------------------------------------------------------------
|
||||
// Production source maps must remain enabled for browser debugging.
|
||||
// The remaining Kotlin/Gradle message
|
||||
// `Cannot rewrite paths in JavaScript source maps: Too many sources or format is not supported`
|
||||
// is treated as an external Kotlin/JS toolchain limitation and is documented separately.
|
||||
|
||||
// Configure a duplicate handling strategy for distribution tasks
|
||||
tasks.withType<Tar> {
|
||||
@@ -268,7 +166,7 @@ tasks.withType<Zip> {
|
||||
|
||||
// Duplicate-Handling für Distribution
|
||||
tasks.withType<Copy> {
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE // Statt EXCLUDE
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
}
|
||||
|
||||
tasks.withType<Sync> {
|
||||
|
||||
@@ -39,6 +39,7 @@ fun MainApp() {
|
||||
onPrimaryCta = { currentScreen = AppScreen.Login },
|
||||
onSecondary = { currentScreen = AppScreen.Home }
|
||||
)
|
||||
|
||||
is AppScreen.Home -> WelcomeScreen(
|
||||
authTokenManager = authTokenManager,
|
||||
onOpenPing = { currentScreen = AppScreen.Ping },
|
||||
@@ -169,7 +170,7 @@ private fun LandingScreen(
|
||||
|
||||
@Composable
|
||||
private fun FeatureCard(number: String, title: String, body: String) {
|
||||
Surface( tonalElevation = 0.dp ) {
|
||||
Surface(tonalElevation = 0.dp) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.width(56.dp).padding(top = 6.dp)) {
|
||||
Text(text = number, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary)
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
// This is a dummy file to satisfy Webpack's requirement for sqlite3.wasm and other modules.
|
||||
// It mimics the structure of the sqlite3-wasm module to prevent build errors.
|
||||
|
||||
// The worker code imports it like this:
|
||||
// import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
|
||||
|
||||
// So we need a default export that is a function.
|
||||
// This function should mimic the behavior of sqlite3InitModule, which returns a Promise.
|
||||
export default function dummySqlite3InitModule() {
|
||||
// Since we are manually loading the WASM binary in the worker, this dummy module
|
||||
// is primarily here to satisfy Webpack's resolution and prevent errors.
|
||||
// It doesn't need to actually load the WASM.
|
||||
return Promise.resolve({});
|
||||
};
|
||||
@@ -15,7 +15,7 @@
|
||||
<script>
|
||||
// Prefer explicit query param override (?apiBaseUrl=http://host:port),
|
||||
// then fall back to same-origin. This avoids Docker secrets and works with Nginx proxy.
|
||||
(function(){
|
||||
(function () {
|
||||
try {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const override = params.get('apiBaseUrl');
|
||||
@@ -29,13 +29,13 @@
|
||||
}
|
||||
})();
|
||||
// KMP bundle will read globalThis.API_BASE_URL in PlatformConfig.js
|
||||
</script>
|
||||
</script>
|
||||
<script src="web-app.js"></script>
|
||||
<script>
|
||||
// Register Service Worker only in non-localhost environments
|
||||
if ('serviceWorker' in navigator && !['localhost', '127.0.0.1', '::1'].includes(location.hostname)) {
|
||||
window.addEventListener('load', function() {
|
||||
navigator.serviceWorker.register('/sw.js').catch(function(err){
|
||||
window.addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('/sw.js').catch(function (err) {
|
||||
console.warn('ServiceWorker registration failed:', err);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// Dummy module to satisfy WASI imports in Webpack
|
||||
// Used for skiko.wasm and potentially others
|
||||
|
||||
export function abort() {
|
||||
console.error("WASI abort called");
|
||||
}
|
||||
|
||||
// Some WASM modules might look for these
|
||||
export function emscripten_notify_memory_growth() {
|
||||
}
|
||||
|
||||
export default {
|
||||
abort,
|
||||
emscripten_notify_memory_growth
|
||||
};
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
// Suppress a known, external webpack warning coming from `@sqlite.org/sqlite-wasm`.
|
||||
//
|
||||
// Webpack warning:
|
||||
// "Critical dependency: the request of a dependency is an expression"
|
||||
//
|
||||
// Root cause:
|
||||
// `@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3.mjs` uses a dynamic Worker URL:
|
||||
// `new Worker(new URL(options.proxyUri, import.meta.url))`
|
||||
// which webpack cannot statically analyze.
|
||||
//
|
||||
// We keep this suppression максимально spezifisch:
|
||||
// - match only this warning message
|
||||
// - and only if it originates from the sqlite-wasm package path.
|
||||
|
||||
(function (config) {
|
||||
config.ignoreWarnings = config.ignoreWarnings || []
|
||||
|
||||
// Webpack passes warning objects with `message` and `module.resource`.
|
||||
config.ignoreWarnings.push((warning) => {
|
||||
const message = String(warning && warning.message ? warning.message : warning)
|
||||
if (!message.includes('Critical dependency: the request of a dependency is an expression')) return false
|
||||
|
||||
const resource = warning && warning.module && warning.module.resource
|
||||
? String(warning.module.resource)
|
||||
: ''
|
||||
|
||||
return resource.includes('node_modules/@sqlite.org/sqlite-wasm/')
|
||||
})
|
||||
})(config)
|
||||
@@ -1,104 +0,0 @@
|
||||
// This file contains Webpack configuration adjustments for WebAssembly modules,
|
||||
// specifically to handle `skiko.wasm` and `sqlite3.wasm` correctly.
|
||||
|
||||
var pathModule;
|
||||
try {
|
||||
pathModule = path;
|
||||
} catch (e) {
|
||||
pathModule = require('path');
|
||||
}
|
||||
|
||||
var webpackModule;
|
||||
try {
|
||||
webpackModule = webpack;
|
||||
} catch (e) {
|
||||
webpackModule = require('webpack');
|
||||
}
|
||||
|
||||
// 1. Enable WebAssembly experiments in Webpack 5
|
||||
config.experiments = config.experiments || {};
|
||||
config.experiments.asyncWebAssembly = true;
|
||||
|
||||
config.module = config.module || {};
|
||||
config.module.rules = config.module.rules || [];
|
||||
|
||||
// 2. Add a rule to correctly handle .wasm files (like skiko.wasm) as WebAssembly modules
|
||||
config.module.rules.push({
|
||||
test: /\.wasm$/,
|
||||
type: 'webassembly/async'
|
||||
});
|
||||
|
||||
// 3. NormalModuleReplacementPlugin to redirect 'sqlite3.wasm' AND other internal sqlite-wasm modules to our dummy JS file.
|
||||
// This is needed because the `sqlite-wasm` library tries to `require` these files in a Webpack environment.
|
||||
// We want these `require` calls to return an empty JS object (from dummy.js) instead of failing.
|
||||
// Our worker will manually fetch the real sqlite3.wasm.
|
||||
|
||||
const dummyPath = pathModule.resolve(__dirname, "../../../../frontend/shells/meldestelle-portal/build/processedResources/js/main/dummy.js");
|
||||
|
||||
// Redirect sqlite3.wasm
|
||||
config.plugins.push(
|
||||
new webpackModule.NormalModuleReplacementPlugin(
|
||||
/sqlite3\.wasm$/,
|
||||
dummyPath
|
||||
)
|
||||
);
|
||||
|
||||
// Redirect other internal sqlite-wasm modules that might be causing issues
|
||||
// The error log showed: Can't resolve './sqlite-wasm/jswasm/sqlite3.mjs' and 'sqlite3-worker1-promiser.mjs'
|
||||
// We redirect them to dummy.js as well, assuming we don't need them for our manual loading approach.
|
||||
// Be careful not to redirect the main entry point if it's needed.
|
||||
// The errors seem to come from inside the node_modules package trying to resolve relative paths.
|
||||
|
||||
// Let's try to be more specific. If these are optional dependencies or part of the node-loading logic,
|
||||
// replacing them with dummy.js should be fine.
|
||||
config.plugins.push(
|
||||
new webpackModule.NormalModuleReplacementPlugin(
|
||||
/sqlite3\.mjs$/,
|
||||
function(resource) {
|
||||
// Only replace if it's inside the sqlite-wasm package structure we want to avoid
|
||||
if (resource.context.includes('@sqlite.org/sqlite-wasm')) {
|
||||
resource.request = dummyPath;
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
config.plugins.push(
|
||||
new webpackModule.NormalModuleReplacementPlugin(
|
||||
/sqlite3-worker1-promiser\.mjs$/,
|
||||
dummyPath
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
// 4. Handle WASI imports for skiko.wasm (env, wasi_snapshot_preview1)
|
||||
// Webpack needs to know how to resolve these "magic" imports.
|
||||
// We can treat them as externals or empty modules.
|
||||
// Since we are in a browser environment, these are often provided by the runtime or polyfilled.
|
||||
// Mapping them to false tells Webpack to ignore them (empty module).
|
||||
|
||||
config.resolve = config.resolve || {};
|
||||
config.resolve.fallback = config.resolve.fallback || {};
|
||||
|
||||
// Fallbacks for Node.js core modules that might be required by libraries
|
||||
config.resolve.fallback.fs = false;
|
||||
config.resolve.fallback.path = false;
|
||||
config.resolve.fallback.crypto = false;
|
||||
|
||||
// Ignore WASI imports
|
||||
config.ignoreWarnings = config.ignoreWarnings || [];
|
||||
config.ignoreWarnings.push(/Critical dependency: the request of a dependency is an expression/);
|
||||
|
||||
// Use externals to handle WASI imports if fallback doesn't work
|
||||
config.externals = config.externals || {};
|
||||
// config.externals['env'] = 'env'; // This might expect a global 'env' variable
|
||||
// config.externals['wasi_snapshot_preview1'] = 'wasi_snapshot_preview1';
|
||||
|
||||
// Better approach for WASI in Webpack 5 with asyncWebAssembly:
|
||||
// Webpack should handle this if we don't interfere.
|
||||
// The error "Can't resolve 'env'" suggests it's looking for a module named 'env'.
|
||||
// We can provide a dummy module for these.
|
||||
|
||||
config.resolve.alias = config.resolve.alias || {};
|
||||
config.resolve.alias['env'] = dummyPath;
|
||||
config.resolve.alias['wasi_snapshot_preview1'] = dummyPath;
|
||||
@@ -3,6 +3,6 @@
|
||||
config.devServer = config.devServer || {};
|
||||
config.devServer.headers = {
|
||||
...config.devServer.headers,
|
||||
"Cross-Origin-Opener-Policy": "same-origin",
|
||||
"Cross-Origin-Embedder-Policy": "require-corp"
|
||||
"Cross-Origin-Opener-Policy": "same-origin",
|
||||
"Cross-Origin-Embedder-Policy": "require-corp"
|
||||
};
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
// Webpack configuration for SQLite WASM support AND Skiko fixes
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
config.resolve = config.resolve || {};
|
||||
config.resolve.fallback = config.resolve.fallback || {};
|
||||
config.resolve.alias = config.resolve.alias || {};
|
||||
|
||||
// 1. Fallbacks for Node.js core modules
|
||||
config.resolve.fallback.fs = false;
|
||||
config.resolve.fallback.path = false;
|
||||
config.resolve.fallback.crypto = false;
|
||||
|
||||
// 2. Resolve sqlite3 paths
|
||||
let sqliteBaseDir;
|
||||
try {
|
||||
const packagePath = path.dirname(require.resolve('@sqlite.org/sqlite-wasm/package.json'));
|
||||
sqliteBaseDir = path.join(packagePath, 'sqlite-wasm/jswasm');
|
||||
} catch (e) {
|
||||
console.warn("Could not resolve @sqlite.org/sqlite-wasm path automatically. Using fallback path.");
|
||||
sqliteBaseDir = path.resolve(__dirname, '../../../../../../node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm');
|
||||
}
|
||||
|
||||
// 3. Copy ALL sqlite3 assets (wasm, js, and auxiliary workers)
|
||||
if (fs.existsSync(sqliteBaseDir)) {
|
||||
console.log("Copying sqlite3 assets from:", sqliteBaseDir);
|
||||
config.plugins.push(
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: sqliteBaseDir,
|
||||
to: '.', // Copy to root of dist
|
||||
globOptions: {
|
||||
ignore: ['**/package.json'] // Don't copy package.json if present
|
||||
},
|
||||
noErrorOnMissing: true
|
||||
}
|
||||
]
|
||||
})
|
||||
);
|
||||
} else {
|
||||
console.error("ERROR: sqlite3 base directory does not exist:", sqliteBaseDir);
|
||||
}
|
||||
|
||||
// 4. Alias sqlite3.wasm (still needed for some internal checks maybe)
|
||||
const sqliteWasmPath = path.join(sqliteBaseDir, 'sqlite3.wasm');
|
||||
config.resolve.alias['sqlite3.wasm'] = sqliteWasmPath;
|
||||
config.resolve.alias['./sqlite3.wasm'] = sqliteWasmPath;
|
||||
|
||||
// 5. Handle .wasm files
|
||||
config.experiments = config.experiments || {};
|
||||
config.experiments.asyncWebAssembly = true;
|
||||
|
||||
config.module = config.module || {};
|
||||
config.module.rules = config.module.rules || [];
|
||||
|
||||
// Treat Skiko WASM as resource to avoid parsing errors
|
||||
config.module.rules.push({
|
||||
test: /skiko\.wasm$/,
|
||||
type: 'asset/resource'
|
||||
});
|
||||
|
||||
// Treat other WASM as async (default)
|
||||
config.module.rules.push({
|
||||
test: /\.wasm$/,
|
||||
exclude: /skiko\.wasm$/,
|
||||
type: 'webassembly/async'
|
||||
});
|
||||
|
||||
// 6. Ignore warnings
|
||||
config.ignoreWarnings = config.ignoreWarnings || [];
|
||||
config.ignoreWarnings.push(/Critical dependency: the request of a dependency is an expression/);
|
||||
|
||||
// 7. Fix for "webpackEmptyContext" in sqlite3.mjs
|
||||
config.plugins.push(
|
||||
new webpack.ContextReplacementPlugin(
|
||||
/@sqlite\.org\/sqlite-wasm/,
|
||||
(data) => {
|
||||
delete data.dependencies;
|
||||
return data;
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// 8. MIME types
|
||||
config.devServer = config.devServer || {};
|
||||
config.devServer.devMiddleware = config.devServer.devMiddleware || {};
|
||||
config.devServer.devMiddleware.mimeTypes = {
|
||||
'application/wasm': ['wasm'],
|
||||
'application/javascript': ['js']
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
// Fix für sql.js unter Webpack 5
|
||||
config.resolve = config.resolve || {};
|
||||
config.resolve.fallback = config.resolve.fallback || {};
|
||||
config.resolve.fallback.fs = false;
|
||||
config.resolve.fallback.path = false;
|
||||
config.resolve.fallback.crypto = false;
|
||||
config.resolve.fallback.os = false;
|
||||
config.resolve.fallback.stream = false;
|
||||
config.resolve.fallback.buffer = false;
|
||||
Reference in New Issue
Block a user