summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/FundLab.Api/App.fs53
-rw-r--r--src/FundLab.Api/Authentication.fs25
-rw-r--r--src/FundLab.Api/FundLab.Api.fsproj20
-rw-r--r--src/FundLab.Api/Health.fs16
-rw-r--r--src/FundLab.Api/Program.fs15
-rw-r--r--src/FundLab.Domain/Domain.fs60
-rw-r--r--src/FundLab.Domain/FundLab.Domain.fsproj13
-rw-r--r--src/FundLab.Domain/Ledger.fs921
-rw-r--r--src/FundLab.Domain/Performance.fs51
-rw-r--r--src/FundLab.Web/App.fs41
-rw-r--r--src/FundLab.Web/FundLab.Web.fsproj17
-rw-r--r--src/FundLab.Web/index.html12
-rw-r--r--src/FundLab.Web/package-lock.json1581
-rw-r--r--src/FundLab.Web/package.json17
-rw-r--r--src/FundLab.Web/src/main.js3
-rw-r--r--src/FundLab.Web/vite.config.js8
-rw-r--r--src/FundLab.Worker/FundLab.Worker.fsproj18
-rw-r--r--src/FundLab.Worker/Program.fs12
-rw-r--r--src/FundLab.Worker/Worker.fs15
19 files changed, 2898 insertions, 0 deletions
diff --git a/src/FundLab.Api/App.fs b/src/FundLab.Api/App.fs
new file mode 100644
index 0000000..44628ab
--- /dev/null
+++ b/src/FundLab.Api/App.fs
@@ -0,0 +1,53 @@
+namespace FundLab.Api
+
+open System
+open Giraffe
+open Microsoft.AspNetCore.Http
+
+type EmptyPortfolioResponse =
+ {
+ status: string
+ message: string
+ }
+
+module App =
+ let private unauthorized : HttpHandler =
+ setStatusCode 401
+ >=> setHttpHeader "WWW-Authenticate" "Bearer"
+ >=> text "Unauthorized"
+
+ let private requireBearer (next: HttpFunc) (ctx: HttpContext) =
+ let expectedToken =
+ Environment.GetEnvironmentVariable("FUND_LAB_AUTH_TOKEN")
+ |> Option.ofObj
+ |> Option.defaultValue ""
+
+ let authorizationHeader = ctx.Request.Headers.Authorization.ToString()
+
+ match Authentication.authorize expectedToken authorizationHeader with
+ | AuthDecision.Authorized -> next ctx
+ | AuthDecision.Unauthorized -> unauthorized next ctx
+
+ let private health : HttpHandler =
+ json ({
+ service = "fund-lab-api"
+ status = "ok"
+ } : HealthResponse)
+
+ let private emptyPortfolio : HttpHandler =
+ json {
+ status = "empty"
+ message = "尚未创建基金/尚未选择投资"
+ }
+
+ let createApplication () : HttpHandler =
+ choose [
+ GET >=> route "/health" >=> health
+ subRoute "/api" (
+ requireBearer
+ >=> choose [
+ GET >=> route "/portfolio/summary" >=> emptyPortfolio
+ ]
+ )
+ setStatusCode 404 >=> text "Not Found"
+ ]
diff --git a/src/FundLab.Api/Authentication.fs b/src/FundLab.Api/Authentication.fs
new file mode 100644
index 0000000..e98d71e
--- /dev/null
+++ b/src/FundLab.Api/Authentication.fs
@@ -0,0 +1,25 @@
+namespace FundLab.Api
+
+open System
+open System.Security.Cryptography
+open System.Text
+
+type AuthDecision =
+ | Authorized
+ | Unauthorized
+
+module Authentication =
+ let authorize (expectedToken: string) (authorizationHeader: string) : AuthDecision =
+ if String.IsNullOrWhiteSpace expectedToken
+ || String.IsNullOrWhiteSpace authorizationHeader
+ || not (authorizationHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) then
+ AuthDecision.Unauthorized
+ else
+ let presentedToken = authorizationHeader.Substring("Bearer ".Length)
+ let expectedBytes = Encoding.UTF8.GetBytes expectedToken
+ let presentedBytes = Encoding.UTF8.GetBytes presentedToken
+
+ if CryptographicOperations.FixedTimeEquals(expectedBytes, presentedBytes) then
+ AuthDecision.Authorized
+ else
+ AuthDecision.Unauthorized
diff --git a/src/FundLab.Api/FundLab.Api.fsproj b/src/FundLab.Api/FundLab.Api.fsproj
new file mode 100644
index 0000000..81952df
--- /dev/null
+++ b/src/FundLab.Api/FundLab.Api.fsproj
@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+ <PropertyGroup>
+ <TargetFramework>net8.0</TargetFramework>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+ <ItemGroup>
+ <PackageReference Include="Giraffe" Version="6.0.0" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\FundLab.Domain\FundLab.Domain.fsproj" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Authentication.fs" />
+ <Compile Include="Health.fs" />
+ <Compile Include="App.fs" />
+ <Compile Include="Program.fs" />
+ </ItemGroup>
+</Project>
diff --git a/src/FundLab.Api/Health.fs b/src/FundLab.Api/Health.fs
new file mode 100644
index 0000000..719446f
--- /dev/null
+++ b/src/FundLab.Api/Health.fs
@@ -0,0 +1,16 @@
+namespace FundLab.Api
+
+open System.Text.Json
+
+type HealthResponse =
+ {
+ service: string
+ status: string
+ }
+
+module Health =
+ let payload (serviceName: string) : string =
+ JsonSerializer.Serialize<HealthResponse> {
+ service = serviceName
+ status = "ok"
+ }
diff --git a/src/FundLab.Api/Program.fs b/src/FundLab.Api/Program.fs
new file mode 100644
index 0000000..d5cc0b2
--- /dev/null
+++ b/src/FundLab.Api/Program.fs
@@ -0,0 +1,15 @@
+module FundLab.Api.Program
+
+open Giraffe
+open Microsoft.AspNetCore.Builder
+open Microsoft.Extensions.DependencyInjection
+
+[<EntryPoint>]
+let main argv =
+ let builder = WebApplication.CreateBuilder(argv)
+ builder.Services.AddGiraffe() |> ignore
+
+ let app = builder.Build()
+ app.UseGiraffe(App.createApplication())
+ app.Run()
+ 0
diff --git a/src/FundLab.Domain/Domain.fs b/src/FundLab.Domain/Domain.fs
new file mode 100644
index 0000000..9c42d5a
--- /dev/null
+++ b/src/FundLab.Domain/Domain.fs
@@ -0,0 +1,60 @@
+namespace FundLab.Domain
+
+open System
+
+type FundStatus =
+ | Empty
+ | Active
+ | ZeroUnits
+
+type UnitNavStatus =
+ | HasValue of decimal
+ | ZeroUnits
+
+type Fund =
+ {
+ Id: Guid
+ Name: string
+ Currency: string
+ InitialCash: decimal
+ InitialUnitNav: decimal
+ NetAssets: decimal
+ OutstandingUnits: decimal
+ Status: FundStatus
+ }
+
+module Fund =
+ let create (name: string) (initialCash: decimal) (initialUnitNav: decimal) : Fund =
+ {
+ Id = Guid.NewGuid()
+ Name = name
+ Currency = "CNY"
+ InitialCash = initialCash
+ InitialUnitNav = initialUnitNav
+ NetAssets = initialCash
+ OutstandingUnits = 0m
+ Status = FundStatus.Empty
+ }
+
+ let unitNavStatus (fund: Fund) : UnitNavStatus =
+ if fund.OutstandingUnits = 0m then
+ UnitNavStatus.ZeroUnits
+ else
+ UnitNavStatus.HasValue (fund.NetAssets / fund.OutstandingUnits)
+
+type OrderStatus =
+ | Submitted
+ | CashFrozen
+ | UnitsFrozen
+ | PendingNav
+ | Confirmed
+ | Settled
+ | Cancelled
+ | Failed
+
+module OrderStatus =
+ let isConfirmed status =
+ match status with
+ | OrderStatus.Confirmed
+ | OrderStatus.Settled -> true
+ | _ -> false
diff --git a/src/FundLab.Domain/FundLab.Domain.fsproj b/src/FundLab.Domain/FundLab.Domain.fsproj
new file mode 100644
index 0000000..9e46c00
--- /dev/null
+++ b/src/FundLab.Domain/FundLab.Domain.fsproj
@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <TargetFramework>net8.0</TargetFramework>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+ <ItemGroup>
+ <Compile Include="Domain.fs" />
+ <Compile Include="Ledger.fs" />
+ <Compile Include="Performance.fs" />
+ </ItemGroup>
+</Project>
diff --git a/src/FundLab.Domain/Ledger.fs b/src/FundLab.Domain/Ledger.fs
new file mode 100644
index 0000000..2937ca6
--- /dev/null
+++ b/src/FundLab.Domain/Ledger.fs
@@ -0,0 +1,921 @@
+namespace FundLab.Domain
+
+open System
+
+type ResidualOwner =
+ | FundReserve
+ | ExternalParty
+
+type ResidualState =
+ | Clear
+ | RequiresAllocation of decimal
+ | Allocated of decimal * ResidualOwner
+
+type Holding =
+ {
+ InstrumentId: Guid
+ AvailableUnits: decimal
+ FrozenUnits: decimal
+ SettledValue: decimal
+ }
+
+type BalanceSheet =
+ {
+ AvailableCash: decimal
+ FrozenCash: decimal
+ SettledHoldingsValue: decimal
+ RedemptionReceivable: decimal
+ OtherInTransitAssets: decimal
+ Assets: decimal
+ RedemptionPayable: decimal
+ SubscriptionRefundPayable: decimal
+ FeePayable: decimal
+ OtherInTransitLiabilities: decimal
+ Liabilities: decimal
+ NetAssets: decimal
+ }
+
+type CashFlowKind =
+ | ExternalDeposit
+ | ExternalRedemption
+ | ExternalRedemptionPayment
+ | UnderlyingPurchase
+ | UnderlyingRedemption
+ | UnderlyingRedemptionReceipt
+
+type CashFlow =
+ {
+ TransactionId: Guid
+ FundId: Guid
+ Kind: CashFlowKind
+ EffectiveAt: DateTimeOffset
+ GrossCash: decimal
+ CreditedCash: decimal
+ ConvertedUnits: decimal
+ UnitNav: decimal option
+ RoundingResidual: decimal
+ ResidualOwner: ResidualOwner option
+ IsSynthetic: bool
+ }
+
+type ExternalRedemptionStatus =
+ | PendingPayment
+ | Paid
+
+type ExternalRedemptionRecord =
+ {
+ TransactionId: Guid
+ FundId: Guid
+ IdempotencyKey: string
+ Units: decimal
+ UnitNav: decimal
+ PayableCash: decimal
+ Status: ExternalRedemptionStatus
+ ConfirmedAt: DateTimeOffset
+ PaidAt: DateTimeOffset option
+ IsSynthetic: bool
+ }
+
+type ResidualAllocationRecord =
+ {
+ TransactionId: Guid
+ FundId: Guid
+ Amount: decimal
+ Owner: ResidualOwner
+ EffectiveAt: DateTimeOffset
+ IsSynthetic: bool
+ }
+
+type LedgerEvent =
+ {
+ TransactionId: Guid
+ FundId: Guid
+ EventType: string
+ EffectiveAt: DateTimeOffset
+ CashAmount: decimal
+ Units: decimal
+ RoundingResidual: decimal
+ ResidualOwner: ResidualOwner option
+ IsSynthetic: bool
+ }
+
+type LedgerOrderKind =
+ | Purchase
+ | Redemption
+
+type LedgerOrder =
+ {
+ Id: Guid
+ FundId: Guid
+ InstrumentId: Guid
+ Kind: LedgerOrderKind
+ IdempotencyKey: string
+ Status: OrderStatus
+ RequestedCash: decimal
+ RequestedUnits: decimal
+ ConfirmedCash: decimal
+ ConfirmedUnits: decimal
+ RoundingResidual: decimal
+ ResidualOwner: ResidualOwner option
+ IsSynthetic: bool
+ SubmittedAt: DateTimeOffset
+ ConfirmedAt: DateTimeOffset option
+ }
+
+type LedgerFund =
+ {
+ Id: Guid
+ Name: string
+ Currency: string
+ InitialCash: decimal
+ InitialUnitNav: decimal
+ IsSynthetic: bool
+ AvailableCash: decimal
+ FrozenCash: decimal
+ Holdings: Map<Guid, Holding>
+ OutstandingUnits: decimal
+ RedemptionPayable: decimal
+ RedemptionReceivable: decimal
+ OtherInTransitAssets: decimal
+ SubscriptionRefundPayable: decimal
+ FeePayable: decimal
+ OtherInTransitLiabilities: decimal
+ Status: FundStatus
+ Orders: Map<Guid, LedgerOrder>
+ ExternalRedemptions: Map<Guid, ExternalRedemptionRecord>
+ CashFlows: CashFlow list
+ Events: LedgerEvent list
+ ResidualAllocations: ResidualAllocationRecord list
+ ResidualState: ResidualState
+ }
+ with
+ member this.SettledHoldingsValue =
+ this.Holdings
+ |> Map.toSeq
+ |> Seq.sumBy (fun (_, holding) -> holding.SettledValue)
+
+ member this.balanceSheet =
+ let settledHoldingsValue = this.SettledHoldingsValue
+ let assets =
+ this.AvailableCash
+ + this.FrozenCash
+ + settledHoldingsValue
+ + this.RedemptionReceivable
+ + this.OtherInTransitAssets
+
+ let liabilities =
+ this.RedemptionPayable
+ + this.SubscriptionRefundPayable
+ + this.FeePayable
+ + this.OtherInTransitLiabilities
+
+ {
+ AvailableCash = this.AvailableCash
+ FrozenCash = this.FrozenCash
+ SettledHoldingsValue = settledHoldingsValue
+ RedemptionReceivable = this.RedemptionReceivable
+ OtherInTransitAssets = this.OtherInTransitAssets
+ Assets = assets
+ RedemptionPayable = this.RedemptionPayable
+ SubscriptionRefundPayable = this.SubscriptionRefundPayable
+ FeePayable = this.FeePayable
+ OtherInTransitLiabilities = this.OtherInTransitLiabilities
+ Liabilities = liabilities
+ NetAssets = assets - liabilities
+ }
+
+ member this.BalanceSheet = this.balanceSheet
+ member this.NetAssets = this.balanceSheet.NetAssets
+ member this.UnitNavStatus =
+ if this.OutstandingUnits = 0m then
+ UnitNavStatus.ZeroUnits
+ else
+ UnitNavStatus.HasValue (this.NetAssets / this.OutstandingUnits)
+
+type LedgerState =
+ {
+ Funds: Map<Guid, LedgerFund>
+ Idempotencies: Map<Guid * string, string>
+ }
+
+type LedgerError =
+ | FundNotFound of Guid
+ | FundAlreadyExists of Guid
+ | OrderNotFound of Guid
+ | ExternalRedemptionNotFound of Guid
+ | IdempotencyConflict of string
+ | InvalidIdentifier of string
+ | InvalidAmount of string
+ | InvalidPrecision of string
+ | InvalidState of string
+ | TransactionAlreadyExists of Guid
+ | InsufficientCash of decimal
+ | InsufficientUnits of decimal
+ | ResidualOwnershipRequired of decimal
+
+module Ledger =
+ type private ResultBuilder() =
+ member _.Bind(result, binder) = Result.bind binder result
+ member _.Return(value) = Ok value
+ member _.ReturnFrom(result) = result
+ member _.Zero() = Ok()
+ member _.Combine(first, second) = Result.bind (fun () -> second) first
+ member _.Delay(generator) = generator()
+
+ let private result = ResultBuilder()
+
+ [<Literal>]
+ let CashScale = 2
+
+ [<Literal>]
+ let UnitScale = 8
+
+ let empty =
+ {
+ Funds = Map.empty
+ Idempotencies = Map.empty
+ }
+
+ let private decimalScale value =
+ let bits = Decimal.GetBits(value)
+ int ((bits[3] >>> 16) &&& 0x7f)
+
+ let private roundDown scale value =
+ Decimal.Round(value, scale, MidpointRounding.ToZero)
+
+ let private validateNonNegative label scale value =
+ if value < 0m then
+ Error(InvalidAmount label)
+ elif decimalScale value > scale then
+ Error(InvalidPrecision label)
+ else
+ Ok()
+
+ let private validatePositive label scale value =
+ validateNonNegative label scale value
+ |> Result.bind (fun () ->
+ if value = 0m then
+ Error(InvalidAmount label)
+ else
+ Ok())
+
+ let private fundStatus outstandingUnits =
+ if outstandingUnits = 0m then
+ FundStatus.ZeroUnits
+ else
+ FundStatus.Active
+
+ let getFund fundId state =
+ match state.Funds |> Map.tryFind fundId with
+ | Some fund -> Ok fund
+ | None -> Error(FundNotFound fundId)
+
+ let private updateFund fund state =
+ { state with Funds = state.Funds |> Map.add fund.Id fund }
+
+ let private validateIdentifier label value =
+ if value = Guid.Empty then
+ Error(InvalidIdentifier label)
+ else
+ Ok()
+
+ let private transactionExists transactionId fund =
+ fund.Events |> List.exists (fun event -> event.TransactionId = transactionId)
+ || fund.CashFlows |> List.exists (fun cashFlow -> cashFlow.TransactionId = transactionId)
+ || fund.ExternalRedemptions |> Map.containsKey transactionId
+ || fund.ResidualAllocations |> List.exists (fun allocation -> allocation.TransactionId = transactionId)
+
+ let private ensureTransactionIsNew transactionId fund =
+ if transactionExists transactionId fund then
+ Error(TransactionAlreadyExists transactionId)
+ else
+ Ok()
+
+ let private addResidual residual residualState =
+ if residual = 0m then
+ residualState
+ else
+ match residualState with
+ | Clear -> RequiresAllocation residual
+ | RequiresAllocation existing -> RequiresAllocation(existing + residual)
+ | Allocated _ -> RequiresAllocation residual
+
+ let private runIdempotent fundId key fingerprint state action =
+ if fundId = Guid.Empty then
+ Error(InvalidIdentifier "fund id")
+ elif String.IsNullOrWhiteSpace key then
+ Error(InvalidState "idempotency key cannot be empty")
+ else
+ match state.Idempotencies |> Map.tryFind (fundId, key) with
+ | Some previous when previous = fingerprint -> Ok state
+ | Some _ -> Error(IdempotencyConflict key)
+ | None ->
+ action state
+ |> Result.map (fun next ->
+ {
+ next with
+ Idempotencies = next.Idempotencies |> Map.add (fundId, key) fingerprint
+ })
+
+ let initializeFund fundId name initialCash initialUnitNav isSynthetic state =
+ result {
+ do! validateIdentifier "fund id" fundId
+ do! validateNonNegative "initial cash" CashScale initialCash
+ do! validatePositive "initial unit NAV" UnitScale initialUnitNav
+
+ if String.IsNullOrWhiteSpace name then
+ return! Error(InvalidState "fund name cannot be empty")
+
+ if state.Funds |> Map.containsKey fundId then
+ return! Error(FundAlreadyExists fundId)
+
+ let fund =
+ {
+ Id = fundId
+ Name = name
+ Currency = "CNY"
+ InitialCash = initialCash
+ InitialUnitNav = initialUnitNav
+ IsSynthetic = isSynthetic
+ AvailableCash = initialCash
+ FrozenCash = 0m
+ Holdings = Map.empty
+ OutstandingUnits = 0m
+ RedemptionPayable = 0m
+ RedemptionReceivable = 0m
+ OtherInTransitAssets = 0m
+ SubscriptionRefundPayable = 0m
+ FeePayable = 0m
+ OtherInTransitLiabilities = 0m
+ Status = FundStatus.Empty
+ Orders = Map.empty
+ ExternalRedemptions = Map.empty
+ CashFlows = []
+ Events = []
+ ResidualAllocations = []
+ ResidualState = Clear
+ }
+
+ return updateFund fund state
+ }
+
+ let private canIssueUnits fund =
+ match fund.ResidualState with
+ | RequiresAllocation residual when fund.OutstandingUnits = 0m ->
+ Error(ResidualOwnershipRequired residual)
+ | _ -> Ok()
+
+ let private addEvent transactionId eventType at cashAmount units residual owner isSynthetic fund =
+ {
+ fund with
+ Events =
+ {
+ TransactionId = transactionId
+ FundId = fund.Id
+ EventType = eventType
+ EffectiveAt = at
+ CashAmount = cashAmount
+ Units = units
+ RoundingResidual = residual
+ ResidualOwner = owner
+ IsSynthetic = isSynthetic
+ }
+ :: fund.Events
+ }
+
+ let confirmExternalDeposit fundId transactionId idempotencyKey grossCash unitNav effectiveAt isSynthetic state =
+ let fingerprint = sprintf "external-deposit|%O|%M|%M|%O|%b" transactionId grossCash unitNav effectiveAt isSynthetic
+
+ runIdempotent fundId idempotencyKey fingerprint state (fun current ->
+ result {
+ do! validateIdentifier "transaction id" transactionId
+ do! validatePositive "external deposit cash" CashScale grossCash
+ do! validatePositive "external deposit unit NAV" UnitScale unitNav
+
+ let! fund = getFund fundId current
+ do! ensureTransactionIsNew transactionId fund
+ do! canIssueUnits fund
+
+ let convertedUnits = roundDown UnitScale (grossCash / unitNav)
+ if convertedUnits = 0m then
+ return! Error(InvalidAmount "external deposit does not create units")
+
+ let creditedCash = roundDown CashScale (convertedUnits * unitNav)
+ let residual = grossCash - creditedCash
+ let updatedFund =
+ {
+ fund with
+ AvailableCash = fund.AvailableCash + creditedCash
+ OutstandingUnits = fund.OutstandingUnits + convertedUnits
+ Status = FundStatus.Active
+ ResidualState = addResidual residual fund.ResidualState
+ CashFlows =
+ {
+ TransactionId = transactionId
+ FundId = fundId
+ Kind = CashFlowKind.ExternalDeposit
+ EffectiveAt = effectiveAt
+ GrossCash = grossCash
+ CreditedCash = creditedCash
+ ConvertedUnits = convertedUnits
+ UnitNav = Some unitNav
+ RoundingResidual = residual
+ ResidualOwner = None
+ IsSynthetic = isSynthetic
+ }
+ :: fund.CashFlows
+ }
+ |> addEvent transactionId "external_deposit_confirmed" effectiveAt creditedCash convertedUnits residual None isSynthetic
+
+ return updateFund updatedFund current
+ })
+
+ let private orderAlreadyExists orderId fund =
+ if fund.Orders |> Map.containsKey orderId then
+ Error(InvalidState "order already exists")
+ else
+ Ok()
+
+ let private emptyHolding instrumentId =
+ {
+ InstrumentId = instrumentId
+ AvailableUnits = 0m
+ FrozenUnits = 0m
+ SettledValue = 0m
+ }
+
+ let private addSettledHolding instrumentId units value holdings =
+ let holding = holdings |> Map.tryFind instrumentId |> Option.defaultValue (emptyHolding instrumentId)
+
+ holdings
+ |> Map.add instrumentId {
+ holding with
+ AvailableUnits = holding.AvailableUnits + units
+ SettledValue = holding.SettledValue + value
+ }
+
+ let private addCashFlow cashFlow fund =
+ { fund with CashFlows = cashFlow :: fund.CashFlows }
+
+ let private missingOrder orderId =
+ Error(OrderNotFound orderId)
+
+ let freezeUnderlyingPurchase fundId orderId idempotencyKey instrumentId requestedCash effectiveAt isSynthetic state =
+ let fingerprint = sprintf "underlying-purchase-freeze|%O|%O|%M|%O|%b" orderId instrumentId requestedCash effectiveAt isSynthetic
+
+ runIdempotent fundId idempotencyKey fingerprint state (fun current ->
+ result {
+ do! validateIdentifier "order id" orderId
+ do! validateIdentifier "instrument id" instrumentId
+ do! validatePositive "underlying purchase cash" CashScale requestedCash
+ let! fund = getFund fundId current
+ do! orderAlreadyExists orderId fund
+ do! ensureTransactionIsNew orderId fund
+
+ if fund.AvailableCash < requestedCash then
+ return! Error(InsufficientCash requestedCash)
+
+ let order =
+ {
+ Id = orderId
+ FundId = fundId
+ InstrumentId = instrumentId
+ Kind = LedgerOrderKind.Purchase
+ IdempotencyKey = idempotencyKey
+ Status = OrderStatus.CashFrozen
+ RequestedCash = requestedCash
+ RequestedUnits = 0m
+ ConfirmedCash = 0m
+ ConfirmedUnits = 0m
+ RoundingResidual = 0m
+ ResidualOwner = None
+ IsSynthetic = isSynthetic
+ SubmittedAt = effectiveAt
+ ConfirmedAt = None
+ }
+
+ let updatedFund =
+ {
+ fund with
+ AvailableCash = fund.AvailableCash - requestedCash
+ FrozenCash = fund.FrozenCash + requestedCash
+ Orders = fund.Orders |> Map.add orderId order
+ }
+ |> addEvent orderId "underlying_purchase_frozen" effectiveAt requestedCash 0m 0m None isSynthetic
+
+ return updateFund updatedFund current
+ })
+
+ let confirmUnderlyingPurchase fundId orderId idempotencyKey confirmedUnits unitNav effectiveAt isSynthetic state =
+ let fingerprint = sprintf "underlying-purchase-confirm|%O|%M|%M|%O|%b" orderId confirmedUnits unitNav effectiveAt isSynthetic
+
+ runIdempotent fundId idempotencyKey fingerprint state (fun current ->
+ result {
+ do! validateIdentifier "order id" orderId
+ do! validatePositive "underlying purchase units" UnitScale confirmedUnits
+ do! validatePositive "underlying purchase unit NAV" UnitScale unitNav
+ let! fund = getFund fundId current
+ let! order = fund.Orders |> Map.tryFind orderId |> Option.map Ok |> Option.defaultValue (missingOrder orderId)
+
+ if order.Kind <> LedgerOrderKind.Purchase || order.Status <> OrderStatus.CashFrozen then
+ return! Error(InvalidState "underlying purchase is not cash frozen")
+
+ let grossCash = confirmedUnits * unitNav
+ let confirmedCash = roundDown CashScale grossCash
+ if confirmedCash <= 0m then
+ return! Error(InvalidAmount "underlying purchase cash rounds to zero")
+
+ if confirmedCash > order.RequestedCash then
+ return! Error(InvalidState "underlying purchase exceeds frozen cash")
+
+ let residual = grossCash - confirmedCash
+ let updatedOrder =
+ {
+ order with
+ Status = OrderStatus.Confirmed
+ ConfirmedCash = confirmedCash
+ ConfirmedUnits = confirmedUnits
+ RoundingResidual = residual
+ ResidualOwner = None
+ ConfirmedAt = Some effectiveAt
+ }
+
+ let updatedFund =
+ {
+ fund with
+ AvailableCash = fund.AvailableCash + order.RequestedCash - confirmedCash
+ FrozenCash = fund.FrozenCash - order.RequestedCash
+ Holdings = fund.Holdings |> addSettledHolding order.InstrumentId confirmedUnits confirmedCash
+ Orders = fund.Orders |> Map.add orderId updatedOrder
+ ResidualState = addResidual residual fund.ResidualState
+ }
+ |> addCashFlow {
+ TransactionId = orderId
+ FundId = fundId
+ Kind = CashFlowKind.UnderlyingPurchase
+ EffectiveAt = effectiveAt
+ GrossCash = grossCash
+ CreditedCash = confirmedCash
+ ConvertedUnits = confirmedUnits
+ UnitNav = Some unitNav
+ RoundingResidual = residual
+ ResidualOwner = None
+ IsSynthetic = isSynthetic
+ }
+ |> addEvent orderId "underlying_purchase_confirmed" effectiveAt confirmedCash confirmedUnits residual None isSynthetic
+
+ return updateFund updatedFund current
+ })
+
+ let cancelUnderlyingPurchase fundId orderId idempotencyKey effectiveAt isSynthetic state =
+ let fingerprint = sprintf "underlying-purchase-cancel|%O|%O|%b" orderId effectiveAt isSynthetic
+
+ runIdempotent fundId idempotencyKey fingerprint state (fun current ->
+ result {
+ do! validateIdentifier "order id" orderId
+ let! fund = getFund fundId current
+ let! order = fund.Orders |> Map.tryFind orderId |> Option.map Ok |> Option.defaultValue (missingOrder orderId)
+
+ if order.Kind <> LedgerOrderKind.Purchase || order.Status <> OrderStatus.CashFrozen then
+ return! Error(InvalidState "underlying purchase is not cancellable")
+
+ let updatedOrder = { order with Status = OrderStatus.Cancelled }
+ let updatedFund =
+ {
+ fund with
+ AvailableCash = fund.AvailableCash + order.RequestedCash
+ FrozenCash = fund.FrozenCash - order.RequestedCash
+ Orders = fund.Orders |> Map.add orderId updatedOrder
+ }
+ |> addEvent orderId "underlying_purchase_cancelled" effectiveAt order.RequestedCash 0m 0m None isSynthetic
+
+ return updateFund updatedFund current
+ })
+
+ let freezeUnderlyingRedemption fundId orderId idempotencyKey instrumentId requestedUnits effectiveAt isSynthetic state =
+ let fingerprint = sprintf "underlying-redemption-freeze|%O|%O|%M|%O|%b" orderId instrumentId requestedUnits effectiveAt isSynthetic
+
+ runIdempotent fundId idempotencyKey fingerprint state (fun current ->
+ result {
+ do! validateIdentifier "order id" orderId
+ do! validateIdentifier "instrument id" instrumentId
+ do! validatePositive "underlying redemption units" UnitScale requestedUnits
+ let! fund = getFund fundId current
+ do! orderAlreadyExists orderId fund
+ do! ensureTransactionIsNew orderId fund
+ let! holding = fund.Holdings |> Map.tryFind instrumentId |> Option.map Ok |> Option.defaultValue (Error(InvalidState "holding not found"))
+
+ if holding.AvailableUnits < requestedUnits then
+ return! Error(InsufficientUnits requestedUnits)
+
+ let updatedHolding =
+ {
+ holding with
+ AvailableUnits = holding.AvailableUnits - requestedUnits
+ FrozenUnits = holding.FrozenUnits + requestedUnits
+ }
+
+ let order =
+ {
+ Id = orderId
+ FundId = fundId
+ InstrumentId = instrumentId
+ Kind = LedgerOrderKind.Redemption
+ IdempotencyKey = idempotencyKey
+ Status = OrderStatus.UnitsFrozen
+ RequestedCash = 0m
+ RequestedUnits = requestedUnits
+ ConfirmedCash = 0m
+ ConfirmedUnits = 0m
+ RoundingResidual = 0m
+ ResidualOwner = None
+ IsSynthetic = isSynthetic
+ SubmittedAt = effectiveAt
+ ConfirmedAt = None
+ }
+
+ let updatedFund =
+ {
+ fund with
+ Holdings = fund.Holdings |> Map.add instrumentId updatedHolding
+ Orders = fund.Orders |> Map.add orderId order
+ }
+ |> addEvent orderId "underlying_redemption_frozen" effectiveAt 0m requestedUnits 0m None isSynthetic
+
+ return updateFund updatedFund current
+ })
+
+ let private releaseFrozenHolding holding units =
+ let totalUnits = holding.AvailableUnits + holding.FrozenUnits
+ let releasedValue =
+ if units = totalUnits then
+ holding.SettledValue
+ else
+ roundDown CashScale (holding.SettledValue * units / totalUnits)
+
+ let remaining =
+ {
+ holding with
+ FrozenUnits = holding.FrozenUnits - units
+ SettledValue = holding.SettledValue - releasedValue
+ }
+
+ remaining, releasedValue
+
+ let confirmUnderlyingRedemption fundId orderId idempotencyKey unitNav effectiveAt isSynthetic state =
+ let fingerprint = sprintf "underlying-redemption-confirm|%O|%M|%O|%b" orderId unitNav effectiveAt isSynthetic
+
+ runIdempotent fundId idempotencyKey fingerprint state (fun current ->
+ result {
+ do! validateIdentifier "order id" orderId
+ do! validatePositive "underlying redemption unit NAV" UnitScale unitNav
+ let! fund = getFund fundId current
+ let! order = fund.Orders |> Map.tryFind orderId |> Option.map Ok |> Option.defaultValue (missingOrder orderId)
+
+ if order.Kind <> LedgerOrderKind.Redemption || order.Status <> OrderStatus.UnitsFrozen then
+ return! Error(InvalidState "underlying redemption is not units frozen")
+
+ let! holding = fund.Holdings |> Map.tryFind order.InstrumentId |> Option.map Ok |> Option.defaultValue (Error(InvalidState "holding not found"))
+ if holding.FrozenUnits < order.RequestedUnits then
+ return! Error(InsufficientUnits order.RequestedUnits)
+
+ let grossCash = order.RequestedUnits * unitNav
+ let receivable = roundDown CashScale grossCash
+ if receivable <= 0m then
+ return! Error(InvalidAmount "underlying redemption cash rounds to zero")
+
+ let residual = grossCash - receivable
+ let updatedHolding, _ = releaseFrozenHolding holding order.RequestedUnits
+ let updatedHoldings =
+ if updatedHolding.AvailableUnits = 0m && updatedHolding.FrozenUnits = 0m then
+ fund.Holdings |> Map.remove order.InstrumentId
+ else
+ fund.Holdings |> Map.add order.InstrumentId updatedHolding
+
+ let updatedOrder =
+ {
+ order with
+ Status = OrderStatus.Confirmed
+ ConfirmedCash = receivable
+ ConfirmedUnits = order.RequestedUnits
+ RoundingResidual = residual
+ ResidualOwner = None
+ ConfirmedAt = Some effectiveAt
+ }
+
+ let updatedFund =
+ {
+ fund with
+ Holdings = updatedHoldings
+ RedemptionReceivable = fund.RedemptionReceivable + receivable
+ Orders = fund.Orders |> Map.add orderId updatedOrder
+ ResidualState = addResidual residual fund.ResidualState
+ }
+ |> addCashFlow {
+ TransactionId = orderId
+ FundId = fundId
+ Kind = CashFlowKind.UnderlyingRedemption
+ EffectiveAt = effectiveAt
+ GrossCash = grossCash
+ CreditedCash = receivable
+ ConvertedUnits = order.RequestedUnits
+ UnitNav = Some unitNav
+ RoundingResidual = residual
+ ResidualOwner = None
+ IsSynthetic = isSynthetic
+ }
+ |> addEvent orderId "underlying_redemption_confirmed" effectiveAt receivable order.RequestedUnits residual None isSynthetic
+
+ return updateFund updatedFund current
+ })
+
+ let receiveUnderlyingRedemption fundId orderId idempotencyKey effectiveAt isSynthetic state =
+ let fingerprint = sprintf "underlying-redemption-receive|%O|%O|%b" orderId effectiveAt isSynthetic
+
+ runIdempotent fundId idempotencyKey fingerprint state (fun current ->
+ result {
+ do! validateIdentifier "order id" orderId
+ let! fund = getFund fundId current
+ let! order = fund.Orders |> Map.tryFind orderId |> Option.map Ok |> Option.defaultValue (missingOrder orderId)
+
+ if order.Kind <> LedgerOrderKind.Redemption || order.Status <> OrderStatus.Confirmed then
+ return! Error(InvalidState "underlying redemption is not confirmed")
+
+ if fund.RedemptionReceivable < order.ConfirmedCash then
+ return! Error(InvalidState "underlying redemption receivable is unavailable")
+
+ let updatedOrder = { order with Status = OrderStatus.Settled }
+ let updatedFund =
+ {
+ fund with
+ AvailableCash = fund.AvailableCash + order.ConfirmedCash
+ RedemptionReceivable = fund.RedemptionReceivable - order.ConfirmedCash
+ Orders = fund.Orders |> Map.add orderId updatedOrder
+ }
+ |> addCashFlow {
+ TransactionId = orderId
+ FundId = fundId
+ Kind = CashFlowKind.UnderlyingRedemptionReceipt
+ EffectiveAt = effectiveAt
+ GrossCash = order.ConfirmedCash
+ CreditedCash = order.ConfirmedCash
+ ConvertedUnits = 0m
+ UnitNav = None
+ RoundingResidual = 0m
+ ResidualOwner = None
+ IsSynthetic = isSynthetic
+ }
+ |> addEvent orderId "underlying_redemption_received" effectiveAt order.ConfirmedCash 0m 0m None isSynthetic
+
+ return updateFund updatedFund current
+ })
+
+ let allocateResidual fundId transactionId idempotencyKey owner effectiveAt isSynthetic state =
+ let fingerprint = sprintf "residual-allocation|%O|%A|%O|%b" transactionId owner effectiveAt isSynthetic
+
+ runIdempotent fundId idempotencyKey fingerprint state (fun current ->
+ result {
+ do! validateIdentifier "transaction id" transactionId
+ let! fund = getFund fundId current
+
+ match fund.ResidualState with
+ | RequiresAllocation residual ->
+ if fund.OutstandingUnits <> 0m then
+ return! Error(InvalidState "residual allocation requires zero outstanding units")
+
+ do! ensureTransactionIsNew transactionId fund
+ let allocation =
+ {
+ TransactionId = transactionId
+ FundId = fundId
+ Amount = residual
+ Owner = owner
+ EffectiveAt = effectiveAt
+ IsSynthetic = isSynthetic
+ }
+
+ let updatedFund =
+ {
+ fund with
+ ResidualState = Allocated(residual, owner)
+ ResidualAllocations = allocation :: fund.ResidualAllocations
+ }
+ |> addEvent transactionId "residual_allocated" effectiveAt 0m 0m residual (Some owner) isSynthetic
+
+ return updateFund updatedFund current
+ | _ ->
+ return! Error(InvalidState "no residual requires allocation")
+ })
+
+ let confirmExternalRedemption fundId transactionId idempotencyKey units unitNav effectiveAt isSynthetic state =
+ let fingerprint = sprintf "external-redemption|%O|%M|%M|%O|%b" transactionId units unitNav effectiveAt isSynthetic
+
+ runIdempotent fundId idempotencyKey fingerprint state (fun current ->
+ result {
+ do! validateIdentifier "transaction id" transactionId
+ do! validatePositive "external redemption units" UnitScale units
+ do! validatePositive "external redemption unit NAV" UnitScale unitNav
+
+ let! fund = getFund fundId current
+ do! ensureTransactionIsNew transactionId fund
+ if fund.OutstandingUnits < units then
+ return! Error(InsufficientUnits units)
+
+ let grossCash = units * unitNav
+ let payableCash = roundDown CashScale grossCash
+ if payableCash = 0m then
+ return! Error(InvalidAmount "external redemption cash rounds to zero")
+
+ let residual = grossCash - payableCash
+ let redemption =
+ {
+ TransactionId = transactionId
+ FundId = fundId
+ IdempotencyKey = idempotencyKey
+ Units = units
+ UnitNav = unitNav
+ PayableCash = payableCash
+ Status = ExternalRedemptionStatus.PendingPayment
+ ConfirmedAt = effectiveAt
+ PaidAt = None
+ IsSynthetic = isSynthetic
+ }
+
+ let updatedFund =
+ {
+ fund with
+ OutstandingUnits = fund.OutstandingUnits - units
+ RedemptionPayable = fund.RedemptionPayable + payableCash
+ Status = fundStatus (fund.OutstandingUnits - units)
+ ResidualState = addResidual residual fund.ResidualState
+ ExternalRedemptions = fund.ExternalRedemptions |> Map.add transactionId redemption
+ CashFlows =
+ {
+ TransactionId = transactionId
+ FundId = fundId
+ Kind = CashFlowKind.ExternalRedemption
+ EffectiveAt = effectiveAt
+ GrossCash = grossCash
+ CreditedCash = payableCash
+ ConvertedUnits = units
+ UnitNav = Some unitNav
+ RoundingResidual = residual
+ ResidualOwner = None
+ IsSynthetic = isSynthetic
+ }
+ :: fund.CashFlows
+ }
+ |> addEvent transactionId "external_redemption_confirmed" effectiveAt payableCash units residual None isSynthetic
+
+ return updateFund updatedFund current
+ })
+
+ let payExternalRedemption fundId transactionId idempotencyKey effectiveAt isSynthetic state =
+ let fingerprint = sprintf "external-redemption-payment|%O|%O|%b" transactionId effectiveAt isSynthetic
+
+ runIdempotent fundId idempotencyKey fingerprint state (fun current ->
+ result {
+ do! validateIdentifier "transaction id" transactionId
+ let! fund = getFund fundId current
+ let! redemption =
+ fund.ExternalRedemptions
+ |> Map.tryFind transactionId
+ |> Option.map Ok
+ |> Option.defaultValue (Error(ExternalRedemptionNotFound transactionId))
+
+ if redemption.Status <> ExternalRedemptionStatus.PendingPayment then
+ return! Error(InvalidState "external redemption is already paid")
+
+ let payable = redemption.PayableCash
+
+ if fund.AvailableCash < payable then
+ return! Error(InsufficientCash payable)
+
+ let updatedRedemption = { redemption with Status = ExternalRedemptionStatus.Paid; PaidAt = Some effectiveAt }
+
+ let updatedFund =
+ {
+ fund with
+ AvailableCash = fund.AvailableCash - payable
+ RedemptionPayable = fund.RedemptionPayable - payable
+ ExternalRedemptions = fund.ExternalRedemptions |> Map.add transactionId updatedRedemption
+ CashFlows =
+ {
+ TransactionId = transactionId
+ FundId = fundId
+ Kind = CashFlowKind.ExternalRedemptionPayment
+ EffectiveAt = effectiveAt
+ GrossCash = payable
+ CreditedCash = payable
+ ConvertedUnits = 0m
+ UnitNav = None
+ RoundingResidual = 0m
+ ResidualOwner = None
+ IsSynthetic = isSynthetic
+ }
+ :: fund.CashFlows
+ }
+ |> addEvent transactionId "external_redemption_paid" effectiveAt payable 0m 0m None isSynthetic
+
+ return updateFund updatedFund current
+ })
diff --git a/src/FundLab.Domain/Performance.fs b/src/FundLab.Domain/Performance.fs
new file mode 100644
index 0000000..b1c51db
--- /dev/null
+++ b/src/FundLab.Domain/Performance.fs
@@ -0,0 +1,51 @@
+namespace FundLab.Domain
+
+open System
+
+type PerformanceObservation =
+ {
+ At: DateTimeOffset
+ NetAssets: decimal
+ ExternalCashFlow: decimal
+ }
+
+type PerformanceError =
+ | InvalidObservation of string
+
+module Performance =
+ let timeWeightedReturn observations =
+ let validate observation previousAt =
+ if observation.NetAssets < 0m then
+ Error(InvalidObservation "net assets cannot be negative")
+ elif previousAt |> Option.exists (fun at -> observation.At <= at) then
+ Error(InvalidObservation "observations must be strictly ordered")
+ else
+ Ok()
+
+ match observations with
+ | [] -> Ok 0m
+ | first :: rest ->
+ validate first None
+ |> Result.bind (fun () ->
+ if first.NetAssets <= 0m then
+ Error(InvalidObservation "first net assets must be positive")
+ else
+ rest
+ |> List.fold
+ (fun result observation ->
+ result
+ |> Result.bind (fun (previous, linkedReturn) ->
+ validate observation (Some previous.At)
+ |> Result.bind (fun () ->
+ let endingAssetsBeforeFlow = observation.NetAssets - observation.ExternalCashFlow
+
+ if endingAssetsBeforeFlow < 0m then
+ Error(InvalidObservation "external cash flow exceeds ending net assets")
+ elif previous.NetAssets <= 0m then
+ Error(InvalidObservation "period start net assets must be positive")
+ else
+ let periodReturn = endingAssetsBeforeFlow / previous.NetAssets
+ Ok(observation, linkedReturn * periodReturn)))
+ )
+ (Ok(first, 1m))
+ |> Result.map (fun (_, linkedReturn) -> linkedReturn - 1m))
diff --git a/src/FundLab.Web/App.fs b/src/FundLab.Web/App.fs
new file mode 100644
index 0000000..630a8e9
--- /dev/null
+++ b/src/FundLab.Web/App.fs
@@ -0,0 +1,41 @@
+module FundLab.Web
+
+open Elmish
+open Elmish.React
+open Feliz
+
+type Model =
+ {
+ hasFund: bool
+ }
+
+type Msg =
+ | CreateFundRequested
+
+let init () =
+ { hasFund = false }
+
+let update message model =
+ match message with
+ | CreateFundRequested -> model
+
+let view model dispatch =
+ Html.main [
+ prop.className "empty-state"
+ prop.children [
+ Html.p [ prop.className "eyebrow"; prop.text "FUND LAB" ]
+ Html.h1 "尚未创建基金"
+ Html.p "先创建一支人民币模拟基金,再选择基金代码和持仓金额。"
+ Html.p (if model.hasFund then "已创建基金" else "尚未选择投资")
+ Html.button [
+ prop.className "primary-action"
+ prop.text "创建基金"
+ prop.onClick (fun _ -> dispatch CreateFundRequested)
+ ]
+ ]
+ ]
+
+let start () =
+ Program.mkSimple init update view
+ |> Program.withReactSynchronous "fund-lab-root"
+ |> Program.run
diff --git a/src/FundLab.Web/FundLab.Web.fsproj b/src/FundLab.Web/FundLab.Web.fsproj
new file mode 100644
index 0000000..529740f
--- /dev/null
+++ b/src/FundLab.Web/FundLab.Web.fsproj
@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <TargetFramework>net8.0</TargetFramework>
+ <OutputType>Library</OutputType>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+ <ItemGroup>
+ <PackageReference Include="Elmish" Version="4.1.0" />
+ <PackageReference Include="Fable.Core" Version="4.4.0" />
+ <PackageReference Include="Fable.Elmish.React" Version="4.0.0" />
+ <PackageReference Include="Feliz" Version="2.9.0" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="App.fs" />
+ </ItemGroup>
+</Project>
diff --git a/src/FundLab.Web/index.html b/src/FundLab.Web/index.html
new file mode 100644
index 0000000..7b27019
--- /dev/null
+++ b/src/FundLab.Web/index.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<html lang="zh-CN">
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <title>Fund Lab</title>
+ </head>
+ <body>
+ <div id="fund-lab-root"></div>
+ <script type="module" src="/src/main.js"></script>
+ </body>
+</html>
diff --git a/src/FundLab.Web/package-lock.json b/src/FundLab.Web/package-lock.json
new file mode 100644
index 0000000..56782fe
--- /dev/null
+++ b/src/FundLab.Web/package-lock.json
@@ -0,0 +1,1581 @@
+{
+ "name": "fund-lab-web",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "fund-lab-web",
+ "dependencies": {
+ "react": "18.3.1",
+ "react-dom": "18.3.1"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-react": "4.3.4",
+ "vite": "5.4.14"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helpers": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.8",
+ "@babel/types": "^7.29.8",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.8"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.29.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.8",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.8",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.8",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.6.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@napi-rs/lzma-linux-x64-gnu": {
+ "version": "1.5.1",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^22.20 || ^24.12 || >=25"
+ }
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.63.4",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.3.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.26.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.25.9",
+ "@babel/plugin-transform-react-jsx-source": "^7.25.9",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.14.2"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.11.25",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.29.0",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.11.23",
+ "caniuse-lite": "^1.0.30001810",
+ "electron-to-chromium": "^1.5.427",
+ "node-releases": "^2.0.55",
+ "update-browserslist-db": "^1.3.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/browserslist/node_modules/node-releases": {
+ "version": "2.0.56",
+ "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.56.tgz",
+ "integrity": "sha512-x0InOIyzgdk+eyaWaRJFH5snEtiImgBgblZ2CyPrLmqqcuMQkEvcDPHbzqbD8eDsSeJbVOjn+crzyzHaM4D+/A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001810",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.433",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/esbuild/node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild/node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild/node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild/node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild/node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild/node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild/node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild/node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild/node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild/node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild/node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild/node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild/node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/esbuild/node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.19",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.28",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.18",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.14.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.63.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.9"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@napi-rs/lzma-linux-x64-gnu": "1.5.1",
+ "@rollup/rollup-android-arm-eabi": "4.63.4",
+ "@rollup/rollup-android-arm64": "4.63.4",
+ "@rollup/rollup-darwin-arm64": "4.63.4",
+ "@rollup/rollup-darwin-x64": "4.63.4",
+ "@rollup/rollup-freebsd-arm64": "4.63.4",
+ "@rollup/rollup-freebsd-x64": "4.63.4",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.63.4",
+ "@rollup/rollup-linux-arm-musleabihf": "4.63.4",
+ "@rollup/rollup-linux-arm64-gnu": "4.63.4",
+ "@rollup/rollup-linux-arm64-musl": "4.63.4",
+ "@rollup/rollup-linux-loong64-gnu": "4.63.4",
+ "@rollup/rollup-linux-loong64-musl": "4.63.4",
+ "@rollup/rollup-linux-ppc64-gnu": "4.63.4",
+ "@rollup/rollup-linux-ppc64-musl": "4.63.4",
+ "@rollup/rollup-linux-riscv64-gnu": "4.63.4",
+ "@rollup/rollup-linux-riscv64-musl": "4.63.4",
+ "@rollup/rollup-linux-s390x-gnu": "4.63.4",
+ "@rollup/rollup-linux-x64-gnu": "4.63.4",
+ "@rollup/rollup-linux-x64-musl": "4.63.4",
+ "@rollup/rollup-openbsd-x64": "4.63.4",
+ "@rollup/rollup-openharmony-arm64": "4.63.4",
+ "@rollup/rollup-win32-arm64-msvc": "4.63.4",
+ "@rollup/rollup-win32-ia32-msvc": "4.63.4",
+ "@rollup/rollup-win32-x64-gnu": "4.63.4",
+ "@rollup/rollup-win32-x64-msvc": "4.63.4",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.4.tgz",
+ "integrity": "sha512-I+BSHzTAhKN2n7ZwGZsegGcZjDpLqFOMAtJz/u6uFGe0pUFbq56dEHjqJV/ZUdRJtNXNxA+hREUatZBvMR3Oiw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.4.tgz",
+ "integrity": "sha512-pu3BdjS2LtEzRu2elmGzS3fIeWSZy4BMDIaLNwjorO76+k2d0LMluijhsDx3KQyQBQ/lLUZCQA9/s6csvUfuhw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.4.tgz",
+ "integrity": "sha512-xfSrj9MHnWK9GaSqT9U0ImHtH/N8WZlHLx4cZHiuLcqs640hvZ3hLPd5UR2AZS57FaE8HrRUSpltbZdWRxHiDA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.4.tgz",
+ "integrity": "sha512-bqU99PLJb/dqb3S0GIMdeuyAEETSUgZBoqXYd3Sd+WCsV+MmPhnN6JrotWyir31+QgH7EvvE5/mwGJlEoci8Fw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.4.tgz",
+ "integrity": "sha512-JinsFZ5G40oXQb+sUuiA5x689vhr6dDYK0H0NL+rwKdL6CqnmYN8PE4ZwfRSoIjrCxqTQG/SLfTtSvHeGxoVlw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.4.tgz",
+ "integrity": "sha512-GAdA4UxpiNm27cLHr2GqXBpAD0x9FqwYBY7/YSP0Ss0/PNi4k8gbviqpIpYbVSRBaS2ZcegXEzgTQMbRNCwxCw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.4.tgz",
+ "integrity": "sha512-qDd6NoA1znaLjp4jR5U/KWCdLAKDJNB8W9ChbbDaKbo0xA+Atln5HK6LFCZ4oJQpemtRZA288DCirFRjrspptw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.4.tgz",
+ "integrity": "sha512-WtB5Tz5KTNINb8ZA+8sQ7bmjuS1JrRT7YverYIhUGdWWDlpzVWmIwuZE+jidkEXUn1l0zrEkaIMa8dHF3NGcsA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.4.tgz",
+ "integrity": "sha512-VcQ3L1tjnkKzWjryAVaFhHEWcqOfICX9uxVVoDzm2t0DpgKRHd2zOpVrJc0xsWeBZcBFyYROCIBdyR/fS174pg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.4.tgz",
+ "integrity": "sha512-6+ZQX6P5s0cMDN2Ypb8Lbm2+/sZYmZjdaYny992ujUU9UKi/4CWoJWsl1pNvjWJHNHGK51m+jKGLlh1ylb2ifQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.4.tgz",
+ "integrity": "sha512-D72ZnvkFkBXOfzMMQLcwfPLyGkKb7HZ9/mf97B7v6/P5Lbv4oFOtSY/uHbS8lH6uKUOxoKiuokdb50XZSzzbJw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.4.tgz",
+ "integrity": "sha512-piU6BxeqA3O9KSu3kRCIQQtNqFFaTu21SEV4FwaRZowpnj3bLaWPZHw+xFqCs0XlJ+aOH3PTRWGoglH+mKA/OA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.4.tgz",
+ "integrity": "sha512-/5PGpHwqt2EEEOUs1XwzubE/ucr0dWDQ+to3zqi4Ds7EWpwtQ79wXc4JBoxqj/OwpawTsKWzJxHfSuBOq3DrWA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.4.tgz",
+ "integrity": "sha512-cX3beZDLWt7G2oJF+nhChiT+qtaihs+S2xi7ziGmVB+2pwPng6D0Ed0HmElQOgv2UsUmSJJLGwpBao/3TDx3VA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.4.tgz",
+ "integrity": "sha512-1uz2mGWHyptR7DgHHrlbdRAjXK7v7elGZ9lMja910/RP+ZYbX6xAmCiU9UZSX4hqmgtHMv6lr5l3kq1HIOpcag==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.4.tgz",
+ "integrity": "sha512-nLS8topojxyz7SRpKR2IODRpQ0XPZ+xaOXvT3+hqK/Uy8Lo5HFgkkIBiIrCu5tL5YqzTvgovGw55PwpahTAGig==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.4.tgz",
+ "integrity": "sha512-gs7DRKotr3l3q+jGPQBjH0ng1FjlEDm5ueQrkw5JtQvtLyEIcLASqAEaor56BhkKRzk+IcQzrcanBdb/bBQn8g==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.4.tgz",
+ "integrity": "sha512-iwZQRcmj7g88g3tzefIrQY7qvmuA/cfYwhrDtTBhsmukO4U2huVO5W+86XacUMRvdSFVAc6kZUZy21JaRwiB9w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.4.tgz",
+ "integrity": "sha512-dVHFp9gRWrdTpnqQuGfCwd7hOQDatK1VCP2iWhLY/cGrOQs/ucFzJ6A5SRqbXX12ZDI8EUuejSM5kwg+ja7Png==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.4.tgz",
+ "integrity": "sha512-t3NlauOW6gxZVVFcBEnO62Cb4wbyDFL416gTg1uFI/2tgqYQlf69FbSE115Ajre9I+c26Lk4mcmdFUsS/DGifQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.4.tgz",
+ "integrity": "sha512-xWuIaSye5FWZF8+UYtVEcHtRJDN5kN9Kfgxx3Kq8XIov9KSKbc1fiqQCm90SKrgQbUXZelbnUhnlUJmfSE7P9A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.4.tgz",
+ "integrity": "sha512-9ALJJUOg/ZflMJepVo2PlgsGxSaxN7SQ4Z8GoZfVlarWr6r3rkHUNsd/zAio7p4YMtChSMXPionxej4Hkf6CXQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.4.tgz",
+ "integrity": "sha512-blj9z5qx/Pv4WU0W1NMFDB97e0JH5ed+aZGywW8WCvp/NhWX/4PFAq5uu6Q0AebNn+Vo6KzUYDT++JzTT5ojlQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.63.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.4.tgz",
+ "integrity": "sha512-Erx822VRBwLa124shbj+wNXe//BOgMEctDV0m1aqTQdNO1S69DgNUCFKC1RCeZfixs1J31l6igk1ziyXErbigQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/rollup/node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.3.3",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.14",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "ISC"
+ }
+ }
+}
diff --git a/src/FundLab.Web/package.json b/src/FundLab.Web/package.json
new file mode 100644
index 0000000..98db215
--- /dev/null
+++ b/src/FundLab.Web/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "fund-lab-web",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "dotnet fable FundLab.Web.fsproj --outDir dist && vite build",
+ "dev": "dotnet fable FundLab.Web.fsproj --outDir dist --run vite"
+ },
+ "dependencies": {
+ "react": "18.3.1",
+ "react-dom": "18.3.1"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-react": "4.3.4",
+ "vite": "5.4.14"
+ }
+}
diff --git a/src/FundLab.Web/src/main.js b/src/FundLab.Web/src/main.js
new file mode 100644
index 0000000..57c83db
--- /dev/null
+++ b/src/FundLab.Web/src/main.js
@@ -0,0 +1,3 @@
+import { start } from "../dist/App.js";
+
+start();
diff --git a/src/FundLab.Web/vite.config.js b/src/FundLab.Web/vite.config.js
new file mode 100644
index 0000000..5e01572
--- /dev/null
+++ b/src/FundLab.Web/vite.config.js
@@ -0,0 +1,8 @@
+import { defineConfig } from "vite";
+
+export default defineConfig({
+ build: {
+ outDir: "dist",
+ emptyOutDir: true
+ }
+});
diff --git a/src/FundLab.Worker/FundLab.Worker.fsproj b/src/FundLab.Worker/FundLab.Worker.fsproj
new file mode 100644
index 0000000..9694479
--- /dev/null
+++ b/src/FundLab.Worker/FundLab.Worker.fsproj
@@ -0,0 +1,18 @@
+<Project Sdk="Microsoft.NET.Sdk.Worker">
+ <PropertyGroup>
+ <TargetFramework>net8.0</TargetFramework>
+ <OutputType>Exe</OutputType>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\FundLab.Domain\FundLab.Domain.fsproj" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Worker.fs" />
+ <Compile Include="Program.fs" />
+ </ItemGroup>
+</Project>
diff --git a/src/FundLab.Worker/Program.fs b/src/FundLab.Worker/Program.fs
new file mode 100644
index 0000000..058d9b7
--- /dev/null
+++ b/src/FundLab.Worker/Program.fs
@@ -0,0 +1,12 @@
+module FundLab.Worker.Program
+
+open Microsoft.Extensions.DependencyInjection
+open Microsoft.Extensions.Hosting
+
+[<EntryPoint>]
+let main argv =
+ Host.CreateDefaultBuilder(argv)
+ .ConfigureServices(fun services -> services.AddHostedService<Worker>() |> ignore)
+ .Build()
+ .Run()
+ 0
diff --git a/src/FundLab.Worker/Worker.fs b/src/FundLab.Worker/Worker.fs
new file mode 100644
index 0000000..6a7b7a7
--- /dev/null
+++ b/src/FundLab.Worker/Worker.fs
@@ -0,0 +1,15 @@
+namespace FundLab.Worker
+
+open System
+open System.Threading
+open System.Threading.Tasks
+open Microsoft.Extensions.Hosting
+
+type Worker() =
+ inherit BackgroundService()
+
+ override _.ExecuteAsync(stoppingToken: CancellationToken) : Task =
+ task {
+ while not stoppingToken.IsCancellationRequested do
+ do! Task.Delay(TimeSpan.FromMinutes 5, stoppingToken)
+ }