diff options
Diffstat (limited to 'server')
33 files changed, 3888 insertions, 0 deletions
diff --git a/server/aio/build.gradle.kts b/server/aio/build.gradle.kts new file mode 100644 index 0000000..22819f0 --- /dev/null +++ b/server/aio/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + kotlin("jvm") + application +} + +dependencies { + declareKtorVersion() + implementation(project(":server:core")) + implementation(project(":server:frontend")) +} + +java { + toolchain.languageVersion.set(JavaLanguageVersion.of(21)) +} + +application { + mainClass.set("moe.nea.ledger.server.core.ApplicationKt") +} + diff --git a/server/aio/src/main/kotlin/moe/nea/ledger/server/aio/AIO.kt b/server/aio/src/main/kotlin/moe/nea/ledger/server/aio/AIO.kt new file mode 100644 index 0000000..e59301f --- /dev/null +++ b/server/aio/src/main/kotlin/moe/nea/ledger/server/aio/AIO.kt @@ -0,0 +1,20 @@ +package moe.nea.ledger.server.aio + +import io.ktor.server.application.Application +import io.ktor.server.http.content.singlePageApplication +import io.ktor.server.routing.Routing +import moe.nea.ledger.server.core.AIOProvider + + +class AIO : AIOProvider { + override fun Routing.installExtraRouting() { + singlePageApplication { + useResources = true + filesPath = "ledger-web-dist" + defaultPage = "index.html" + } + } + + override fun Application.module() { + } +}
\ No newline at end of file diff --git a/server/analysis/build.gradle.kts b/server/analysis/build.gradle.kts new file mode 100644 index 0000000..d5d48a0 --- /dev/null +++ b/server/analysis/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + kotlin("jvm") + kotlin("plugin.serialization") + id("com.google.devtools.ksp") +} + +dependencies { + api("org.jetbrains.kotlinx:kotlinx-serialization-core:1.8.0") + ksp("dev.zacsweers.autoservice:auto-service-ksp:1.2.0") + implementation("com.google.auto.service:auto-service-annotations:1.1.1") + implementation(project(":database:impl")) +} + +java { + toolchain.languageVersion.set(JavaLanguageVersion.of(21)) +} diff --git a/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/Analysis.kt b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/Analysis.kt new file mode 100644 index 0000000..abcf8ed --- /dev/null +++ b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/Analysis.kt @@ -0,0 +1,9 @@ +package moe.nea.ledger.analysis + +import java.sql.Connection + +interface Analysis { + val id: String + val name: String + fun perform(database: Connection, filter: AnalysisFilter): AnalysisResult +}
\ No newline at end of file diff --git a/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/AnalysisFilter.kt b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/AnalysisFilter.kt new file mode 100644 index 0000000..10d9b9c --- /dev/null +++ b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/AnalysisFilter.kt @@ -0,0 +1,26 @@ +package moe.nea.ledger.analysis + +import moe.nea.ledger.database.DBLogEntry +import moe.nea.ledger.database.Query +import moe.nea.ledger.database.columns.DBUlid +import moe.nea.ledger.database.sql.Clause +import moe.nea.ledger.utils.ULIDWrapper +import java.time.Instant +import java.time.ZoneId +import java.util.UUID + +interface AnalysisFilter { + fun applyTo(query: Query) { + query.where(Clause { column(DBLogEntry.transactionId) ge value(DBUlid, ULIDWrapper.lowerBound(startWindow)) }) + .where(Clause { column(DBLogEntry.transactionId) le value(DBUlid, ULIDWrapper.upperBound(endWindow)) }) +//TODO: .where(Clause { column(DBLogEntry.profileId) inList profiles }) + } + + fun timeZone(): ZoneId { + return ZoneId.systemDefault() + } + + val startWindow: Instant + val endWindow: Instant + val profiles: List<UUID> +} diff --git a/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/AnalysisResult.kt b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/AnalysisResult.kt new file mode 100644 index 0000000..4ad47f7 --- /dev/null +++ b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/AnalysisResult.kt @@ -0,0 +1,8 @@ +package moe.nea.ledger.analysis + +import kotlinx.serialization.Serializable + +@Serializable +data class AnalysisResult( + val visualizations: List<Visualization> +)
\ No newline at end of file diff --git a/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/CoinsSpentOnAuctions.kt b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/CoinsSpentOnAuctions.kt new file mode 100644 index 0000000..d1ce52b --- /dev/null +++ b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/CoinsSpentOnAuctions.kt @@ -0,0 +1,49 @@ +package moe.nea.ledger.analysis + +import com.google.auto.service.AutoService +import moe.nea.ledger.ItemChange +import moe.nea.ledger.ItemId +import moe.nea.ledger.TransactionType +import moe.nea.ledger.database.DBItemEntry +import moe.nea.ledger.database.DBLogEntry +import moe.nea.ledger.database.sql.Clause +import java.sql.Connection +import java.time.LocalDate + +@AutoService(Analysis::class) +class CoinsSpentOnAuctions : Analysis { + override val name: String + get() = "Shopping Costs" + override val id: String + get() = "coins-spent-on-auctions" + + override fun perform(database: Connection, filter: AnalysisFilter): AnalysisResult { + val query = DBLogEntry.from(database) + .join(DBItemEntry, Clause { column(DBItemEntry.transactionId) eq column(DBLogEntry.transactionId) }) + .where(Clause { column(DBItemEntry.itemId) eq ItemId.COINS }) + .where(Clause { column(DBItemEntry.mode) eq ItemChange.ChangeDirection.LOST }) + .where(Clause { column(DBLogEntry.type) eq TransactionType.AUCTION_BOUGHT }) + .select(DBItemEntry.size, DBLogEntry.transactionId) + filter.applyTo(query) + val spentThatDay = mutableMapOf<LocalDate, Double>() + for (resultRow in query) { + val timestamp = resultRow[DBLogEntry.transactionId].getTimestamp() + val damage = resultRow[DBItemEntry.size] + val localZone = filter.timeZone() + val localDate = timestamp.atZone(localZone).toLocalDate() + spentThatDay.merge(localDate, damage) { a, b -> a + b } + } + return AnalysisResult( + listOf( + Visualization( + "Coins spent on auctions", + xLabel = "Time", + yLabel = "Coins Spent that day", + dataPoints = spentThatDay.entries.map { (k, v) -> + DataPoint(k.atTime(12, 0).atZone(filter.timeZone()).toInstant(), v) + } + ) + ) + ) + } +}
\ No newline at end of file diff --git a/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/DailyCashflow.kt b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/DailyCashflow.kt new file mode 100644 index 0000000..3dcb438 --- /dev/null +++ b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/DailyCashflow.kt @@ -0,0 +1,52 @@ +package moe.nea.ledger.analysis + +import com.google.auto.service.AutoService +import moe.nea.ledger.ItemChange +import moe.nea.ledger.ItemId +import moe.nea.ledger.TransactionType +import moe.nea.ledger.database.DBItemEntry +import moe.nea.ledger.database.DBLogEntry +import moe.nea.ledger.database.sql.Clause +import java.sql.Connection +import java.time.LocalDate +import kotlin.collections.component1 +import kotlin.collections.component2 + +@AutoService(Analysis::class) +class DailyCashflow : Analysis { + override val id: String + get() = "daily-cashflow" + override val name: String + get() = "Daily Cashflow" + + override fun perform( + database: Connection, + filter: AnalysisFilter + ): AnalysisResult { + val query = DBLogEntry.from(database) + .join(DBItemEntry, Clause { column(DBItemEntry.transactionId) eq column(DBLogEntry.transactionId) }) + .where(Clause { column(DBItemEntry.itemId) eq ItemId.COINS }) + .select(DBItemEntry.size, DBLogEntry.transactionId) + filter.applyTo(query) + val spentThatDay = mutableMapOf<LocalDate, Double>() + for (resultRow in query) { + val timestamp = resultRow[DBLogEntry.transactionId].getTimestamp() + val damage = resultRow[DBItemEntry.size] + val localZone = filter.timeZone() + val localDate = timestamp.atZone(localZone).toLocalDate() + spentThatDay.merge(localDate, damage) { a, b -> a + b } + } + return AnalysisResult( + listOf( + Visualization( + "Daily Cashflow", + xLabel = "Day", + yLabel = "Coins +/-", + dataPoints = spentThatDay.entries.map { (k, v) -> + DataPoint(k.atTime(12, 0).atZone(filter.timeZone()).toInstant(), v) + } + ) + ) + ) + } +}
\ No newline at end of file diff --git a/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/Visualization.kt b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/Visualization.kt new file mode 100644 index 0000000..d0c0d56 --- /dev/null +++ b/server/analysis/src/main/kotlin/moe/nea/ledger/analysis/Visualization.kt @@ -0,0 +1,30 @@ +package moe.nea.ledger.analysis + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import java.time.Instant + +@Serializable +data class Visualization( + val label: String, + val xLabel: String, + val yLabel: String, + val dataPoints: List<DataPoint> +) + +@Serializable +data class DataPoint( + val time: @Serializable(InstantSerializer::class) Instant, + val value: Double, +) + +object InstantSerializer : KSerializer<Instant> { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("java.time.Instant", PrimitiveKind.LONG) + override fun serialize(encoder: Encoder, value: Instant) = encoder.encodeLong(value.toEpochMilli()) + override fun deserialize(decoder: Decoder): Instant = Instant.ofEpochMilli(decoder.decodeLong()) +}
\ No newline at end of file diff --git a/server/core/build.gradle.kts b/server/core/build.gradle.kts new file mode 100644 index 0000000..b2a3222 --- /dev/null +++ b/server/core/build.gradle.kts @@ -0,0 +1,37 @@ +plugins { + kotlin("jvm") + kotlin("plugin.serialization") + application + id("com.github.gmazzo.buildconfig") +} + + +dependencies { + declareKtorVersion() + api("io.ktor:ktor-server-netty") + api("io.ktor:ktor-server-status-pages") + api("io.ktor:ktor-server-content-negotiation") + api("io.ktor:ktor-serialization-kotlinx-json") + api("io.ktor:ktor-server-compression") + api("io.ktor:ktor-server-cors") + api("sh.ondr:kotlin-json-schema:0.1.1") + api(project(":server:analysis")) + api(project(":database:impl")) + api(project(":server:swagger")) + + runtimeOnly("ch.qos.logback:logback-classic:1.5.16") + runtimeOnly("org.xerial:sqlite-jdbc:3.45.3.0") +} + +java { + toolchain.languageVersion.set(JavaLanguageVersion.of(21)) +} +application { + val isDevelopment: Boolean = project.ext.has("development") + applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment", + "-Dledger.databasefolder=${project(":mod").file("run/money-ledger").absoluteFile}") + mainClass.set("moe.nea.ledger.server.core.ApplicationKt") +} +buildConfig { + packageName("moe.nea.ledger.gen") +} diff --git a/server/core/src/main/kotlin/moe/nea/ledger/server/core/Application.kt b/server/core/src/main/kotlin/moe/nea/ledger/server/core/Application.kt new file mode 100644 index 0000000..23b2a6a --- /dev/null +++ b/server/core/src/main/kotlin/moe/nea/ledger/server/core/Application.kt @@ -0,0 +1,81 @@ +package moe.nea.ledger.server.core + +import io.ktor.serialization.kotlinx.json.json +import io.ktor.server.application.Application +import io.ktor.server.application.install +import io.ktor.server.netty.EngineMain +import io.ktor.server.plugins.compression.Compression +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation +import io.ktor.server.plugins.cors.routing.CORS +import io.ktor.server.response.respondRedirect +import io.ktor.server.routing.Routing +import io.ktor.server.routing.get +import io.ktor.server.routing.route +import io.ktor.server.routing.routing +import kotlinx.serialization.json.Json +import moe.nea.ledger.database.Database +import moe.nea.ledger.gen.BuildConfig +import moe.nea.ledger.server.core.api.Documentation +import moe.nea.ledger.server.core.api.Info +import moe.nea.ledger.server.core.api.Server +import moe.nea.ledger.server.core.api.apiRouting +import moe.nea.ledger.server.core.api.openApiDocsJson +import moe.nea.ledger.server.core.api.openApiUi +import moe.nea.ledger.server.core.api.setApiRoot +import java.io.File + +fun main(args: Array<String>) { + EngineMain.main(args) +} + +interface AIOProvider { + fun Routing.installExtraRouting() + fun Application.module() +} + +fun Application.module() { + val aio = runCatching { + Class.forName("moe.nea.ledger.server.aio.AIO") + .newInstance() as AIOProvider + }.getOrNull() + aio?.run { module() } + install(Compression) + install(Documentation) { + info = Info( + "Ledger Analysis Server", + "Your local API for loading ledger data", + BuildConfig.VERSION + ) + servers.add( + Server("http://localhost:8080/api", "Your Local Server") + ) + } + install(ContentNegotiation) { + json(Json { + this.explicitNulls = false + this.encodeDefaults = true + }) +// cbor() + } + install(CORS) { + anyHost() + } + val database = Database(File(System.getProperty("ledger.databasefolder", + "/home/nea/.local/share/PrismLauncher/instances/Skyblock/.minecraft/money-ledger"))) + database.loadAndUpgrade() + routing { + route("/api") { + setApiRoot() + get { call.respondRedirect("/openapi/") } + apiRouting(database) + } + route("/api.json") { + openApiDocsJson() + } + route("/openapi") { + openApiUi("/api.json") + } + aio?.run { installExtraRouting() } + } +} + diff --git a/server/core/src/main/kotlin/moe/nea/ledger/server/core/api/BaseApi.kt b/server/core/src/main/kotlin/moe/nea/ledger/server/core/api/BaseApi.kt new file mode 100644 index 0000000..3240a65 --- /dev/null +++ b/server/core/src/main/kotlin/moe/nea/ledger/server/core/api/BaseApi.kt @@ -0,0 +1,205 @@ +package moe.nea.ledger.server.core.api + +import io.ktor.http.Url +import io.ktor.http.toURI +import io.ktor.server.response.respond +import io.ktor.server.routing.Route +import io.ktor.server.routing.get +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async +import kotlinx.coroutines.withContext +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import moe.nea.ledger.ItemChange +import moe.nea.ledger.TransactionType +import moe.nea.ledger.analysis.Analysis +import moe.nea.ledger.analysis.AnalysisFilter +import moe.nea.ledger.analysis.AnalysisResult +import moe.nea.ledger.database.DBItemEntry +import moe.nea.ledger.database.DBLogEntry +import moe.nea.ledger.database.Database +import moe.nea.ledger.database.sql.Clause +import moe.nea.ledger.server.core.Profile +import moe.nea.ledger.utils.ULIDWrapper +import java.time.Instant +import java.util.ServiceLoader +import java.util.UUID + +fun Route.apiRouting(database: Database) { + val allOfferedAnalysisServices: Map<String, Analysis> = run { + val serviceLoader = ServiceLoader.load(Analysis::class.java, environment.classLoader) + val map = mutableMapOf<String, Analysis>() + serviceLoader.forEach { + map[it.id] = it + } + map + } + + get("/profiles") { + val profiles = DBLogEntry.from(database.connection) + .select(DBLogEntry.playerId, DBLogEntry.profileId) + .distinct() + .map { + Profile(it[DBLogEntry.playerId], it[DBLogEntry.profileId]) + } + call.respond(profiles) + }.docs { + summary = "List all profiles and players known to ledger" + operationId = "listProfiles" + tag(Tags.PROFILE) + respondsOk { + schema<List<Profile>>() + } + } + @OptIn(DelicateCoroutinesApi::class) + val itemNames = GlobalScope.async { + val itemNamesUrl = + Url("https://github.com/nea89o/ledger-auxiliary-data/raw/refs/heads/master/data/item_names.json") + Json.decodeFromStream<Map<String, String>>(itemNamesUrl.toURI().toURL().openStream()) + } + get("/analysis/execute") { + val analysis = allOfferedAnalysisServices[call.queryParameters["analysis"]] ?: TODO() + val start = call.queryParameters["tStart"]?.toLongOrNull()?.let { Instant.ofEpochMilli(it) } ?: TODO() + val end = call.queryParameters["tEnd"]?.toLongOrNull()?.let { Instant.ofEpochMilli(it) } ?: TODO() + val analysisResult = withContext(Dispatchers.IO) { + analysis.perform( + database.connection, + object : AnalysisFilter { + override val startWindow: Instant + get() = start + override val endWindow: Instant + get() = end + override val profiles: List<UUID> + get() = listOf() + } + ) + } + call.respond(analysisResult) + }.docs { + summary = "Execute an analysis on a given timeframe" + operationId = "executeAnalysis" + queryParameter<String>("analysis", description = "An analysis id obtained from getAnalysis") + queryParameter<Long>("tStart", description = "The start of the timeframe to analyze") + queryParameter<Long>("tEnd", + description = "The end of the timeframe to analyze. Make sure to use the end of the day if you want the entire day included.") + tag(Tags.DATA) + respondsOk { + schema<AnalysisResult>() + } + } + get("/analysis/list") { + call.respond(allOfferedAnalysisServices.values.map { + AnalysisListing(it.name, it.id) + }) + }.docs { + summary = "List all installed analysis" + operationId = "getAnalysis" + tag(Tags.DATA) + respondsOk { + schema<List<AnalysisListing>>() + } + } + get("/item") { + val itemIds = call.queryParameters.getAll("itemId")?.toSet() ?: emptySet() + val itemNameMap = itemNames.await() + call.respond(itemIds.associateWith { itemNameMap[it] }) + }.docs { + summary = "Get item names for item ids" + operationId = "getItemNames" + tag(Tags.HYPIXEL) + queryParameter<List<String>>("itemId") + respondsOk { + schema<Map<String, String?>>() + } + } + get("/entries") { + val logs = mutableMapOf<ULIDWrapper, LogEntry>() + val items = mutableMapOf<ULIDWrapper, MutableList<SerializableItemChange>>() + withContext(Dispatchers.IO) { + DBLogEntry.from(database.connection) + .join(DBItemEntry, Clause { column(DBItemEntry.transactionId) eq column(DBLogEntry.transactionId) }) + .select(DBLogEntry.profileId, + DBLogEntry.playerId, + DBLogEntry.transactionId, + DBLogEntry.type, + DBItemEntry.mode, + DBItemEntry.itemId, + DBItemEntry.size) + .forEach { row -> + logs.getOrPut(row[DBLogEntry.transactionId]) { + LogEntry(row[DBLogEntry.type], + row[DBLogEntry.transactionId], + listOf()) + } + items.getOrPut(row[DBLogEntry.transactionId]) { mutableListOf() } + .add(SerializableItemChange( + row[DBItemEntry.itemId].string, + row[DBItemEntry.mode], + row[DBItemEntry.size], + )) + } + } + val compiled = logs.values.map { it.copy(items = items[it.id]!!) } + call.respond(compiled) + }.docs { + summary = "Get all log entries" + operationId = "getLogEntries" + tag(Tags.DATA) + respondsOk { + schema<List<LogEntry>>() + } + } +} + +@Serializable +data class AnalysisListing( + val name: String, + val id: String, +) + +@Serializable +data class LogEntry( + val type: TransactionType, + val id: @Serializable(ULIDSerializer::class) ULIDWrapper, + val items: List<SerializableItemChange>, +) + +@Serializable +data class SerializableItemChange( + val itemId: String, + val direction: ItemChange.ChangeDirection, + val amount: Double, +) + +object ULIDSerializer : KSerializer<ULIDWrapper> { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ULID", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): ULIDWrapper { + return ULIDWrapper(decoder.decodeString()) + } + + override fun serialize(encoder: Encoder, value: ULIDWrapper) { + encoder.encodeString(value.wrapped) + } +} + +enum class Tags : IntoTag { + PROFILE, + HYPIXEL, + MANAGEMENT, + DATA, + ; + + override fun intoTag(): String { + return name + } +}
\ No newline at end of file diff --git a/server/core/src/main/kotlin/moe/nea/ledger/server/core/model.kt b/server/core/src/main/kotlin/moe/nea/ledger/server/core/model.kt new file mode 100644 index 0000000..a27a729 --- /dev/null +++ b/server/core/src/main/kotlin/moe/nea/ledger/server/core/model.kt @@ -0,0 +1,30 @@ +@file:UseSerializers(UUIDSerializer::class) + +package moe.nea.ledger.server.core + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import java.util.UUID + +object UUIDSerializer : KSerializer<UUID> { + override val descriptor: SerialDescriptor + get() = PrimitiveSerialDescriptor("LedgerUUID", PrimitiveKind.STRING) + + override fun deserialize(decoder: kotlinx.serialization.encoding.Decoder): UUID { + return UUID.fromString(decoder.decodeString()) + } + + override fun serialize(encoder: kotlinx.serialization.encoding.Encoder, value: UUID) { + encoder.encodeString(value.toString()) + } +} + +@Serializable +data class Profile( + val playerId: UUID, + val profileId: UUID, +)
\ No newline at end of file diff --git a/server/core/src/main/resources/application.conf b/server/core/src/main/resources/application.conf new file mode 100644 index 0000000..386ffd3 --- /dev/null +++ b/server/core/src/main/resources/application.conf @@ -0,0 +1,10 @@ +ktor { + application { + modules = [ + moe.nea.ledger.server.core.ApplicationKt.module + ] + } + deployment { + port = 8080 + } +}
\ No newline at end of file diff --git a/server/frontend/.gitignore b/server/frontend/.gitignore new file mode 100644 index 0000000..76add87 --- /dev/null +++ b/server/frontend/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist
\ No newline at end of file diff --git a/server/frontend/build.gradle.kts b/server/frontend/build.gradle.kts new file mode 100644 index 0000000..72fe6ea --- /dev/null +++ b/server/frontend/build.gradle.kts @@ -0,0 +1,17 @@ +import com.github.gradle.node.pnpm.task.PnpmTask + +plugins { + id("com.github.node-gradle.node") version "7.1.0" + `java-library` +} + +val webDist by tasks.register("webDist", PnpmTask::class) { + dependsOn(tasks.pnpmInstall) + args.addAll("build") + outputs.dir("dist") +} +tasks.jar { + from(webDist) { + into("ledger-web-dist/") + } +} diff --git a/server/frontend/index.html b/server/frontend/index.html new file mode 100644 index 0000000..afc19c2 --- /dev/null +++ b/server/frontend/index.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <meta name="theme-color" content="#000000" /> + <link rel="shortcut icon" type="image/ico" href="/src/assets/favicon.ico" /> + <title>Solid App</title> + </head> + <body> + <noscript>You need to enable JavaScript to run this app.</noscript> + <div id="root" class="min-h-[100vh]"></div> + + <script src="/src/index.tsx" type="module"></script> + </body> +</html> diff --git a/server/frontend/package.json b/server/frontend/package.json new file mode 100644 index 0000000..e344700 --- /dev/null +++ b/server/frontend/package.json @@ -0,0 +1,40 @@ +{ + "name": "ledger-frontend", + "version": "0.0.0", + "description": "", + "type": "module", + "scripts": { + "start": "vite", + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "test:ts": "tsc --noEmit", + "genApi": "openapi-typescript http://localhost:8080/api.json -o src/api-schema.d.ts" + }, + "license": "MIT", + "devDependencies": { + "openapi-typescript": "^7.5.2", + "solid-devtools": "^0.33.0", + "typescript": "^5.7.2", + "vite": "^6.0.0", + "vite-plugin-solid": "^2.11.0" + }, + "dependencies": { + "@solidjs/router": "^0.15.3", + "@tailwindcss/vite": "^4.1.5", + "apexcharts": "^4.3.0", + "chartist": "^1.3.1", + "moment": "^2.30.1", + "openapi-fetch": "^0.13.4", + "solid-apexcharts": "^0.4.0", + "solid-js": "^1.9.3", + "tailwindcss": "^4.1.5" + }, + "devEngines": { + "packageManager": { + "name": "pnpm", + "onFail": "error" + } + }, + "packageManager": "pnpm@^9.3.0" +} diff --git a/server/frontend/pnpm-lock.yaml b/server/frontend/pnpm-lock.yaml new file mode 100644 index 0000000..d4b6e96 --- /dev/null +++ b/server/frontend/pnpm-lock.yaml @@ -0,0 +1,2238 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@solidjs/router': + specifier: ^0.15.3 + version: 0.15.3(solid-js@1.9.4) + '@tailwindcss/vite': + specifier: ^4.1.5 + version: 4.1.5(vite@6.0.7(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.83.4)) + apexcharts: + specifier: ^4.3.0 + version: 4.3.0 + chartist: + specifier: ^1.3.1 |
