diff options
87 files changed, 2386 insertions, 175 deletions
diff --git a/basetypes/src/main/kotlin/moe/nea/ledger/TransactionType.kt b/basetypes/src/main/kotlin/moe/nea/ledger/TransactionType.kt index a3910a0..d4c15e5 100644 --- a/basetypes/src/main/kotlin/moe/nea/ledger/TransactionType.kt +++ b/basetypes/src/main/kotlin/moe/nea/ledger/TransactionType.kt @@ -8,13 +8,16 @@ enum class TransactionType { AUCTION_SOLD, AUTOMERCHANT_PROFIT_COLLECT, BANK_DEPOSIT, + BANK_INTEREST, BANK_WITHDRAW, + BASIC_REFORGE, BAZAAR_BUY_INSTANT, BAZAAR_BUY_ORDER, BAZAAR_SELL_INSTANT, BAZAAR_SELL_ORDER, BITS_PURSE_STATUS, BOOSTER_COOKIE_ATE, + CADUCOUS_FEEDER_USED, CAPSAICIN_EYEDROPS_USED, COMMUNITY_SHOP_BUY, CORPSE_DESECRATED, @@ -22,6 +25,7 @@ enum class TransactionType { DRACONIC_SACRIFICE, DUNGEON_CHEST_OPEN, FORGED, + GHOST_COIN_DROP, GOD_POTION_DRANK, GOD_POTION_MIXIN_DRANK, GUMMY_POLAR_BEAR_ATE, @@ -32,6 +36,7 @@ enum class TransactionType { NPC_BUY, NPC_SELL, PEST_REPELLENT_USED, + STONKS_AUCTION, VISITOR_BARGAIN, WYRM_EVOKED, }
\ No newline at end of file diff --git a/basetypes/src/main/kotlin/moe/nea/ledger/utils/ULIDWrapper.kt b/basetypes/src/main/kotlin/moe/nea/ledger/utils/ULIDWrapper.kt index b8c5d3b..29d5e31 100644 --- a/basetypes/src/main/kotlin/moe/nea/ledger/utils/ULIDWrapper.kt +++ b/basetypes/src/main/kotlin/moe/nea/ledger/utils/ULIDWrapper.kt @@ -9,6 +9,14 @@ value class ULIDWrapper( val wrapped: String ) { companion object { + fun lowerBound(timestamp: Instant): ULIDWrapper { + return ULIDWrapper(ULID.generate(timestamp.toEpochMilli(), ByteArray(10))) + } + + fun upperBound(timestamp: Instant): ULIDWrapper { + return ULIDWrapper(ULID.generate(timestamp.toEpochMilli(), ByteArray(10) { -1 })) + } + fun createULIDAt(timestamp: Instant): ULIDWrapper { return ULIDWrapper(ULID.generate( timestamp.toEpochMilli(), diff --git a/build.gradle.kts b/build.gradle.kts index 8377205..4c6ee45 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,11 +2,12 @@ import com.github.gmazzo.buildconfig.BuildConfigExtension import java.io.ByteArrayOutputStream plugins { - val kotlinVersion = "2.0.20" + val kotlinVersion = "2.0.21" kotlin("jvm") version kotlinVersion apply false kotlin("plugin.serialization") version kotlinVersion apply false id("com.github.gmazzo.buildconfig") version "5.5.0" apply false id("ledger-globals") + id("com.google.devtools.ksp") version "2.0.21-1.0.26" apply false id("com.github.johnrengelman.shadow") version "8.1.1" apply false } diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/Column.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/Column.kt index 33727de..c21a159 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/Column.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/Column.kt @@ -4,20 +4,20 @@ import moe.nea.ledger.database.sql.IntoSelectable import moe.nea.ledger.database.sql.Selectable import java.sql.PreparedStatement -class Column<T> @Deprecated("Use Table.column instead") constructor( +class Column<T, Raw> @Deprecated("Use Table.column instead") constructor( val table: Table, val name: String, - val type: DBType<T> + val type: DBType<T, Raw> ) : IntoSelectable<T> { - override fun asSelectable() = object : Selectable<T> { + override fun asSelectable() = object : Selectable<T, Raw> { override fun asSql(): String { return qualifiedSqlName } - override val dbType: DBType<T> + override val dbType: DBType<T, Raw> get() = this@Column.type - override fun guessColumn(): Column<T>? { + override fun guessColumn(): Column<T, Raw> { return this@Column } diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/Constraint.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/Constraint.kt index 9f7c9ef..729c6b8 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/Constraint.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/Constraint.kt @@ -1,6 +1,6 @@ package moe.nea.ledger.database interface Constraint { - val affectedColumns: Collection<Column<*>> + val affectedColumns: Collection<Column<*, *>> fun asSQL(): String }
\ No newline at end of file diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/DBType.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/DBType.kt index 86ff544..622aff3 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/DBType.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/DBType.kt @@ -1,9 +1,19 @@ package moe.nea.ledger.database +import moe.nea.ledger.database.sql.ClauseBuilder import java.sql.PreparedStatement import java.sql.ResultSet -interface DBType<T> { + +interface DBType< + /** + * Mapped type of this db type. Represents the Java type this db type accepts for saving to the database. + */ + T, + /** + * Phantom marker type representing how this db type is presented to the actual DB. Is used by APIs such as [ClauseBuilder] to allow for rough typechecking. + */ + RawType> { val dbType: String fun get(result: ResultSet, index: Int): T @@ -12,8 +22,8 @@ interface DBType<T> { fun <R> mapped( from: (R) -> T, to: (T) -> R, - ): DBType<R> { - return object : DBType<R> { + ): DBType<R, RawType> { + return object : DBType<R, RawType> { override fun getName(): String { return "Mapped(${this@DBType.getName()})" } diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/InsertStatement.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/InsertStatement.kt index 7871ba8..25bef22 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/InsertStatement.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/InsertStatement.kt @@ -1,7 +1,7 @@ package moe.nea.ledger.database -class InsertStatement(val properties: MutableMap<Column<*>, Any>) { - operator fun <T : Any> set(key: Column<T>, value: T) { +class InsertStatement(val properties: MutableMap<Column<*, *>, Any>) { + operator fun <T : Any> set(key: Column<T, *>, value: T) { properties[key] = value } }
\ No newline at end of file diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/Query.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/Query.kt index e58eef4..a23c878 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/Query.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/Query.kt @@ -12,7 +12,7 @@ import java.sql.Connection class Query( val connection: Connection, - val selectedColumns: MutableList<Selectable<*>>, + val selectedColumns: MutableList<Selectable<*, *>>, var table: Table, var limit: UInt? = null, var skip: UInt? = null, diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/ResultRow.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/ResultRow.kt index 6715f27..7b57abd 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/ResultRow.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/ResultRow.kt @@ -2,19 +2,19 @@ package moe.nea.ledger.database import moe.nea.ledger.database.sql.Selectable -class ResultRow(val selectableValues: Map<Selectable<*>, *>) { +class ResultRow(val selectableValues: Map<Selectable<*, *>, *>) { val columnValues = selectableValues.mapNotNull { val col = it.key.guessColumn() ?: return@mapNotNull null col to it.value }.toMap() - operator fun <T> get(column: Column<T>): T { + operator fun <T> get(column: Column<T, *>): T { val value = columnValues[column] ?: error("Invalid column ${column.name}. Only ${columnValues.keys.joinToString { it.name }} are available.") return value as T } - operator fun <T> get(column: Selectable<T>): T { + operator fun <T> get(column: Selectable<T, *>): T { val value = selectableValues[column] ?: error("Invalid selectable ${column}. Only ${selectableValues.keys} are available.") return value as T diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/Table.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/Table.kt index 61dc8f0..a462813 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/Table.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/Table.kt @@ -4,15 +4,15 @@ import java.sql.Connection abstract class Table(val name: String) { val sqlName get() = "`$name`" - protected val _mutable_columns: MutableList<Column<*>> = mutableListOf() + protected val _mutable_columns: MutableList<Column<*, *>> = mutableListOf() protected val _mutable_constraints: MutableList<Constraint> = mutableListOf() - val columns: List<Column<*>> get() = _mutable_columns + val columns: List<Column<*, *>> get() = _mutable_columns val constraints get() = _mutable_constraints - protected fun unique(vararg columns: Column<*>) { + protected fun unique(vararg columns: Column<*, *>) { _mutable_constraints.add(UniqueConstraint(columns.toList())) } - protected fun <T> column(name: String, type: DBType<T>): Column<T> { + protected fun <T, R> column(name: String, type: DBType<T, R>): Column<T, R> { @Suppress("DEPRECATION") val column = Column(this, name, type) _mutable_columns.add(column) return column @@ -39,7 +39,7 @@ abstract class Table(val name: String) { fun createIfNotExists( connection: Connection, - filteredColumns: List<Column<*>> = columns + filteredColumns: List<Column<*, *>> = columns ) { val properties = mutableListOf<String>() for (column in filteredColumns) { @@ -57,7 +57,7 @@ abstract class Table(val name: String) { fun alterTableAddColumns( connection: Connection, - newColumns: List<Column<*>> + newColumns: List<Column<*, *>> ) { for (column in newColumns) { connection.prepareAndLog("ALTER TABLE $sqlName ADD ${column.sqlName} ${column.type.dbType}") @@ -88,7 +88,7 @@ abstract class Table(val name: String) { val statement = connection.prepareAndLog("INSERT OR ${onConflict.asSql()} INTO $sqlName ($columnNames) VALUES ($valueNames)") for ((index, column) in columns.withIndex()) { - (column as Column<Any>).type.set(statement, index + 1, insert.properties[column]!!) + (column as Column<Any, *>).type.set(statement, index + 1, insert.properties[column]!!) } statement.execute() } diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/UniqueConstraint.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/UniqueConstraint.kt index 32e9f79..31ef06c 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/UniqueConstraint.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/UniqueConstraint.kt @@ -1,11 +1,11 @@ package moe.nea.ledger.database -class UniqueConstraint(val columns: List<Column<*>>) : Constraint { +class UniqueConstraint(val columns: List<Column<*, *>>) : Constraint { init { require(columns.isNotEmpty()) } - override val affectedColumns: Collection<Column<*>> + override val affectedColumns: Collection<Column<*, *>> get() = columns override fun asSQL(): String { diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBDouble.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBDouble.kt index 6828308..9840df2 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBDouble.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBDouble.kt @@ -4,7 +4,7 @@ import moe.nea.ledger.database.DBType import java.sql.PreparedStatement import java.sql.ResultSet -object DBDouble : DBType<Double> { +object DBDouble : DBType<Double, Double> { override val dbType: String get() = "DOUBLE" diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBEnum.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBEnum.kt index 3cce8bc..78ac578 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBEnum.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBEnum.kt @@ -6,7 +6,7 @@ import java.sql.ResultSet class DBEnum<T : Enum<T>>( val type: Class<T>, -) : DBType<T> { +) : DBType<T, String> { companion object { inline operator fun <reified T : Enum<T>> invoke(): DBEnum<T> { return DBEnum(T::class.java) diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBInstant.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBInstant.kt index a9f36ef..2cf1882 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBInstant.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBInstant.kt @@ -5,7 +5,7 @@ import java.sql.PreparedStatement import java.sql.ResultSet import java.time.Instant -object DBInstant : DBType<Instant> { +object DBInstant : DBType<Instant, Long> { override val dbType: String get() = "INTEGER" diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBInt.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBInt.kt index 9cf2aa0..b5406e1 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBInt.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBInt.kt @@ -4,7 +4,7 @@ import moe.nea.ledger.database.DBType import java.sql.PreparedStatement import java.sql.ResultSet -object DBInt : DBType<Long> { +object DBInt : DBType<Long, Long> { override val dbType: String get() = "INTEGER" diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBString.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBString.kt index 3994b7d..4406627 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBString.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBString.kt @@ -4,7 +4,7 @@ import moe.nea.ledger.database.DBType import java.sql.PreparedStatement import java.sql.ResultSet -object DBString : DBType<String> { +object DBString : DBType<String, String> { override val dbType: String get() = "TEXT" diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBUlid.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBUlid.kt index 33db65f..1fcc9d8 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBUlid.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBUlid.kt @@ -5,7 +5,7 @@ import moe.nea.ledger.database.DBType import java.sql.PreparedStatement import java.sql.ResultSet -object DBUlid : DBType<ULIDWrapper> { +object DBUlid : DBType<ULIDWrapper, String> { override val dbType: String get() = "TEXT" diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBUuid.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBUuid.kt index ede385f..eaea440 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBUuid.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/columns/DBUuid.kt @@ -6,7 +6,7 @@ import java.sql.PreparedStatement import java.sql.ResultSet import java.util.UUID -object DBUuid : DBType<UUID> { +object DBUuid : DBType<UUID, String> { override val dbType: String get() = "TEXT" diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ANDExpression.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ANDExpression.kt index 6ea1b64..43d5a53 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ANDExpression.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ANDExpression.kt @@ -10,7 +10,10 @@ data class ANDExpression( } override fun asSql(): String { - return (elements + SQLQueryComponent.standalone("TRUE")).joinToString(" AND ", "(", ")") { it.asSql() } + elements.singleOrNull()?.let { + return "(" + it.asSql() + ")" + } + return elements.joinToString(" AND ", "(", ")") { it.asSql() } } override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int { diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/Clause.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/Clause.kt index 2921d80..205e566 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/Clause.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/Clause.kt @@ -1,10 +1,12 @@ package moe.nea.ledger.database.sql +/** + * Directly constructing [clauses][Clause] is discouraged. Instead [Clause.invoke] should be used. + */ interface Clause : BooleanExpression { companion object { - operator fun invoke(builder: ClauseBuilder.() -> Clause): Clause { + operator fun <T> invoke(builder: ClauseBuilder.() -> T): T { return builder(ClauseBuilder()) } } - }
\ No newline at end of file diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ClauseBuilder.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ClauseBuilder.kt index 2b141f0..cb0ddfc 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ClauseBuilder.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ClauseBuilder.kt @@ -1,11 +1,25 @@ package moe.nea.ledger.database.sql import moe.nea.ledger.database.Column +import moe.nea.ledger.database.DBType class ClauseBuilder { - fun <T> column(column: Column<T>): Operand<T> = ColumnOperand(column) + // TODO: should we match on T AND R? maybe allow explicit upcasting + fun <T, R> column(column: Column<T, R>): ColumnOperand<T, R> = ColumnOperand(column) fun string(string: String): StringOperand = StringOperand(string) - infix fun Operand<*>.eq(operand: Operand<*>) = EqualsClause(this, operand) - infix fun Operand<*>.like(op: StringOperand) = LikeClause(this, op) - infix fun Operand<*>.like(op: String) = LikeClause(this, string(op)) + fun <T, R> value(dbType: DBType<T, R>, value: T): Operand<T, R> = ValuedOperand(dbType, value) + infix fun <T> Operand<*, T>.eq(operand: Operand<*, T>): Clause = EqualsClause(this, operand) + infix fun <T, R> TypedOperand<T, R>.eq(value: T): Clause = EqualsClause(this, value(dbType, value)) + infix fun Operand<*, String>.like(op: StringOperand): Clause = LikeClause(this, op) + infix fun Operand<*, String>.like(op: String): Clause = LikeClause(this, string(op)) + infix fun <T> Operand<*, T>.lt(op: Operand<*, T>): BooleanExpression = LessThanExpression(this, op) + infix fun <T> Operand<*, T>.le(op: Operand<*, T>): BooleanExpression = LessThanEqualsExpression(this, op) + infix fun <T> Operand<*, T>.gt(op: Operand<*, T>): BooleanExpression = op lt this + infix fun <T> Operand<*, T>.ge(op: Operand<*, T>): BooleanExpression = op le this + infix fun <T> Operand<*, T>.inList(list: ListExpression<*, T>): Clause = ListClause(this, list) + infix fun <T, R> TypedOperand<T, R>.inList(list: List<T>): Clause = this inList list(dbType, list) + fun <T, R> list(dbType: DBType<T, R>, vararg values: T): ListExpression<T, R> = list(dbType, values.toList()) + fun <T, R> list(dbType: DBType<T, R>, values: List<T>): ListExpression<T, R> = ListExpression(values, dbType) + infix fun BooleanExpression.and(clause: BooleanExpression): BooleanExpression = ANDExpression(listOf(this, clause)) + infix fun BooleanExpression.or(clause: BooleanExpression): BooleanExpression = ORExpression(listOf(this, clause)) }
\ No newline at end of file diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ColumnOperand.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ColumnOperand.kt index 9150aa7..430d592 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ColumnOperand.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ColumnOperand.kt @@ -1,9 +1,13 @@ package moe.nea.ledger.database.sql import moe.nea.ledger.database.Column +import moe.nea.ledger.database.DBType import java.sql.PreparedStatement -data class ColumnOperand<T>(val column: Column<T>) : Operand<T> { +data class ColumnOperand<T, Raw>(val column: Column<T, Raw>) : TypedOperand<T, Raw> { + override val dbType: DBType<T, Raw> + get() = column.type + override fun asSql(): String { return column.qualifiedSqlName } diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/EqualsClause.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/EqualsClause.kt index c6c482a..cfe72ef 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/EqualsClause.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/EqualsClause.kt @@ -2,7 +2,7 @@ package moe.nea.ledger.database.sql import java.sql.PreparedStatement -data class EqualsClause(val left: Operand<*>, val right: Operand<*>) : Clause { // TODO: typecheck this somehow +data class EqualsClause(val left: Operand<*, *>, val right: Operand<*, *>) : Clause { override fun asSql(): String { return left.asSql() + " = " + right.asSql() } diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/IntoSelectable.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/IntoSelectable.kt index 0068f6b..3775387 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/IntoSelectable.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/IntoSelectable.kt @@ -1,5 +1,5 @@ package moe.nea.ledger.database.sql interface IntoSelectable<T> { - fun asSelectable(): Selectable<T> + fun asSelectable(): Selectable<T, *> }
\ No newline at end of file diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/LessThanEqualsExpression.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/LessThanEqualsExpression.kt new file mode 100644 index 0000000..4820c97 --- /dev/null +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/LessThanEqualsExpression.kt @@ -0,0 +1,15 @@ +package moe.nea.ledger.database.sql + +import java.sql.PreparedStatement + +class LessThanEqualsExpression(val lhs: Operand<*, *>, val rhs: Operand<*, *>) : + BooleanExpression { + override fun asSql(): String { + return "${lhs.asSql()} <= ${rhs.asSql()}" + } + + override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int { + val next = lhs.appendToStatement(stmt, startIndex) + return rhs.appendToStatement(stmt, next) + } +} diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/LessThanExpression.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/LessThanExpression.kt new file mode 100644 index 0000000..4609ac1 --- /dev/null +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/LessThanExpression.kt @@ -0,0 +1,15 @@ +package moe.nea.ledger.database.sql + +import java.sql.PreparedStatement + +class LessThanExpression(val lhs: Operand<*, *>, val rhs: Operand<*, *>) : + BooleanExpression { + override fun asSql(): String { + return "${lhs.asSql()} < ${rhs.asSql()}" + } + + override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int { + val next = lhs.appendToStatement(stmt, startIndex) + return rhs.appendToStatement(stmt, next) + } +} diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/LikeClause.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/LikeClause.kt index f84c5cc..1122329 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/LikeClause.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/LikeClause.kt @@ -2,7 +2,7 @@ package moe.nea.ledger.database.sql import java.sql.PreparedStatement -data class LikeClause<T>(val left: Operand<T>, val right: StringOperand) : Clause { +data class LikeClause<T>(val left: Operand<T, String>, val right: StringOperand) : Clause { //TODO: check type safety with this one override fun asSql(): String { return "(" + left.asSql() + " LIKE " + right.asSql() + ")" diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ListClause.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ListClause.kt new file mode 100644 index 0000000..d240472 --- /dev/null +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ListClause.kt @@ -0,0 +1,8 @@ +package moe.nea.ledger.database.sql + +class ListClause<R>( + val lhs: Operand<*, R>, + val list: ListExpression<*, R>, +) : Clause, SQLQueryComponent by SQLQueryComponent.composite( + lhs, SQLQueryComponent.standalone("IN"), list +)
\ No newline at end of file diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ListExpression.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ListExpression.kt new file mode 100644 index 0000000..e1522d0 --- /dev/null +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ListExpression.kt @@ -0,0 +1,22 @@ +package moe.nea.ledger.database.sql + +import moe.nea.ledger.database.DBType +import java.sql.PreparedStatement + +data class ListExpression<T, R>( + val elements: List<T>, + val dbType: DBType<T, R> +) : Operand<List<T>, List<R>> { + override fun asSql(): String { + return elements.joinToString(prefix = "(", postfix = ")") { "?" } + } + + override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int { + var index = startIndex + for (element in elements) { + dbType.set(stmt, index, element) + index++ + } + return index + } +} diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/Operand.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/Operand.kt index 9937414..b085103 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/Operand.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/Operand.kt @@ -1,5 +1,10 @@ package moe.nea.ledger.database.sql -interface Operand<T> : SQLQueryComponent { +interface Operand<T, + /** + * The db sided type (or a rough equivalence). + * @see moe.nea.ledger.database.DBType Raw type parameter + */ + Raw> : SQLQueryComponent { }
\ No newline at end of file diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/SQLQueryComponent.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/SQLQueryComponent.kt index 10b2be6..77d63d3 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/SQLQueryComponent.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/SQLQueryComponent.kt @@ -11,6 +11,25 @@ interface SQLQueryComponent { fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int companion object { + fun composite(vararg elements: SQLQueryComponent): SQLQueryComponent { + return object : SQLQueryComponent { + override fun asSql(): String { + return elements.joinToString(" ") { it.asSql() } + } + + override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int { + var index = startIndex + for (element in elements) { + val lastIndex = index + index = element.appendToStatement(stmt, index) + require(lastIndex <= index) { "$element just tried to go back in time $index < $lastIndex" } + } + return index + + } + } + } + fun standalone(sql: String): SQLQueryComponent { return object : SQLQueryComponent { override fun asSql(): String { diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/Selectable.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/Selectable.kt index a95b66b..8241a9d 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/Selectable.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/Selectable.kt @@ -6,12 +6,12 @@ import moe.nea.ledger.database.DBType /** * Something that can be selected. Like a column, or an expression thereof */ -interface Selectable<T> : SQLQueryComponent, IntoSelectable<T> { - override fun asSelectable(): Selectable<T> { +interface Selectable<T, Raw> : SQLQueryComponent, IntoSelectable<T> { + override fun asSelectable(): Selectable<T, Raw> { return this } - val dbType: DBType<T> - fun guessColumn(): Column<T>? + val dbType: DBType<T, Raw> + fun guessColumn(): Column<T, *>? } diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/StringOperand.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/StringOperand.kt index 24f4e78..b8d3690 100644 --- a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/StringOperand.kt +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/StringOperand.kt @@ -2,7 +2,10 @@ package moe.nea.ledger.database.sql import java.sql.PreparedStatement -data class StringOperand(val value: String) : Operand<String> { +/** + * As opposed to just any [Operand<*, String>][Operand], this string operand represents a string operand that is part of the query, as opposed to potentially the state of a column. + */ +data class StringOperand(val value: String) : Operand<String, String> { override fun asSql(): String { return "?" } diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/TypedOperand.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/TypedOperand.kt new file mode 100644 index 0000000..8a1f723 --- /dev/null +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/TypedOperand.kt @@ -0,0 +1,7 @@ +package moe.nea.ledger.database.sql + +import moe.nea.ledger.database.DBType + +interface TypedOperand<T, Raw> : Operand<T, Raw> { + val dbType: DBType<T, Raw> +} diff --git a/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ValuedOperand.kt b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ValuedOperand.kt new file mode 100644 index 0000000..714b4b5 --- /dev/null +++ b/database/core/src/main/kotlin/moe/nea/ledger/database/sql/ValuedOperand.kt @@ -0,0 +1,15 @@ +package moe.nea.ledger.database.sql + +import moe.nea.ledger.database.DBType +import java.sql.PreparedStatement + +class ValuedOperand<T, R>(val dbType: DBType<T, R>, val value: T) : Operand<T, R> { + override fun asSql(): String { + return "?" + } + + override fun appendToStatement(stmt: PreparedStatement, startIndex: Int): Int { + dbType.set(stmt, startIndex, value) + return startIndex + 1 + } +} diff --git a/database/impl/src/main/kotlin/moe/nea/ledger/database/DBUpgrade.kt b/database/impl/src/main/kotlin/moe/nea/ledger/database/DBUpgrade.kt index 7d1782a..9739978 100644 --- a/database/impl/src/main/kotlin/moe/nea/ledger/database/DBUpgrade.kt +++ b/database/impl/src/main/kotlin/moe/nea/ledger/database/DBUpgrade.kt @@ -37,14 +37,14 @@ interface DBUpgrade { return upgrades.groupBy { it.toVersion } } - fun createTable(to: Long, table: Table, vararg columns: Column<*>): DBUpgrade { + fun createTable(to: Long, table: Table, vararg columns: Column<*, *>): DBUpgrade { require(columns.all { it in table.columns }) return of("Create table ${table}", to) { table.createIfNotExists(it, columns.toList()) } } - fun addColumns(to: Long, table: Table, vararg columns: Column<*>): DBUpgrade { + fun addColumns(to: Long, table: Table, vararg columns: Column<*, *>): DBUpgrade { return of("Add columns to table $table", to) { table.alterTableAddColumns(it, columns.toList()) } diff --git a/database/impl/src/main/kotlin/moe/nea/ledger/database/Upgrades.kt b/database/impl/src/main/kotlin/moe/nea/ledger/database/Upgrades.kt index e83abe7..76dfb5d 100644 --- a/database/impl/src/main/kotlin/moe/nea/ledger/database/Upgrades.kt +++ b/database/impl/src/main/kotlin/moe/nea/ledger/database/Upgrades.kt @@ -15,6 +15,4 @@ class Upgrades { DBItemEntry.itemId, DBItemEntry.size, DBItemEntry.mode, DBItemEntry.transactionId )) } - - }
\ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..1f98193 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1745930157, + "narHash": "sha256-y3h3NLnzRSiUkYpnfvnS669zWZLoqqI6NprtLQ+5dck=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "46e634be05ce9dc6d4db8e664515ba10b78151ae", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..c508abe --- /dev/null +++ b/flake.nix @@ -0,0 +1,30 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { + self, + nixpkgs, + flake-utils, + ... + }: + flake-utils.lib.eachDefaultSystem (system: let + overlays = []; + pkgs = import nixpkgs {inherit system overlays;}; + deps = [ + pkgs.jdk8 + pkgs.jdk21 + pkgs.nodejs_22 + pkgs.nodePackages.pnpm + + ]; + in + with pkgs; { + devShells.default = mkShell { + buildInputs = deps; + }; + formatter = alejandra; + }); +}
\ No newline at end of file diff --git a/mod/log4j2.xml b/mod/log4j2.xml index af9b1b7..ff7a816 100644 --- a/mod/log4j2.xml +++ b/mod/log4j2.xml @@ -1,5 +1,9 @@ <?xml version="1.0" encoding="UTF-8"?> <Configuration status="WARN"> <!-- Filter out Hypixel scoreboard and sound errors --> - <RegexFilter regex="Error executing task.*|Unable to play unknown soundEvent.*" onMatch="DENY" onMismatch="NEUTRAL"/> + <RegexFilter regex="Unable to play unknown soundEvent.*" onMatch="DENY" onMismatch="NEUTRAL"/> + <RegexFilter regex="Zip file .* failed to read properly, it will be ignored.*" onMatch="DENY" onMismatch="NEUTRAL"/> + <RegexFilter regex="There was a problem reading the entry META-INF/versions/9/.* in the jar .* - probably a corrupt zip" onMatch="DENY" onMismatch="NEUTRAL"/> + <RegexFilter regex="Unable to read a class file correctly" onMatch="DENY" onMismatch="NEUTRAL"/> + <RegexFilter regex="Error executing task" onMatch="DENY" onMismatch="NEUTRAL"/> </Configuration>
\ No newline at end of file diff --git a/mod/src/main/java/moe/nea/ledger/init/AutoDiscoveryMixinPlugin.java b/mod/src/main/java/moe/nea/ledger/init/AutoDiscoveryMixinPlugin.java index 56841b5..64fa6c2 100644 --- a/mod/src/main/java/moe/nea/ledger/init/AutoDiscoveryMixinPlugin.java +++ b/mod/src/main/java/moe/nea/ledger/init/AutoDiscoveryMixinPlugin.java @@ -96,6 +96,8 @@ public class AutoDiscoveryMixinPlugin implements IMixinConfigPlugin { * @param className the name or path of a class to be registered as a mixin. */ public void tryAddMixinClass(String className) { + if (!className.endsWith(".class")) return; + if (className.indexOf('$') >= 0) return; String norm = (className.endsWith(".class") ? className.substring(0, className.length() - ".class".length()) : className) .replace("\\", "/") .replace("/", "."); diff --git a/mod/src/main/java/moe/nea/ledger/mixin/AccessorContainerDispenser.java b/mod/src/main/java/moe/nea/ledger/mixin/AccessorContainerDispenser.java new file mode 100644 index 0000000..a3d32c4 --- /dev/null +++ b/mod/src/main/java/moe/nea/ledger/mixin/AccessorContainerDispenser.java @@ -0,0 +1,12 @@ +package moe.nea.ledger.mixin; + +import net.minecraft.inventory.ContainerDispenser; +import net.minecraft.inventory.IInventory; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(ContainerDispenser.class) +public interface AccessorContainerDispenser { + @Accessor("dispenserInventory") + IInventory getDispenserInventory_ledger(); +} diff --git a/mod/src/main/java/moe/nea/ledger/mixin/AccessorContainerHopper.java b/mod/src/main/java/moe/nea/ledger/mixin/AccessorContainerHopper.java new file mode 100644 index 0000000..ee29d4f --- /dev/null +++ b/mod/src/main/java/moe/nea/ledger/mixin/AccessorContainerHopper.java @@ -0,0 +1,13 @@ +package moe.nea.ledger.mixin; + +import net.minecraft.inventory.ContainerDispenser; +import net.minecraft.inventory.ContainerHopper; +import net.minecraft.inventory.IInventory; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(ContainerHopper.class) +public interface AccessorContainerHopper { + @Accessor("hopperInventory") + IInventory getHopperInventory_ledger(); +} diff --git a/mod/src/main/kotlin/moe/nea/ledger/ItemIdProvider.kt b/mod/src/main/kotlin/moe/nea/ledger/ItemIdProvider.kt index 0bacf32..ff2c691 100644 --- a/mod/src/main/kotlin/moe/nea/ledger/ItemIdProvider.kt +++ b/mod/src/main/kotlin/moe/nea/ledger/ItemIdProvider.kt @@ -20,12 +20,12 @@ class ItemIdProvider { @SubscribeEvent fun onMouseInput(event: GuiScreenEvent.MouseInputEvent.Pre) { if (Mouse.getEventButton() == -1) return - MinecraftForge.EVENT_BUS.post(BeforeGuiAction(event.gui)) + BeforeGuiAction(event.gui).post() } @SubscribeEvent fun onKeyInput(event: GuiScreenEvent.KeyboardInputEvent.Pre) { - MinecraftForge.EVENT_BUS.post(BeforeGuiAction(event.gui)) + BeforeGuiAction(event.gui).post() } private val knownNames = mutableMapOf<String, ItemId>() diff --git a/mod/src/main/kotlin/moe/nea/ledger/Ledger.kt b/mod/src/main/kotlin/moe/nea/ledger/Ledger.kt index 5db4346..87c990c 100644 --- a/mod/src/main/kotlin/moe/nea/ledger/Ledger.kt +++ b/mod/src/main/kotlin/moe/nea/ledger/Ledger.kt @@ -16,17 +16,21 @@ import moe.nea.ledger.modules.AccessorySwapperDetection import moe.nea.ledger.modules.AllowanceDetection import moe.nea.ledger.modules.AuctionHouseDetection import moe.nea.ledger.modules.BankDetection +import moe.nea.ledger.modules.BankInterestDetection +import moe.nea.ledger.modules.BasicReforgeDetection import moe.nea.ledger.modules.BazaarDetection import moe.nea.ledger.modules.BazaarOrderDetection import moe.nea.ledger.modules.BitsDetection import moe.nea.ledger.modules.BitsShopDetection +import moe.nea.ledger.modules.CaducousFeederDetection import moe.nea.ledger.modules.DragonEyePlacementDetection -import moe.nea.ledger.modules.`DragonSacrificeDetection` +import moe.nea.ledger.modules.DragonSacrificeDetection import moe.nea.ledger.modules.DungeonChestDetection import moe.nea.ledger.modules.ExternalDataProvider import moe.nea.ledger.modules.EyedropsDetection import moe.nea.ledger.modules.ForgeDetection import moe.nea.ledger.modules.GambleDetection +import moe.nea.ledger.modules.GhostCoinDropDetection import moe.nea.ledger.modules.GodPotionDetection import moe.nea.ledger.modules.GodPotionMixinDetection import moe.nea.ledger.modules.GummyPolarBearDetection @@ -36,8 +40,10 @@ import moe.nea.ledger.modules.MineshaftCorpseDetection import moe.nea.ledger.modules.MinionDetection import moe.nea.ledger.modules.NpcDetection import moe.nea.ledger.modules.PestRepellentDetection +import moe.nea.ledger.modules.StonksAuctionDetection import moe.nea.ledger.modules.UpdateChecker import moe.nea.ledger.modules.VisitorDetection +import moe.nea.ledger.telemetry.TelemetryProvider import moe.nea.ledger.utils.ErrorUtil import moe.nea.ledger.utils.MinecraftExecutor import moe.nea.ledger.utils.di.DI @@ -90,7 +96,7 @@ class Ledger { // You sold Cactus x1 for 3 Coins! // You bought back Potato x3 for 9 Coins! - TODO: TRADING, FORGE, VISITORS / COPPER, CORPSES ÖFFNEN, HIGH / LOW GAMBLES, MINION ITEMS (maybe inferno refuel) + TODO: TRADING, FORGE, MINION ITEMS (maybe inferno refuel) TODO: PET LEVELING COSTS AT FANN, SLAYER / MOB DROPS, SLAYER START COST */ companion object { @@ -108,7 +114,9 @@ class Ledger { tickQueue.add(runnable) } - val di = DI() + private val di = DI() + + fun leakDI() = di } @Mod.EventHandler @@ -127,10 +135,13 @@ class Ledger { AllowanceDetection::class.java, AuctionHouseDetection::class.java, BankDetection::class.java, + BankInterestDetection::class.java, + BasicReforgeDetection::class.java, BazaarDetection::class.java, BazaarOrderDetection::class.java, BitsDetection::class.java, BitsShopDetection::class.java, + CaducousFeederDetection::class.java, ConfigCommand::class.java, DebugDataCommand::class.java, DragonEyePlacementDetection::class.java, @@ -141,6 +152,7 @@ class Ledger { EyedropsDetection::class.java, ForgeDetection::class.java, GambleDetection::class.java, + GhostCoinDropDetection::class.java, GodPotionDetection::class.java, GodPotionMixinDetection::class.java, GummyPolarBearDetection::class.java, @@ -156,6 +168,7 @@ class Ledger { PestRepellentDetection::class.java, QueryCommand::class.java, RequestUtil::class.java, + StonksAuctionDetection::class.java, TriggerCommand::class.java, UpdateChecker::class.java, VisitorDetection::class.java, diff --git a/mod/src/main/kotlin/moe/nea/ledger/QueryCommand.kt b/mod/src/main/kotlin/moe/nea/ledger/QueryCommand.kt index abdc13a..80dd54c 100644 --- a/mod/src/main/kotlin/moe/nea/ledger/QueryCommand.kt +++ b/mod/src/main/kotlin/moe/nea/ledger/QueryCommand.kt @@ -172,7 +172,7 @@ class QueryCommand : CommandBase() { override val name: String get() = "withitem" - private val itemIdProvider = Ledger.di.provide<ItemIdProvider>() // TODO: close this escape hatch + private val itemIdProvider = Ledger.leakDI().provide<ItemIdProvider>() // TODO: close this escape hatch override fun getFilter(text: String): BooleanExpression { return Clause { column(DBItemEntry.itemId) like text } } diff --git a/mod/src/main/kotlin/moe/nea/ledger/config/DebugOptions.kt b/mod/src/main/kotlin/moe/nea/ledger/config/DebugOptions.kt index fd5ed3d..6b4e51c 100644 --- a/mod/src/main/kotlin/moe/nea/ledger/config/DebugOptions.kt +++ b/mod/src/main/kotlin/moe/nea/ledger/config/DebugOptions.kt @@ -2,6 +2,7 @@ package moe.nea.ledger.config import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorBoolean import io.github.notenoughupdates.moulconfig.annotations.ConfigOption +import moe.nea.ledger.DevUtil class DebugOptions { @ConfigOption(name = "Log entries to chat", @@ -9,5 +10,5 @@ class DebugOptions { @Transient @ConfigEditorBoolean @JvmField - var logEntries = false + var logEntries = DevUtil.isDevEnv } diff --git a/mod/src/main/kotlin/moe/nea/ledger/events/BeforeGuiAction.kt b/mod/src/main/kotlin/moe/nea/ledger/events/BeforeGuiAction.kt index 098912a..7f6eae9 100644 --- a/mod/src/main/kotlin/moe/nea/ledger/events/BeforeGuiAction.kt +++ b/mod/src/main/kotlin/moe/nea/ledger/events/BeforeGuiAction.kt @@ -1,11 +1,21 @@ package moe.nea.ledger.events +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import moe.nea.ledger.telemetry.GuiContextValue +import moe.nea.ledger.utils.telemetry.ContextValue import net.minecraft.client.gui.GuiScreen import net.minecraft.client.gui.inventory.GuiChest +import net.minecraft.client.gui.inventory.GuiContainer import net.minecraft.inventory.ContainerChest import net.minecraftforge.fml.common.eventhandler.Event -data class BeforeGuiAction(val gui: GuiScreen) : Event() { +data class BeforeGuiAction(val gui: GuiScreen) : LedgerEvent() { val chest = gui as? GuiChest val chestSlots = chest?.inventorySlots as ContainerChest? + override fun serialize(): JsonElement { + return JsonObject().apply { + add("gui", GuiContextValue(gui).serialize()) + } + } } diff --git a/mod/src/main/kotlin/moe/nea/ledger/events/LedgerEvent.kt b/mod/src/main/kotlin/moe/nea/ledger/events/LedgerEvent.kt new file mode 100644 index 0000000..cbb3f81 --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/events/LedgerEvent.kt @@ -0,0 +1,22 @@ +package moe.nea.ledger.events + +import moe.nea.ledger.Ledger +import moe.nea.ledger.utils.ErrorUtil +import moe.nea.ledger.utils.telemetry.CommonKeys +import moe.nea.ledger.utils.telemetry.ContextValue +import net.minecraftforge.common.MinecraftForge +import net.minecraftforge.fml.common.eventhandler.Event + +abstract class LedgerEvent : Event(), ContextValue { + fun post() { + Ledger.leakDI() + .provide<ErrorUtil>() + .catch( + CommonKeys.EVENT_MESSAGE to ContextValue.string("Error during event execution"), + "event_instance" to this, + "event_type" to ContextValue.string(javaClass.name) + ) { + MinecraftForge.EVENT_BUS.post(this) + } + } +}
\ No newline at end of file diff --git a/mod/src/main/kotlin/moe/nea/ledger/modules/BankInterestDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/BankInterestDetection.kt new file mode 100644 index 0000000..5069930 --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/modules/BankInterestDetection.kt @@ -0,0 +1,44 @@ +package moe.nea.ledger.modules + +import moe.nea.ledger.ItemChange +import moe.nea.ledger.LedgerEntry +import moe.nea.ledger.LedgerLogger +import moe.nea.ledger.SHORT_NUMBER_PATTERN +import moe.nea.ledger.TransactionType +import moe.nea.ledger.events.ChatReceived +import moe.nea.ledger.parseShortNumber +import moe.nea.ledger.useMatcher +import moe.nea.ledger.utils.di.Inject +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import java.util.regex.Matcher +import java.util.regex.Pattern + +class BankInterestDetection { + + val bankInterestPattern = + Pattern.compile("You have just received (?<coins>$SHORT_NUMBER_PATTERN) coins as interest in your (co-op|personal) bank account!") + val offlineBankInterestPattern = + Pattern.compile("Since you've been away you earned (?<coins>$SHORT_NUMBER_PATTERN) coins as interest in your personal bank account!") + + @Inject + lateinit var logger: LedgerLogger + + + @SubscribeEvent + fun onChat(event: ChatReceived) { + fun Matcher.logInterest() { + logger.logEntry( + LedgerEntry( + TransactionType.BANK_INTEREST, + event.timestamp, + listOf( + ItemChange.gainCoins(parseShortNumber(group("coins"))), + ) + ) + ) + } + + bankInterestPattern.useMatcher(event.message) { logInterest() } + offlineBankInterestPattern.useMatcher(event.message) { logInterest() } + } +} diff --git a/mod/src/main/kotlin/moe/nea/ledger/modules/BasicReforgeDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/BasicReforgeDetection.kt new file mode 100644 index 0000000..17e2983 --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/modules/BasicReforgeDetection.kt @@ -0,0 +1,71 @@ +package moe.nea.ledger.modules + +import moe.nea.ledger.ExpiringValue +import moe.nea.ledger.ItemChange +import moe.nea.ledger.ItemId +import moe.nea.ledger.LedgerEntry +import moe.nea.ledger.LedgerLogger +import moe.nea.ledger.SHORT_NUMBER_PATTERN +import moe.nea.ledger.TransactionType +import moe.nea.ledger.events.ChatReceived +import moe.nea.ledger.events.GuiClickEvent +import moe.nea.ledger.getDisplayNameU +import moe.nea.ledger.getInternalId +import moe.nea.ledger.getLore +import moe.nea.ledger.parseShortNumber +import moe.nea.ledger.unformattedString +import moe.nea.ledger.useMatcher +import moe.nea.ledger.utils.di.Inject +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import kotlin.time.Duration.Companion.seconds + +class BasicReforgeDetection { + + var costPattern = "(?<cost>$SHORT_NUMBER_PATTERN) Coins".toPattern() + + @Inject + lateinit var logger: LedgerLogger + + data class ReforgeInstance( + val price: Double, + val item: ItemId, + ) + + var lastReforge = ExpiringValue.empty<ReforgeInstance>() + + @SubscribeEvent + fun onReforgeClick(event: GuiClickEvent) { + val slot = event.slotIn ?: return + val displayName = slot.inventory.displayName.unformattedText + if (!displayName.unformattedString().contains("Reforge Item") && + !displayName.unformattedString().startsWith("The Hex") + ) return + val stack = slot.stack ?: return + val cost = stack.getLore() + .firstNotNullOfOrNull { costPattern.useMatcher(it.unformattedString()) { parseShortNumber(group("cost")) } } + ?: return + + if (stack.getDisplayNameU() == "§aReforge Item" || stack.getDisplayNameU() == "§aRandom Basic Reforge") { + lastReforge = ExpiringValue(ReforgeInstance(cost, ItemId.NIL /*TODO: read out item stack that is being reforged to save it as a transformed item!*/)) + } + } + + val reforgeChatNotification = "You reforged your .* into a .*!".toPattern() + + @SubscribeEvent + fun onReforgeChat(event: ChatReceived) { + reforgeChatNotification.useMatcher(event.message) { + val reforge = lastReforge.get(3.seconds) ?: return + logger.logEntry( + LedgerEntry( + TransactionType.BASIC_REFORGE, + event.timestamp, + listOf( + ItemChange.loseCoins(reforge.price), + ItemChange(reforge.item, 1.0, ItemChange.ChangeDirection.TRANSFORM) + ) + ) + ) + } + } +}
\ No newline at end of file diff --git a/mod/src/main/kotlin/moe/nea/ledger/modules/CaducousFeederDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/CaducousFeederDetection.kt new file mode 100644 index 0000000..b64c7e5 --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/modules/CaducousFeederDetection.kt @@ -0,0 +1,48 @@ +package moe.nea.ledger.modules + +import moe.nea.ledger.ItemChange +import moe.nea.ledger.ItemId +import moe.nea.ledger.LedgerEntry +import moe.nea.ledger.LedgerLogger +import moe.nea.ledger.TransactionType +import moe.nea.ledger.events.GuiClickEvent +import moe.nea.ledger.gen.ItemIds +import moe.nea.ledger.getDisplayNameU +import moe.nea.ledger.getInternalId +import moe.nea.ledger.unformattedString +import moe.nea.ledger.utils.di.Inject +import net.minecraft.client.Minecraft +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import java.time.Instant + +class CaducousFeederDetection { + + @Inject + lateinit var logger: LedgerLogger + + @Inject + lateinit var minecraft: Minecraft + + @SubscribeEvent + fun onFeederClick(event: GuiClickEvent) { + val slot = event.slotIn ?: return + val displayName = slot.inventory.displayName.unformattedText + if (!displayName.unformattedString().contains("Confirm Caducous Feeder")) return + val stack = slot.stack ?: return + val player = minecraft.thePlayer ?: return + if (!player.inventory.mainInventory.any { it?.getInternalId() == ItemIds.ULTIMATE_CARROT_CANDY }) return + if (stack.getDisplayNameU() != "§aUse Caducous Feeder") return + val petId = slot.inventory.getStackInSlot(13)?.getInternalId() ?: ItemId.NIL + + logger.logEntry( + LedgerEntry( + TransactionType.CADUCOUS_FEEDER_USED, + Instant.now(), + listOf( + ItemChange.lose(ItemIds.ULTIMATE_CARROT_CANDY, 1), + ItemChange(petId, 1.0, ItemChange.ChangeDirection.TRANSFORM), + ) + ) + ) + } +}
\ No newline at end of file diff --git a/mod/src/main/kotlin/moe/nea/ledger/modules/ExternalDataProvider.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/ExternalDataProvider.kt index 93bb453..42a1f42 100644 --- a/mod/src/main/kotlin/moe/nea/ledger/modules/ExternalDataProvider.kt +++ b/mod/src/main/kotlin/moe/nea/ledger/modules/ExternalDataProvider.kt @@ -14,6 +14,7 @@ import java.util.concurrent.CompletableFuture class ExternalDataProvider @Inject constructor( val requestUtil: RequestUtil ) { + // TODO: Save all the data locally, so in case of a failed request older versions can be used fun createAuxillaryDataRequest(path: String): Request { return requestUtil.createRequest("https://github.com/nea89o/ledger-auxiliary-data/raw/refs/heads/master/$path") @@ -22,7 +23,9 @@ class ExternalDataProvider @Inject constructor( private val itemNameFuture: CompletableFuture<Map<String, String>> = CompletableFuture.supplyAsync { val request = createAuxillaryDataRequest("data/item_names.json") val response = request.execute(requestUtil) - val nameMap = response.json(GsonUtil.typeToken<Map<String, String>>()) + val nameMap = + response?.json(GsonUtil.typeToken<Map<String, String>>()) + ?: mapOf() return@supplyAsync nameMap } diff --git a/mod/src/main/kotlin/moe/nea/ledger/modules/ForgeDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/ForgeDetection.kt index 95811ed..b8974c0 100644 --- a/mod/src/main/kotlin/moe/nea/ledger/modules/ForgeDetection.kt +++ b/mod/src/main/kotlin/moe/nea/ledger/modules/ForgeDetection.kt @@ -9,6 +9,7 @@ import moe.nea.ledger.getInternalId import moe.nea.ledger.matches import moe.nea.ledger.unformattedString import moe.nea.ledger.utils.di.Inject +import net.minecraft.item.EnumDyeColor import net.minecraftforge.fml.common.eventhandler.SubscribeEvent import java.time.Instant @@ -20,7 +21,9 @@ class ForgeDetection { fun onClick(event: GuiClickEvent) { val slot = event.slotIn ?: return val clickedItem = slot.stack ?: return + val dyeColor = EnumDyeColor.byMetadata(clickedItem.itemDamage) if (clickedItem.displayName.unformattedString() != "Confirm") return + if (dyeColor == EnumDyeColor.RED) return val furnaceSlotName = slot.inventory.getStackInSlot(furnaceSlot)?.displayName?.unformattedString() ?: return if (!furnaceName.matches(furnaceSlotName)) return diff --git a/mod/src/main/kotlin/moe/nea/ledger/modules/GhostCoinDropDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/GhostCoinDropDetection.kt new file mode 100644 index 0000000..42084e2 --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/modules/GhostCoinDropDetection.kt @@ -0,0 +1,38 @@ +package moe.nea.ledger.modules + +import moe.nea.ledger.ItemChange +import moe.nea.ledger.LedgerEntry +import moe.nea.ledger.LedgerLogger +import moe.nea.ledger.SHORT_NUMBER_PATTERN +import moe.nea.ledger.TransactionType +import moe.nea.ledger.events.ChatReceived +import moe.nea.ledger.parseShortNumber +import moe.nea.ledger.useMatcher +import moe.nea.ledger.utils.di.Inject +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import java.util.regex.Pattern + +class GhostCoinDropDetection { + + val ghostCoinPattern = + Pattern.compile("The ghost's death materialized (?<coins>$SHORT_NUMBER_PATTERN) coins from the mists!") + + @Inject + lateinit var logger: LedgerLogger + + @SubscribeEvent + fun onGhostCoinDrop(event: ChatReceived) { + ghostCoinPattern.useMatcher(event.message) { + logger.logEntry( + LedgerEntry( + // TODO: merge this into a generic mob drop tt + TransactionType.GHOST_COIN_DROP, + event.timestamp, + listOf( + ItemChange.gainCoins(parseShortNumber(group("coins"))), + ) + ) + ) + } + } +} diff --git a/mod/src/main/kotlin/moe/nea/ledger/modules/StonksAuctionDetection.kt b/mod/src/main/kotlin/moe/nea/ledger/modules/StonksAuctionDetection.kt new file mode 100644 index 0000000..4f3706c --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/modules/StonksAuctionDetection.kt @@ -0,0 +1,59 @@ +package moe.nea.ledger.modules + +import moe.nea.ledger.ItemChange +import moe.nea.ledger.LedgerEntry +import moe.nea.ledger.LedgerLogger +import moe.nea.ledger.SHORT_NUMBER_PATTERN +import moe.nea.ledger.TransactionType +import moe.nea.ledger.events.ChatReceived +import moe.nea.ledger.gen.ItemIds +import moe.nea.ledger.parseShortNumber +import moe.nea.ledger.useMatcher +import moe.nea.ledger.utils.di.Inject +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import java.util.regex.Matcher +import java.util.regex.Pattern + +class StonksAuctionDetection { + + val stonksPlacedBid = + Pattern.compile("Successfully placed your Stonk bid of (?<coins>$SHORT_NUMBER_PATTERN) Coins!") + val stonksIncreaseBid = + Pattern.compile("Successfully increased your Stonk bid by (?<coins>$SHORT_NUMBER_PATTERN) Coins!") + val stonksClaim = + Pattern.compile("You claimed (?<count>$SHORT_NUMBER_PATTERN)x Stock of Stonks from the Stonks Auction!") + + @Inject + lateinit var logger: LedgerLogger + + @SubscribeEvent + fun onChat(event: ChatReceived) { + fun Matcher.logBidding() { + logger.logEntry( + LedgerEntry( + TransactionType.STONKS_AUCTION, + event.timestamp, + listOf( + ItemChange.loseCoins(parseShortNumber(group("coins"))), + ) + ) + ) + } + + fun Matcher.logClaim() { + logger.logEntry( + LedgerEntry( + TransactionType.STONKS_AUCTION, + event.timestamp, + listOf( + ItemChange.gain(ItemIds.STOCK_OF_STONKS, parseShortNumber(group("count"))) + ) + ) + ) + } + + stonksPlacedBid.useMatcher(event.message) { logBidding() } + stonksIncreaseBid.useMatcher(event.message) { logBidding() } + stonksClaim.useMatcher(event.message) { logClaim() } + } +} diff --git a/mod/src/main/kotlin/moe/nea/ledger/telemetry/GuiContextValue.kt b/mod/src/main/kotlin/moe/nea/ledger/telemetry/GuiContextValue.kt new file mode 100644 index 0000000..2d7db39 --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/telemetry/GuiContextValue.kt @@ -0,0 +1,16 @@ +package moe.nea.ledger.telemetry + +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import moe.nea.ledger.utils.ScreenUtil +import moe.nea.ledger.utils.telemetry.ContextValue +import net.minecraft.client.gui.GuiScreen + +class GuiContextValue(val gui: GuiScreen) : ContextValue { + override fun serialize(): JsonElement { + return JsonObject().apply { + addProperty("class", gui.javaClass.name) + addProperty("name", ScreenUtil.estimateName(gui)) + } + } +} diff --git a/mod/src/main/kotlin/moe/nea/ledger/TelemetryProvider.kt b/mod/src/main/kotlin/moe/nea/ledger/telemetry/TelemetryProvider.kt index d9c7108..c2fff23 100644 --- a/mod/src/main/kotlin/moe/nea/ledger/TelemetryProvider.kt +++ b/mod/src/main/kotlin/moe/nea/ledger/telemetry/TelemetryProvider.kt @@ -1,8 +1,10 @@ -package moe.nea.ledger +package moe.nea.ledger.telemetry import com.google.gson.JsonArray import com.google.gson.JsonElement import com.google.gson.JsonObject +import moe.nea.ledger.DevUtil +import moe.nea.ledger.Ledger import moe.nea.ledger.gen.BuildConfig import moe.nea.ledger.utils.di.DI import moe.nea.ledger.utils.di.DIProvider @@ -40,7 +42,7 @@ object TelemetryProvider { } fun setupDefaultSpan() { - val sp = Span.current() + val sp = Span.rootSpan sp.add(USER, MinecraftUser(Minecraft.getMinecraft().session)) sp.add(MINECRAFT_VERSION, ContextValue.compound( "static" to "1.8.9", diff --git a/mod/src/main/kotlin/moe/nea/ledger/utils/ScreenUtil.kt b/mod/src/main/kotlin/moe/nea/ledger/utils/ScreenUtil.kt new file mode 100644 index 0000000..0305126 --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/utils/ScreenUtil.kt @@ -0,0 +1,29 @@ +package moe.nea.ledger.utils + +import moe.nea.ledger.mixin.AccessorContainerDispenser +import moe.nea.ledger.mixin.AccessorContainerHopper +import net.minecraft.client.gui.GuiScreen +import net.minecraft.client.gui.inventory.GuiContainer +import net.minecraft.inventory.ContainerChest +import net.minecraft.inventory.IInventory + +object ScreenUtil { + fun estimateInventory(screen: GuiScreen?): IInventory? { + if (screen !is GuiContainer) { + return null + } + val container = screen.inventorySlots ?: return null + if (container is ContainerChest) + return container.lowerChestInventory + if (container is AccessorContainerDispenser) + return container.dispenserInventory_ledger + if (container is AccessorContainerHopper) + return container.hopperInventory_ledger + return null + + } + + fun estimateName(screen: GuiScreen?): String? { + return estimateInventory(screen)?.name + } +}
\ No newline at end of file diff --git a/mod/src/main/kotlin/moe/nea/ledger/utils/network/RequestTrace.kt b/mod/src/main/kotlin/moe/nea/ledger/utils/network/RequestTrace.kt new file mode 100644 index 0000000..3953e09 --- /dev/null +++ b/mod/src/main/kotlin/moe/nea/ledger/utils/network/RequestTrace.kt @@ -0,0 +1,21 @@ +package moe.nea.ledger.utils.network + +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import moe.nea.ledger.utils.telemetry.ContextValue + +class RequestTrace(val request: Request) : ContextValue { + override fun serialize(): JsonElement { + return JsonObject().apply { + addProperty("url", request.url.toString()) + addProperty("method", request.method.name) + addProperty("content-type", request.headers["content-type"]) + addProperty("accept", request.headers["accept"]) + } + } + + companion object { + val KEY = "http_request" + fun createTrace(request: Request): Pair<String, RequestTrace> = KEY to RequestTrace(request) + } +}
\ No newline at end of file diff --git a/mod/src/main/kotlin/moe/nea/ledger/utils/network/RequestUtil.kt b/mod/src/main/kotlin/moe/nea/ledger/utils/network/RequestUtil.kt index a49c65a..8101527 100644 --- a/mod/src/main/kotlin/moe/nea/ledger/utils/network/RequestUtil.kt +++ b/mod/src/main/kotlin/moe/nea/ledger/utils/network/RequestUtil.kt @@ -2,6 +2,8 @@ package moe.nea.ledger.utils.network import moe.nea.ledger.utils.ErrorUtil import moe.nea.ledger.utils.di.Inject +import moe.nea.ledger.utils.telemetry.CommonKeys +import moe.nea.ledger.utils.telemetry.ContextValue import java.net.URL import java.net.URLConnection import java.security.KeyStore @@ -38,7 +40,10 @@ class RequestUtil @Inject constructor(val errorUtil: ErrorUtil) { fun createRequest(url: String) = createRequest(URL(url)) fun createRequest(url: URL) = Request(url, Request.Method.GET, null, mapOf()) - fun executeRequest(request: Request): Response { + fun executeRequest(request: Request): Response? = errorUtil.catch( + CommonKeys.EVENT_MESSAGE to ContextValue.string("Failed to execute request"), + RequestTrace.createTrace(request) + ) { val connection = request.url.openConnection() enhanceConnection(connection) connection.setRequestProperty("accept-encoding", "gzip") @@ -56,7 +61,7 @@ class RequestUtil @Inject constructor(val errorUtil: ErrorUtil) { val text = stream.bufferedReader().readText() stream.close() // Do NOT call connection.disconnect() to allow for connection reuse - return Response(request, text, connection.headerFields) + return@catch Response(request, text, connection.headerFields) } diff --git a/mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/Span.kt b/mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/Span.kt index 0d680a9..8b8e284 100644 --- a/mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/Span.kt +++ b/mod/src/main/kotlin/moe/nea/ledger/utils/telemetry/Span.kt @@ -2,9 +2,10 @@ package moe.nea.ledger.utils.telemetry class Span(val parent: Span?) : AutoCloseable { companion object { + val rootSpan = Span(null) private val _current = object : InheritableThreadLocal<Span>() { override fun initialValue(): Span { - return Span(null) + return Span(rootSpan) } override fun childValue(parentValue: Span?): Span { 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 index 6300a4b..b2a3222 100644 --- a/server/core/build.gradle.kts +++ b/server/core/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { 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")) 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 index c58763f..3240a65 100644 --- 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 @@ -6,16 +6,44 @@ 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 sh.ondr.jsonschema.jsonSchema +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) @@ -38,6 +66,48 @@ fun Route.apiRouting(database: Database) { 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() @@ -51,12 +121,82 @@ fun Route.apiRouting(database: Database) { 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 { diff --git a/server/core/test-requests/test-hello.http b/server/core/test-requests/test-hello.http deleted file mode 100644 index 3ddf352..0000000 --- a/server/core/test-requests/test-hello.http +++ /dev/null @@ -1,5 +0,0 @@ -### GET request to example server -GET localhost:8080/ - -### GET profiles -GET localhost:8080/api/profiles
\ No newline at end of file diff --git a/server/frontend/build.gradle.kts b/server/frontend/build.gradle.kts index 71b5c27..72fe6ea 100644 --- a/server/frontend/build.gradle.kts +++ b/server/frontend/build.gradle.kts @@ -6,6 +6,7 @@ plugins { } val webDist by tasks.register("webDist", PnpmTask::class) { + dependsOn(tasks.pnpmInstall) args.addAll("build") outputs.dir("dist") } diff --git a/server/frontend/index.html b/server/frontend/index.html index 48c59fc..afc19c2 100644 --- a/server/frontend/index.html +++ b/server/frontend/index.html @@ -9,7 +9,7 @@ </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> - <div id="root"></div> + <div id="root" class="min-h-[100vh]"></div> <script src="/src/index.tsx" type="module"></script> </body> diff --git a/server/frontend/package.json b/server/frontend/package.json index f8a89f1..e344700 100644 --- a/server/frontend/package.json +++ b/server/frontend/package.json @@ -9,22 +9,28 @@ "build": "vite build", "serve": "vite preview", "test:ts": "tsc --noEmit", - "genApi": "pnpx openapi-typescript http://localhost:8080/api.json -o src/api-schema.d.ts" + "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-js": "^1.9.3" + "solid-apexcharts": "^0.4.0", + "solid-js": "^1.9.3", + "tailwindcss": "^4.1.5" }, - -"devEngines": { + "devEngines": { "packageManager": { "name": "pnpm", "onFail": "error" diff --git a/server/frontend/pnpm-lock.yaml b/server/frontend/pnpm-lock.yaml index e7a2e34..d4b6e96 100644 --- a/server/frontend/pnpm-lock.yaml +++ b/server/frontend/pnpm-lock.yaml @@ -11,25 +11,46 @@ importers: '@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 + version: 1.3.1 + moment: + specifier: ^2.30.1 + version: 2.30.1 openapi-fetch: specifier: ^0.13.4 version: 0.13.4 + solid-apexcharts: + specifier: ^0.4.0 + version: 0.4.0(apexcharts@4.3.0)(solid-js@1.9.4) solid-js: specifier: ^1.9.3 version: 1.9.4 + tailwindcss: + specifier: ^4.1.5 + version: 4.1.5 devDependencies: openapi-typescript: specifier: ^7.5.2 version: 7.5.2(typescript@5.7.3) + solid-devtools: + specifier: ^0.33.0 + version: 0.33.0(solid-js@1.9.4)(vite@6.0.7(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.83.4)) typescript: specifier: ^5.7.2 version: 5.7.3 vite: specifier: ^6.0.0 - version: 6.0.7 + version: 6.0.7(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.83.4) vite-plugin-solid: specifier: ^2.11.0 - version: 2.11.0(solid-js@1.9.4)(vite@6.0.7) + version: 2.11.0(solid-js@1.9.4)(vite@6.0.7(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.83.4)) packages: @@ -102,6 +123,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-typescript@7.25.9': + resolution: {integrity: sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/template@7.25.9': resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} engines: {node: '>=6.9.0'} @@ -282,6 +309,91 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@nothing-but/utils@0.17.0': + resolution: {integrity: sha512-TuCHcHLOqDL0SnaAxACfuRHBNRgNJcNn9X0GiH5H3YSDBVquCr3qEIG3FOQAuMyZCbu9w8nk2CHhOsn7IvhIwQ==} + + '@parcel/watcher-android-arm64@2.5.0': + resolution: {integrity: sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.0': + resolution: {integrity: sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.0': + resolution: {integrity: sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.0': + resolution: {integrity: sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.0': + resolution: {integrity: sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.0': + resolution: {integrity: sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.0': + resolution: {integrity: sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.0': + resolution: {integrity: sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.0': + resolution: {integrity: sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.0': + resolution: {integrity: sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.0': + resolution: {integrity: sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.0': + resolution: {integrity: sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.0': + resolution: {integrity: sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.0': + resolution: {integrity: sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==} + engines: {node: '>= 10.0.0'} + '@redocly/ajv@8.11.2': resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==} @@ -387,11 +499,206 @@ packages: cpu: [x64] os: [win32] + '@solid-devtools/debugger@0.26.0': + resolution: {integrity: sha512-36QxZ+s/lY60E+Pb9q0eTsdqgaog4c823WIj5dC2LFdGrGXbVGBQEj6k7CgvMnEETdwndrd0Fm72fQyYPlZrVA==} + peerDependencies: + solid-js: ^1.9.0 + + '@solid-devtools/shared@0.19.0': + resolution: {integrity: sha512-OGo6l84f9X5YEAqSEM4Xl94+xKXSqmACMzKWsAqO0BStLBMVL0vIVu286AQk5XkNxn11/EB9wrdkZc9GUzKlxA==} + peerDependencies: + solid-js: ^1.9.0 + + '@solid-primitives/bounds@0.0.122': + resolution: {integrity: sha512-kUq/IprOdFr/rg2upon5lQGOoTnDAmxQS4ASKK2l+VwoKSctdPwgu/4qJxEITZikL+nB0myYZzBZWptySV0cRg==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/cursor@0.0.115': + resolution: {integrity: sha512-8nEmUN/sacXPChwuJOAi6Yi6VnxthW/Jk8VGvvcF38AenjUvOA6FHI6AkJILuFXjQw1PGxia1YbH/Mn77dPiOA==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/event-listener@2.3.3': + resolution: {integrity: sha512-DAJbl+F0wrFW2xmcV8dKMBhk9QLVLuBSW+TR4JmIfTaObxd13PuL7nqaXnaYKDWOYa6otB00qcCUIGbuIhSUgQ==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/keyboard@1.2.8': + resolution: {integrity: sha512-pJtcbkjozS6L1xvTht9rPpyPpX55nAkfBzbFWdf3y0Suwh6qClTibvvObzKOf7uzQ+8aZRDH4LsoGmbTKXtJjQ==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/media@2.2.10': + resolution: {integrity: sha512-zICx9lXvevycyHmzUp1AfrxmUsF27JGvDygf51mHUpvy/Y2SmxkM6UHKstBDlRSpLUhPTnF0iHCfdfne6g4Fow==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/platform@0.1.2': + resolution: {integrity: sha512-sSxcZfuUrtxcwV0vdjmGnZQcflACzMfLriVeIIWXKp8hzaS3Or3tO6EFQkTd3L8T5dTq+kTtLvPscXIpL0Wzdg==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/refs@1.0.8': + resolution: {integrity: sha512-+jIsWG8/nYvhaCoG2Vg6CJOLgTmPKFbaCrNQKWfChalgUf9WrVxWw0CdJb3yX15n5lUcQ0jBo6qYtuVVmBLpBw==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/resize-observer@2.0.27': + resolution: {integrity: sha512-RmusjHqoA4U6MKI/T9yBJVDttASHpWBki1+YwM9zGXEDBqbysTa3lZpnlB244LzphQmobgeXVS78v0KtXVsF9g==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/rootless@1.4.5': + resolution: {integrity: sha512-GFJE9GC3ojx0aUKqAUZmQPyU8fOVMtnVNrkdk2yS4kd17WqVSpXpoTmo9CnOwA+PG7FTzdIkogvfLQSLs4lrww==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/scheduled@1.4.4': + resolution: {integrity: sha512-BTGdFP7t+s7RSak+s1u0eTix4lHP23MrbGkgQTFlt1E+4fmnD/bEx3ZfNW7Grylz3GXgKyXrgDKA7jQ/wuWKgA==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/static-store@0.0.8': + resolution: {integrity: sha512-ZecE4BqY0oBk0YG00nzaAWO5Mjcny8Fc06CdbXadH9T9lzq/9GefqcSe/5AtdXqjvY/DtJ5C6CkcjPZO0o/eqg==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/static-store@0.0.9': + resolution: {integrity: sha512-8zaTXTEnQFqdwfkqWmGVb/OYgSTbRgxJSWQNfLuA+KnuW4RzTRQE2jzgnNJjJjaloruv9EHGvikmJzQJ5aOrEw==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/styles@0.0.114': + resolution: {integrity: sha512-SFXr16mgr6LvZAIj6L7i59HHg+prAmIF8VP/U3C6jSHz68Eh1G71vaWr9vlJVpy/j6bh1N8QUzu5CgtvIC92OQ==} + peerDependencies: + solid-js: ^1.6.12 + + '@solid-primitives/utils@6.2.3': + resolution: {integrity: sha512-CqAwKb2T5Vi72+rhebSsqNZ9o67buYRdEJrIFzRXz3U59QqezuuxPsyzTSVCacwS5Pf109VRsgCJQoxKRoECZQ==} + peerDependencies: + solid-js: ^1.6.12 + '@solidjs/router@0.15.3': resolution: {integrity: sha512-iEbW8UKok2Oio7o6Y4VTzLj+KFCmQPGEpm1fS3xixwFBdclFVBvaQVeibl1jys4cujfAK5Kn6+uG2uBm3lxOMw==} peerDependencies: solid-js: ^1.8.6 + '@svgdotjs/svg.draggable.js@3.0.5': + resolution: {integrity: sha512-ljL/fB0tAjRfFOJGhXpr7rEx9DJ6D7Pxt3AXvgxjEM17g6wK3Ho9nXhntraOMx8JLZdq4NBMjokeXMvnQzJVYA==} + peerDependencies: + '@svgdotjs/svg.js': ^3.2.4 + + '@svgdotjs/svg.filter.js@3.0.8': + resolution: {integrity: sha512-YshF2YDaeRA2StyzAs5nUPrev7npQ38oWD0eTRwnsciSL2KrRPMoUw8BzjIXItb3+dccKGTX3IQOd2NFzmHkog==} + engines: {node: '>= 0.8.0'} + + '@svgdotjs/svg.js@3.2.4': + resolution: {integrity: sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==} + + '@svgdotjs/svg.resize.js@2.0.5': + resolution: {integrity: sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA==} + engines: {node: '>= 14.18'} + peerDependencies: + '@svgdotjs/svg.js': ^3.2.4 + '@svgdotjs/svg.select.js': ^4.0.1 + + '@svgdotjs/svg.select.js@4.0.2': + resolution: {integrity: sha512-5gWdrvoQX3keo03SCmgaBbD+kFftq0F/f2bzCbNnpkkvW6tk4rl4MakORzFuNjvXPWwB4az9GwuvVxQVnjaK2g==} + engines: {node: '>= 14.18'} + peerDependencies: + '@svgdotjs/svg.js': ^3.2.4 + + '@tailwindcss/node@4.1.5': + resolution: {integrity: sha512-CBhSWo0vLnWhXIvpD0qsPephiaUYfHUX3U9anwDaHZAeuGpTiB3XmsxPAN6qX7bFhipyGBqOa1QYQVVhkOUGxg==} + + '@tailwindcss/oxide-android-arm64@4.1.5': + resolution: {integrity: sha512-LVvM0GirXHED02j7hSECm8l9GGJ1RfgpWCW+DRn5TvSaxVsv28gRtoL4aWKGnXqwvI3zu1GABeDNDVZeDPOQrw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.5': + resolution: {integrity: sha512-//TfCA3pNrgnw4rRJOqavW7XUk8gsg9ddi8cwcsWXp99tzdBAZW0WXrD8wDyNbqjW316Pk2hiN/NJx/KWHl8oA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.5': + resolution: {integrity: sha512-XQorp3Q6/WzRd9OalgHgaqgEbjP3qjHrlSUb5k1EuS1Z9NE9+BbzSORraO+ecW432cbCN7RVGGL/lSnHxcd+7Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.5': + resolution: {integrity: sha512-bPrLWbxo8gAo97ZmrCbOdtlz/Dkuy8NK97aFbVpkJ2nJ2Jo/rsCbu0TlGx8joCuA3q6vMWTSn01JY46iwG+clg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.5': + resolution: {integrity: sha512-1gtQJY9JzMAhgAfvd/ZaVOjh/Ju/nCoAsvOVJenWZfs05wb8zq+GOTnZALWGqKIYEtyNpCzvMk+ocGpxwdvaVg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.5': + resolution: {integrity: sha512-dtlaHU2v7MtdxBXoqhxwsWjav7oim7Whc6S9wq/i/uUMTWAzq/gijq1InSgn2yTnh43kR+SFvcSyEF0GCNu1PQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.5': + resolution: {integrity: sha512-fg0F6nAeYcJ3CriqDT1iVrqALMwD37+sLzXs8Rjy8Z1ZHshJoYceodfyUwGJEsQoTyWbliFNRs2wMQNXtT7MVA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.5': + resolution: {integrity: sha512-SO+F2YEIAHa1AITwc8oPwMOWhgorPzzcbhWEb+4oLi953h45FklDmM8dPSZ7hNHpIk9p/SCZKUYn35t5fjGtHA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.5': + resolution: {integrity: sha512-6UbBBplywkk/R+PqqioskUeXfKcBht3KU7juTi1UszJLx0KPXUo10v2Ok04iBJIaDPkIFkUOVboXms5Yxvaz+g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.5': + resolution: {integrity: sha512-hwALf2K9FHuiXTPqmo1KeOb83fTRNbe9r/Ixv9ZNQ/R24yw8Ge1HOWDDgTdtzntIaIUJG5dfXCf4g9AD4RiyhQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.5': + resolution: {integrity: sha512-oDKncffWzaovJbkuR7/OTNFRJQVdiw/n8HnzaCItrNQUeQgjy7oUiYpsm9HUBgpmvmDpSSbGaCa2Evzvk3eFmA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.5': + resolution: {integrity: sha512-WiR4dtyrFdbb+ov0LK+7XsFOsG+0xs0PKZKkt41KDn9jYpO7baE3bXiudPVkTqUEwNfiglCygQHl2jklvSBi7Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.5': + resolution: {integrity: sha512-1n4br1znquEvyW/QuqMKQZlBen+jxAbvyduU87RS8R3tUSvByAkcaMTkJepNIrTlYhD+U25K4iiCIxE6BGdRYA==} + engines: {node: '>= 10'} + + '@tailwindcss/vite@4.1.5': + resolution: {integrity: sha512-FE1stRoqdHSb7RxesMfCXE8icwI1W6zGE/512ae3ZDrpkQYTTYeSyUJPRCjZd8CwVAhpDUbi1YR8pcZioFJQ/w==} + peerDependencies: + vite: ^5.2.0 || ^6 + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -407,6 +714,9 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@yr/monotone-cubic-spline@1.0.3': + resolution: {integrity: sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==} + agent-base@7.1.3: resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} engines: {node: '>= 14'} @@ -415,6 +725,9 @@ packages: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} + apexcharts@4.3.0: + resolution: {integrity: sha512-PfvZQpv91T68hzry9l5zP3Gip7sQvF0nFK91uCBrswIKX7rbIdbVNS4fOks9m9yP3Ppgs6LHgU2M/mjoG4NM0A==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -434,6 +747,10 @@ packages: brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + browserslist@4.24.4: resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -445,6 +762,14 @@ packages: change-case@5.4.4: resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + chartist@1.3.1: + resolution: {integrity: sha512-pvdQowirS3f59tkIvUl4MJoWAKU/aVv1RqleKzOkXQ0nD4X9uu+7T1cCJSTTnbAvPNuAt9JpUu6+cffvZh5nHg==} + engines: {node: '>=14'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + colorette@1.4.0: resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} @@ -463,9 +788,25 @@ packages: supports-color: optional: true + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + electron-to-chromium@1.5.83: resolution: {integrity: sha512-LcUDPqSt+V0QmI47XLzZrz5OqILSMGsPFkDYus22rIbgorSvBYEFqq854ltTmUdHkY92FSdAAvsh4jWEULMdfQ==} + enhanced-resolve@5.18.1: + resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} + engines: {node: '>=10.13.0'} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -482,6 +823,10 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -495,6 +840,9 @@ packages: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + html-entities@2.3.3: resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} @@ -502,14 +850,33 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + immutable@5.0.3: + resolution: {integrity: sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==} + index-to-position@0.1.2: resolution: {integrity: sha512-MWDKS3AS1bGCHLBA2VLImJz42f7bJh8wQsTGCzI3j519/CASStoDONUBVz2I/VID0MpiX3SGSnbOD2xUalbE5g==} engines: {node: '>=18'} + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + is-what@4.1.16: resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} engines: {node: '>=12.13'} + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + js-levenshtein@1.1.6: resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} engines: {node: '>=0.10.0'} @@ -534,6 +901,70 @@ packages: engines: {node: '>=6'} hasBin: true + lightningcss-darwin-arm64@1.29.2: + resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.29.2: + resolution: {integrity: sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.29.2: + resolution: {integrity: sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.29.2: + resolution: {integrity: sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.29.2: + resolution: {integrity: sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.29.2: + resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.29.2: + resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.29.2: + resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.29.2: + resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.29.2: + resolution: {integrity: sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.29.2: + resolution: {integrity: sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==} + engines: {node: '>= 12.0.0'} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -541,10 +972,17 @@ packages: resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} engines: {node: '>=12.13'} + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + minimatch@5.1.6: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -553,6 +991,9 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -587,6 +1028,10 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -595,6 +1040,10 @@ packages: resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==} engines: {node: ^10 || ^12 || >=14} + readdirp@4.1.1: + resolution: {integrity: sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==} + engines: {node: '>= 14.18.0'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -604,6 +1053,11 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + sass@1.83.4: + resolution: {integrity: sha512-B1bozCeNQiOgDcLd33e2Cs2U60wZwjUUXzh900ZyQF5qUasvMdDZYbQ566LJu7cqR+sAHlAfO6RMkaID5s6qpA==} + engines: {node: '>=14.0.0'} + hasBin: true + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -618,6 +1072,22 @@ packages: resolution: {integrity: sha512-GURoU99ko2UiAgUC3qDCk59Jb3Ss4Po8VIMGkG8j5PFo2Q7y0YSMP8QG9NuL/fJCoTz9V1XZUbpNIMXPOfaGpA==} engines: {node: '>=10'} + solid-apexcharts@0.4.0: + resolution: {integrity: sha512-b3tjFaYNF2ggvFq+VSaxxj2ocxfKKkB7jnWL60uDaokv2A3RwFXXzmPYZMkN80FQaLnxeNzkUdre/S9HgshLnA==} + engines: {node: '>=18', pnpm: '>=8.6.0'} + peerDependencies: + apexcharts: ^4.0.0 + solid-js: ^1.6.0 + + solid-devtools@0.33.0: + resolution: {integrity: sha512-xRB4Jhgns3dBuM/s0j70BpXKy77sNjISud9xXBv60qC4cnJ/TcuVHI1t+05luj1BEKJVQSokqIaVoZWcjqA9yw==} + peerDependencies: + solid-js: ^1.9.0 + vite: ^2.2.3 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 + peerDependenciesMeta: + vite: + optional: true + solid-js@1.9.4: resolution: {integrity: sha512-ipQl8FJ31bFUoBNScDQTG3BjN6+9Rg+Q+f10bUbnO6EOTTf5NGerJeHc7wyu5I4RMHEl/WwZwUmy/PTRgxxZ8g==} @@ -634,6 +1104,17 @@ packages: resolution: {integrity: sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==} engines: {node: '>=12'} + tailwindcss@4.1.5: + resolution: {integrity: sha512-nYtSPfWGDiWgCkwQG/m+aX83XCwf62sBgg3bIlNiiOcggnS1x3uVRDAuyelBFL+vJdOPPCGElxv9DjHJjRHiVA==} + + tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -825,6 +1306,11 @@ snapshots: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.26.5 + '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/template@7.25.9': dependencies: '@babel/code-frame': 7.26.2 @@ -940,6 +1426,69 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@nothing-but/utils@0.17.0': {} + + '@parcel/watcher-android-arm64@2.5.0': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.0': + optional: true + + '@parcel/watcher-darwin-x64@2.5.0': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.0': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.0': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.0': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.0': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.0': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.0': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.0': + optional: true + + '@parcel/watcher-win32-arm64@2.5.0': + optional: true + + '@parcel/watcher-win32-ia32@2.5.0': + optional: true + + '@parcel/watcher-win32-x64@2.5.0': + optional: true + + '@parcel/watcher@2.5.0': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.0 + '@parcel/watcher-darwin-arm64': 2.5.0 + '@parcel/watcher-darwin-x64': 2.5.0 + '@parcel/watcher-freebsd-x64': 2.5.0 + '@parcel/watcher-linux-arm-glibc': 2.5.0 + '@parcel/watcher-linux-arm-musl': 2.5.0 + '@parcel/watcher-linux-arm64-glibc': 2.5.0 + '@parcel/watcher-linux-arm64-musl': 2.5.0 + '@parcel/watcher-linux-x64-glibc': 2.5.0 + '@parcel/watcher-linux-x64-musl': 2.5.0 + '@parcel/watcher-win32-arm64': 2.5.0 + '@parcel/watcher-win32-ia32': 2.5.0 + '@parcel/watcher-win32-x64': 2.5.0 + optional: true + '@redocly/ajv@8.11.2': dependencies: fast-deep-equal: 3.1.3 @@ -1022,10 +1571,201 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.30.1': optional: true + '@solid-devtools/debugger@0.26.0(solid-js@1.9.4)': + dependencies: + '@nothing-but/utils': 0.17.0 + '@solid-devtools/shared': 0.19.0(solid-js@1.9.4) + '@solid-primitives/bounds': 0.0.122(solid-js@1.9.4) + '@solid-primitives/cursor': 0.0.115(solid-js@1.9.4) + '@solid-primitives/event-listener': 2.3.3(solid-js@1.9.4) + '@solid-primitives/keyboard': 1.2.8(solid-js@1.9.4) + '@solid-primitives/platform': 0.1.2(solid-js@1.9.4) + '@solid-primitives/rootless': 1.4.5(solid-js@1.9.4) + '@solid-primitives/scheduled': 1.4.4(solid-js@1.9.4) + '@solid-primitives/static-store': 0.0.8(solid-js@1.9.4) + '@solid-primitives/utils': 6.2.3(solid-js@1.9.4) + solid-js: 1.9.4 + + '@solid-devtools/shared@0.19.0(solid-js@1.9.4)': + dependencies: + '@nothing-but/utils': 0.17.0 + '@solid-primitives/event-listener': 2.3.3(solid-js@1.9.4) + '@solid-primitives/media': 2.2.10(solid-js@1.9.4) + '@solid-primitives/refs': 1.0.8(solid-js@1.9.4) + '@solid-primitives/rootless': 1.4.5(solid-js@1.9.4) + '@solid-primitives/scheduled': 1.4.4(solid-js@1.9.4) + '@solid-primitives/static-store': 0.0.8(solid-js@1.9.4) + '@solid-primitives/styles': 0.0.114(solid-js@1.9.4) + '@solid-primitives/utils': 6.2.3(solid-js@1.9.4) + solid-js: 1.9.4 + + '@solid-primitives/bounds@0.0.122(solid-js@1.9.4)': + dependencies: + '@solid-primitives/event-listener': 2.3.3(solid-js@1.9.4) + '@solid-primitives/resize-observer': 2.0.27(solid-js@1.9.4) + '@solid-primitives/static-store': 0.0.8(solid-js@1.9.4) + '@solid-primitives/utils': 6.2.3(solid-js@1.9.4) + solid-js: 1.9.4 + + '@solid-primitives/cursor@0.0.115(solid-js@1.9.4)': + dependencies: + '@solid-primitives/utils': 6.2.3(solid-js@1.9.4) + solid-js: 1.9.4 + + '@solid-primitives/event-listener@2.3.3(solid-js@1.9.4)': + dependencies: + '@solid-primitives/utils': 6.2.3(solid-js@1.9.4) + solid-js: 1.9.4 + + '@solid-primitives/keyboard@1.2.8(solid-js@1.9.4)': + dependencies: + '@solid-primitives/event-listener': 2.3.3(solid-js@1.9.4) + '@solid-primitives/rootless': 1.4.5(solid-js@1.9.4) + '@solid-primitives/utils': 6.2.3(solid-js@1.9.4) + solid-js: 1.9.4 + + '@solid-primitives/media@2.2.10(solid-js@1.9.4)': + dependencies: + '@solid-primitives/event-listener': 2.3.3(solid-js@1.9.4) + '@solid-primitives/rootless': 1.4.5(solid-js@1.9.4) + '@solid-primitives/static-store': 0.0.9(solid-js@1.9.4) + '@solid-primitives/utils': 6.2.3(solid-js@1.9.4) + solid-js: 1.9.4 + + '@solid-primitives/platform@0.1.2(solid-js@1.9.4)': + dependencies: + solid-js: 1.9.4 + + '@solid-primitives/refs@1.0.8(solid-js@1.9.4)': + dependencies: + '@solid-primitives/utils': 6.2.3(solid-js@1.9.4) + solid-js: 1.9.4 + + '@solid-primitives/resize-observer@2.0.27(solid-js@1.9.4)': + dependencies: + '@solid-primitives/event-listener': 2.3.3(solid-js@1.9.4) + '@solid-primitives/rootless': 1.4.5(solid-js@1.9.4) + '@solid-primitives/static-store': 0.0.9(solid-js@1.9.4) + '@solid-primitives/utils': 6.2.3(solid-js@1.9.4) + solid-js: 1.9.4 + + '@solid-primitives/rootless@1.4.5(solid-js@1.9.4)': + dependencies: + '@solid-primitives/utils': 6.2.3(solid-js@1.9.4) + solid-js: 1.9.4 + + '@solid-primitives/scheduled@1.4.4(solid-js@1.9.4)': + dependencies: + solid-js: 1.9.4 + + '@solid-primitives/static-store@0.0.8(solid-js@1.9.4)': + dependencies: + '@solid-primitives/utils': 6.2.3(solid-js@1.9.4) + solid-js: 1.9.4 + + '@solid-primitives/static-store@0.0.9(solid-js@1.9.4)': + dependencies: + '@solid-primitives/utils': 6.2.3(solid-js@1.9.4) + solid-js: 1.9.4 + + '@solid-primitives/styles@0.0.114(solid-js@1.9.4)': + dependencies: + '@solid-primitives/rootless': 1.4.5(solid-js@1.9.4) + '@solid-primitives/utils': 6.2.3(solid-js@1.9.4) + solid-js: 1.9.4 + + '@solid-primitives/utils@6.2.3(solid-js@1.9.4)': + dependencies: + solid-js: 1.9.4 + '@solidjs/router@0.15.3(solid-js@1.9.4)': dependencies: solid-js: 1.9.4 + '@svgdotjs/svg.draggable.js@3.0.5(@svgdotjs/svg.js@3.2.4)': + dependencies: + '@svgdotjs/svg.js': 3.2.4 + + '@svgdotjs/svg.filter.js@3.0.8': + dependencies: + '@svgdotjs/svg.js': 3.2.4 + + '@svgdotjs/svg.js@3.2.4': {} + + '@svgdotjs/svg.resize.js@2.0.5(@svgdotjs/svg.js@3.2.4)(@svgdotjs/svg.select.js@4.0.2(@svgdotjs/svg.js@3.2.4))': + dependencies: + '@svgdotjs/svg.js': 3.2.4 + '@svgdotjs/svg.select.js': 4.0.2(@svgdotjs/svg.js@3.2.4) + + '@svgdotjs/svg.select.js@4.0.2(@svgdotjs/svg.js@3.2.4)': + dependencies: + '@svgdotjs/svg.js': 3.2.4 + + '@tailwindcss/node@4.1.5': + dependencies: + enhanced-resolve: 5.18.1 + jiti: 2.4.2 + lightningcss: 1.29.2 + tailwindcss: 4.1.5 + + '@tailwindcss/oxide-android-arm64@4.1.5': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.5': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.5': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.5': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.5': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.5': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.5': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.5': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.5': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.5': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.5': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.5': + optional: true + + '@tailwindcss/oxide@4.1.5': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.5 + '@tailwindcss/oxide-darwin-arm64': 4.1.5 + '@tailwindcss/oxide-darwin-x64': 4.1.5 + '@tailwindcss/oxide-freebsd-x64': 4.1.5 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.5 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.5 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.5 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.5 + '@tailwindcss/oxide-linux-x64-musl': 4.1.5 + '@tailwindcss/oxide-wasm32-wasi': 4.1.5 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.5 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.5 + + '@tailwindcss/vite@4.1.5(vite@6.0.7(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.83.4))': + dependencies: + '@tailwindcss/node': 4.1.5 + '@tailwindcss/oxide': 4.1.5 + tailwindcss: 4.1.5 + vite: 6.0.7(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.83.4) + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.26.5 @@ -1049,10 +1789,21 @@ snapshots: '@types/estree@1.0.6': {} + '@yr/monotone-cubic-spline@1.0.3': {} + agent-base@7.1.3: {} ansi-colors@4.1.3: {} + apexcharts@4.3.0: + dependencies: + '@svgdotjs/svg.draggable.js': 3.0.5(@svgdotjs/svg.js@3.2.4) + '@svgdotjs/svg.filter.js': 3.0.8 + '@svgdotjs/svg.js': 3.2.4 + '@svgdotjs/svg.resize.js': 2.0.5(@svgdotjs/svg.js@3.2.4)(@svgdotjs/svg.select.js@4.0.2(@svgdotjs/svg.js@3.2.4)) + '@svgdotjs/svg.select.js': 4.0.2(@svgdotjs/svg.js@3.2.4) + '@yr/monotone-cubic-spline': 1.0.3 + argparse@2.0.1: {} babel-plugin-jsx-dom-expressions@0.39.5(@babel/core@7.26.0): @@ -1076,6 +1827,11 @@ snapshots: dependencies: balanced-match: 1.0.2 + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + optional: true + browserslist@4.24.4: dependencies: caniuse-lite: 1.0.30001692 @@ -1087,6 +1843,13 @@ snapshots: change-case@5.4.4: {} + chartist@1.3.1: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.1 + optional: true + colorette@1.4.0: {} convert-source-map@2.0.0: {} @@ -1099,8 +1862,20 @@ snapshots: optionalDependencies: supports-color: 9.4.0 + defu@6.1.4: {} + + detect-libc@1.0.3: + optional: true + + detect-libc@2.0.4: {} + electron-to-chromium@1.5.83: {} + enhanced-resolve@5.18.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + entities@4.5.0: {} esbuild@0.24.2: @@ -1135,6 +1910,11 @@ snapshots: fast-deep-equal@3.1.3: {} + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + optional: true + fsevents@2.3.3: optional: true @@ -1142,6 +1922,8 @@ snapshots: globals@11.12.0: {} + graceful-fs@4.2.11: {} + html-entities@2.3.3: {} https-proxy-agent@7.0.6(supports-color@9.4.0): @@ -1151,10 +1933,26 @@ snapshots: transitivePeerDependencies: - supports-color + immutable@5.0.3: + optional: true + index-to-position@0.1.2: {} + is-extglob@2.1.1: + optional: true + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + optional: true + + is-number@7.0.0: + optional: true + is-what@4.1.16: {} + jiti@2.4.2: {} + js-levenshtein@1.1.6: {} js-tokens@4.0.0: {} @@ -1169,6 +1967,51 @@ snapshots: json5@2.2.3: {} + lightningcss-darwin-arm64@1.29.2: + optional: true + + lightningcss-darwin-x64@1.29.2: + optional: true + + lightningcss-freebsd-x64@1.29.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.29.2: + optional: true + + lightningcss-linux-arm64-gnu@1.29.2: + optional: true + + lightningcss-linux-arm64-musl@1.29.2: + optional: true + + lightningcss-linux-x64-gnu@1.29.2: + optional: true + + lightningcss-linux-x64-musl@1.29.2: + optional: true + + lightningcss-win32-arm64-msvc@1.29.2: + optional: true + + lightningcss-win32-x64-msvc@1.29.2: + optional: true + + lightningcss@1.29.2: + dependencies: + detect-libc: 2.0.4 + optionalDependencies: + lightningcss-darwin-arm64: 1.29.2 + lightningcss-darwin-x64: 1.29.2 + lightningcss-freebsd-x64: 1.29.2 + lightningcss-linux-arm-gnueabihf: 1.29.2 + lightningcss-linux-arm64-gnu: 1.29.2 + lightningcss-linux-arm64-musl: 1.29.2 + lightningcss-linux-x64-gnu: 1.29.2 + lightningcss-linux-x64-musl: 1.29.2 + lightningcss-win32-arm64-msvc: 1.29.2 + lightningcss-win32-x64-msvc: 1.29.2 + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -1177,14 +2020,25 @@ snapshots: dependencies: is-what: 4.1.16 + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + optional: true + minimatch@5.1.6: dependencies: brace-expansion: 2.0.1 + moment@2.30.1: {} + ms@2.1.3: {} nanoid@3.3.8: {} + node-addon-api@7.1.1: + optional: true + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 @@ -1221,6 +2075,9 @@ snapshots: picocolors@1.1.1: {} + picomatch@2.3.1: + optional: true + pluralize@8.0.0: {} postcss@8.5.1: @@ -1229,6 +2086,9 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + readdirp@4.1.1: + optional: true + require-from-string@2.0.2: {} rollup@4.30.1: @@ -1256,6 +2116,15 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.30.1 fsevents: 2.3.3 + sass@1.83.4: + dependencies: + chokidar: 4.0.3 + immutable: 5.0.3 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.0 + optional: true + semver@6.3.1: {} seroval-plugins@1.2.0(seroval@1.2.0): @@ -1264,6 +2133,25 @@ snapshots: seroval@1.2.0: {} + solid-apexcharts@0.4.0(apexcharts@4.3.0)(solid-js@1.9.4): + dependencies: + apexcharts: 4.3.0 + defu: 6.1.4 + solid-js: 1.9.4 + + solid-devtools@0.33.0(solid-js@1.9.4)(vite@6.0.7(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.83.4)): + dependencies: + '@babel/core': 7.26.0 + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) + '@babel/types': 7.26.5 + '@solid-devtools/debugger': 0.26.0(solid-js@1.9.4) + '@solid-devtools/shared': 0.19.0(solid-js@1.9.4) + solid-js: 1.9.4 + optionalDependencies: + vite: 6.0.7(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.83.4) + transitivePeerDependencies: + - supports-color + solid-js@1.9.4: dependencies: csstype: 3.1.3 @@ -1283,6 +2171,15 @@ snapshots: supports-color@9.4.0: {} + tailwindcss@4.1.5: {} + + tapable@2.2.1: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + optional: true + tr46@0.0.3: {} type-fest@4.32.0: {} @@ -1299,7 +2196,7 @@ snapshots: validate-html-nesting@1.2.2: {} - vite-plugin-solid@2.11.0(solid-js@1.9.4)(vite@6.0.7): + vite-plugin-solid@2.11.0(solid-js@1.9.4)(vite@6.0.7(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.83.4)): dependencies: '@babel/core': 7.26.0 '@types/babel__core': 7.20.5 @@ -1307,22 +2204,25 @@ snapshots: merge-anything: 5.1.7 solid-js: 1.9.4 solid-refresh: 0.6.3(solid-js@1.9.4) - vite: 6.0.7 - vitefu: 1.0.5(vite@6.0.7) + vite: 6.0.7(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.83.4) + vitefu: 1.0.5(vite@6.0.7(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.83.4)) transitivePeerDependencies: - supports-color - vite@6.0.7: + vite@6.0.7(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.83.4): dependencies: esbuild: 0.24.2 postcss: 8.5.1 rollup: 4.30.1 optionalDependencies: fsevents: 2.3.3 + jiti: 2.4.2 + lightningcss: 1.29.2 + sass: 1.83.4 - vitefu@1.0.5(vite@6.0.7): + vitefu@1.0.5(vite@6.0.7(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.83.4)): optionalDependencies: - vite: 6.0.7 + vite: 6.0.7(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.83.4) webidl-conversions@3.0.1: {} diff --git a/server/frontend/pnpm-workspace.yaml b/server/frontend/pnpm-workspace.yaml new file mode 100644 index 0000000..012c404 --- /dev/null +++ b/server/frontend/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +onlyBuiltDependencies: + - '@parcel/watcher' + - esbuild diff --git a/server/frontend/src/Analysis.tsx b/server/frontend/src/Analysis.tsx new file mode 100644 index 0000000..3317f68 --- /dev/null +++ b/server/frontend/src/Analysis.tsx @@ -0,0 +1,137 @@ +import { createAsync, useParams } from "@solidjs/router" +import { client, getAnalysisList, paths } from "./api.ts"; +import { createSignal, For, onMount, Show, Suspense } from "solid-js"; +import { SolidApexCharts } from "solid-apexcharts"; +import { BarChart, times } from "chartist"; + +type AnalysisResult = + { status: 'not requested' } + | { status: 'loading' } + | { status: 'loaded', result: paths['/analysis/execute']['get']['responses'][200]['content']['application/json'] } + +export default function Analysis() { + const pathParams = useParams(); + const analysisId = pathParams.id!; + let analysis = createAsync(() => getAnalysisList()); + const analysisName = () => analysis()?.data?.find(it => it.id == analysisId)?.name + const [startTimestamp, setStartTimestamp] = createSignal(new Date().getTime() - 1000 * 60 * 60 * 24 * 356); + const [endTimestamp, setEndTimestamp] = createSignal(new Date().getTime()); + const [analysisResult, setAnalysisResult] = createSignal<AnalysisResult>({ status: 'not requested' }); + return <> + <h1 class="text-xl"><Suspense fallback="Name not loaded...">{analysisName()}</Suspense></h1> + <p> + <label> + Start: + <input type="date" value={new Date(startTimestamp()).toISOString().substring(0, 10)} onInput={it => setStartTimestamp(it.target.valueAsNumber)}></input> + </label> + <label> + End: + <input type="date" value={new Date(endTimestamp()).toISOString().substring(0, 10)} onInput={it => setEndTimestamp(it.target.valueAsNumber)}></input> + </label> + <button disabled={analysisResult().status === 'loading'} onClick={() => { + setAnalysisResult({ status: 'loading' }); + (async () => { + const result = await client.GET('/analysis/execute', { + params: { + query: { + analysis: analysisId, + tEnd: endTimestamp(), + tStart: startTimestamp() + } + } + }); + setAnalysisResult({ + status: "loaded", + result: result.data! + }); + })(); + }}> + Refresh + </button> + + <Show when={takeIf(analysisResult(), it => it.status == 'loaded')}> + {element => + <For each={element().result.visualizations}> + {item => + <div class="h-300 max-h-[90vh]"> + <SolidApexCharts + type="bar" + width={"100%"} + height={"100%"} + options={{ + colors: ['#b03060'], + xaxis: { + labels: { + style: { + colors: '#A0A0A0', + }, + formatter(value, timestamp, opts) { + return formatDate(timestamp!) + }, + }, + type: 'numeric', + }, + tooltip: { + enabled: false + }, + yaxis: { + labels: { + style: { + colors: '#A0A0A0' + }, + formatter(val, opts) { + return formatMoney(val) + } + }, + decimalsInFloat: 3 + }, + dataLabels: { + formatter(val, opts) { + return formatMoney(val as number) + }, + } + }} + series={[ + { + name: item.label, + data: item.dataPoints.map(it => ([it.time, it.value])) + } + ]} + ></SolidApexCharts> + </div> + } + </For>} + </Show> + </p > + </> +} + +const formatMoney = (money: number): string => { + if (money < 0) return `-${formatMoney(money)}` + const moneyNames = [ + [1_000_000_000, 'b'], + [1_000_000, 'm'], + [1_000, 'k'], + [1, ''] + ] as const; + + for (const [factor, name] of moneyNames) { + if (money >= factor) { + const scaledValue = Math.round(money / factor * 10) / 10 + return `${scaledValue}${name}` + } + } + return money.toString() +} + +const formatDate = (date: number | Date) => { + const _date = new Date(date); + return `${_date.getDay()}.${_date.getMonth() + 1}.${_date.getFullYear()}` +} + +function takeIf<T extends P, P>( + obj: P, + condition: (arg: P) => arg is T, +): T | false { + return condition(obj) ? obj : false; +}
\ No newline at end of file diff --git a/server/frontend/src/App.tsx b/server/frontend/src/App.tsx index e35bb42..bdc1007 100644 --- a/server/frontend/src/App.tsx +++ b/server/frontend/src/App.tsx @@ -1,11 +1,20 @@ -import type { Component } from "solid-js"; -import { A } from "@solidjs/router"; +import { For, Suspense, type Component } from "solid-js"; +import { A, createAsync } from "@solidjs/router"; +import { client, getAnalysisList } from "./api.ts"; const App: Component = () => { + let analysis = createAsync(() => getAnalysisList()); return ( <> - Hello World - <A href="/test">Test Page</A> + <Suspense fallback="Loading analysis..."> + <ul> + <For each={analysis()?.data}> + {item => + <li><A href={`/analysis/${item.id}`}>{item.name}</A></li> + } + </For> + </ul> + </Suspense> </> ); }; diff --git a/server/frontend/src/api-schema.d.ts b/server/frontend/src/api-schema.d.ts index 93bedc9..7ba1db4 100644 --- a/server/frontend/src/api-schema.d.ts +++ b/server/frontend/src/api-schema.d.ts @@ -4,95 +4,233 @@ */ export interface paths { - "/profiles": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; + "/profiles": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List all profiles and players known to ledger */ + get: operations["listProfiles"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/analysis/execute": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Execute an analysis on a given timeframe */ + get: operations["executeAnalysis"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/analysis/list": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List all installed analysis */ + get: operations["getAnalysis"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/item": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get item names for item ids */ + get: operations["getItemNames"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - /** List all profiles and players known to ledger */ - get: operations["listProfiles"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/item": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; + "/entries": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get all log entries */ + get: operations["getLogEntries"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - /** Get item names for item ids */ - get: operations["getItemNames"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; } export type webhooks = Record<string, never>; export interface components { - schemas: never; - responses: never; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; + schemas: never; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; } export type $defs = Record<string, never>; export interface operations { - listProfiles: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; + listProfiles: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + playerId: string; + profileId: string; + }[]; + }; + }; + }; }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; + executeAnalysis: { + parameters: { + query: { + /** @description An analysis id obtained from getAnalysis */ + analysis: string; + /** @description The start of the timeframe to analyze */ + tStart: number; + /** @description The end of the timeframe to analyze. Make sure to use the end of the day if you want the entire day included. */ + tEnd: number; + }; + header?: never; + path?: never; + cookie?: never; }; - content: { - "application/json": { - playerId: string; - profileId: string; - }[]; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + visualizations: { + label: string; + xLabel: string; + yLabel: string; + dataPoints: { + time: number; + value: number; + }[]; + }[]; + }; + }; + }; }; - }; }; - }; - getItemNames: { - parameters: { - query?: { - itemId?: string[]; - }; - header?: never; - path?: never; - cookie?: never; + getAnalysis: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + name: string; + id: string; + }[]; + }; + }; + }; + }; + getItemNames: { + parameters: { + query: { + itemId: string[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + [key: string]: string; + }; + }; + }; + }; }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; + getLogEntries: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - content: { - "application/json": { - [key: string]: string; - }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @enum {string} */ + type: "ACCESSORIES_SWAPPING" | "ALLOWANCE_GAIN" | "AUCTION_BOUGHT" | "AUCTION_LISTING_CHARGE" | "AUCTION_SOLD" | "AUTOMERCHANT_PROFIT_COLLECT" | "BANK_DEPOSIT" | "BANK_INTEREST" | "BANK_WITHDRAW" | "BASIC_REFORGE" | "BAZAAR_BUY_INSTANT" | "BAZAAR_BUY_ORDER" | "BAZAAR_SELL_INSTANT" | "BAZAAR_SELL_ORDER" | "BITS_PURSE_STATUS" | "BOOSTER_COOKIE_ATE" | "CAPSAICIN_EYEDROPS_USED" | "COMMUNITY_SHOP_BUY" | "CORPSE_DESECRATED" | "DIE_ROLLED" | "DRACONIC_SACRIFICE" | "DUNGEON_CHEST_OPEN" | "FORGED" | "GOD_POTION_DRANK" | "GOD_POTION_MIXIN_DRANK" | "GUMMY_POLAR_BEAR_ATE" | "KAT_TIMESKIP" | "KAT_UPGRADE" | "KISMET_REROLL" | "KUUDRA_CHEST_OPEN" | "NPC_BUY" | "NPC_SELL" | "PEST_REPELLENT_USED" | "VISITOR_BARGAIN" | "WYRM_EVOKED"; + id: string; + items: { + itemId: string; + /** @enum {string} */ + direction: "GAINED" | "TRANSFORM" | "SYNC" | "CATALYST" | "LOST"; + amount: number; + }[]; + }[]; + }; + }; }; - }; }; - }; } diff --git a/server/frontend/src/api.ts b/server/frontend/src/api.ts index 22cf6ca..8ab6272 100644 --- a/server/frontend/src/api.ts +++ b/server/frontend/src/api.ts @@ -1,6 +1,13 @@ import createClient from "openapi-fetch"; import type { paths } from "./api-schema.js"; +import { query } from "@solidjs/router"; +export { type paths }; const apiRoot = import.meta.env.DEV ? "//localhost:8080/api" : "/api"; export const client = createClient<paths>({ baseUrl: apiRoot }); + +export const getAnalysisList = query( + () => client.GET("/analysis/list"), + "getAnalysisList" +) diff --git a/server/frontend/src/index.css b/server/frontend/src/index.css index 4a1df4d..e10ddff 100644 --- a/server/frontend/src/index.css +++ b/server/frontend/src/index.css @@ -1,3 +1,6 @@ +@import "tailwindcss"; + + body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", @@ -10,4 +13,4 @@ body { code { font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; -} +}
\ No newline at end of file diff --git a/server/frontend/src/index.tsx b/server/frontend/src/index.tsx index 6ab7d4e..3009300 100644 --- a/server/frontend/src/index.tsx +++ b/server/frontend/src/index.tsx @@ -1,10 +1,11 @@ /* @refresh reload */ import { render } from "solid-js/web"; +import 'solid-devtools'; import "./index.css"; import type { RouteDefinition } from "@solidjs/router"; import { Router } from "@solidjs/router"; -import { lazy } from "solid-js"; +import { lazy, onMount } from "solid-js"; const root = document.getElementById("root"); @@ -16,6 +17,14 @@ if (!(root instanceof HTMLElement)) { const routes: Array<RouteDefinition> = [ { path: "/", component: lazy(() => import("./App.tsx")) }, { path: "/test/", component: lazy(() => import("./Test.tsx")) }, + { path: "/analysis/:id", component: lazy(() => import("./Analysis.tsx")) }, ]; -render(() => <Router>{routes}</Router>, root!); +const Root = () => { + + return <div class="bg-gray-800 text-white min-h-[100vh]"> + <Router>{routes}</Router> + </div> +} + +render(() => <Root />, root!); diff --git a/server/frontend/tsconfig.json b/server/frontend/tsconfig.json index 73eb483..548d331 100644 --- a/server/frontend/tsconfig.json +++ b/server/frontend/tsconfig.json @@ -6,8 +6,8 @@ "isolatedModules": true, "jsx": "preserve", "jsxImportSource": "solid-js", - "module": "ESNext", - "moduleResolution": "NodeNext", + "module": "Preserve", + "moduleResolution": "bundler", "noEmit": true, "noUncheckedIndexedAccess": true, "strict": true, diff --git a/server/frontend/vite.config.ts b/server/frontend/vite.config.ts index 9ff59a1..b356c7e 100644 --- a/server/frontend/vite.config.ts +++ b/server/frontend/vite.config.ts @@ -1,8 +1,15 @@ import { defineConfig } from 'vite'; import solidPlugin from 'vite-plugin-solid'; - +import tailwindcss from '@tailwindcss/vite'; +import devtools from 'solid-devtools/vite'; export default defineConfig({ - plugins: [solidPlugin()], + plugins: [ + solidPlugin(), + tailwindcss(), + devtools({ + autoname: true + }) + ], server: { port: 3000, }, diff --git a/server/swagger/src/main/kotlin/moe/nea/ledger/server/core/api/OpenApiModel.kt b/server/swagger/src/main/kotlin/moe/nea/ledger/server/core/api/OpenApiModel.kt index 8392c5c..af9d3f4 100644 --- a/server/swagger/src/main/kotlin/moe/nea/ledger/server/core/api/OpenApiModel.kt +++ b/server/swagger/src/main/kotlin/moe/nea/ledger/server/core/api/OpenApiModel.kt @@ -52,6 +52,7 @@ data class OpenApiParameter( val name: String, val description: String, val schema: JsonSchema?, + val required: Boolean = true, ) @Serializable diff --git a/settings.gradle.kts b/settings.gradle.kts index 949cbe4..0de6ae1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,7 +10,7 @@ pluginManagement { maven("https://maven.architectury.dev/") maven("https://maven.minecraftforge.net/") maven("https://repo.spongepowered.org/maven/") - maven("https://repo.sk1er.club/repository/maven-releases/") + maven("https://repo.essential.gg/repository/maven-releases/") } resolutionStrategy { eachPlugin { @@ -18,7 +18,8 @@ pluginManagement { "gg.essential.loom" -> useModule("gg.essential:architectury-loom:${requested.version}") } } - }} + } +} plugins { id("org.gradle.toolchains.foojay-resolver-convention") version ("0.8.0") @@ -35,4 +36,5 @@ include("server:swagger") include("server:core") include("server:frontend") include("server:aio") +include("server:analysis") includeBuild("build-src") |