feat: Add Initial Plume UI

This commit is contained in:
2025-06-26 14:09:56 +02:00
parent 41d792dc2a
commit 1c14274cdb
20 changed files with 1310 additions and 74 deletions

View File

@@ -1,4 +1,4 @@
FROM mcr.microsoft.com/dotnet/sdk:9.0
FROM mcr.microsoft.com/dotnet/sdk:9.0.301
# Bun version
ARG BUN_INSTALL=/usr/local

View File

@@ -110,7 +110,7 @@ Now, we should be able to `restore`:
```sh
dotnet tool restore
dotnet restore
dotnet restore Poseidon.slnx
```
### Mkcert
@@ -163,3 +163,11 @@ In order for your browser to allow you to access the web application, you must a
2. Click on _"View Certificates",_ then _"Import..."_ in the _"Authorities"_ tab.
3. Select the root certificate; `~/.vite-plugin-mkcert/certs/rootCA.pem`.
- Make sure to check _"This certificate can identify websites"._
### Add `user` to OpenFGA
Ask [sales](moritz.jorg@oceanbox.io) to add your `azure-ad-user` to OpenFGA.
### CORS for Sorcerer
Add the `url` of your instance to the CORS list of Sorcerer [here](https://gitlab.com/oceanbox/manifests/-/blob/main/values/sorcerer/kustomize/prod/appsettings.json?ref_type=heads#L52).

View File

@@ -26,13 +26,13 @@ let fableWatch = $"fable watch -e .jsx -o build --run {vite}"
Target.create "Clean" (fun _ -> Shell.cleanDir distPath)
Target.create "Bundle" (fun _ ->
[ "server", dotnet $"publish -tl -c Release -o {distPath}" serverPath
[ "server", dotnet $"build -tl -c Release -o {distPath}" serverPath
"client", dotnet (fable "-m production") clientPath ]
|> runParallel
)
Target.create "BundleDebug" (fun _ ->
[ "server", dotnet $"publish -tl -c Debug -o {distPath}" serverPath
[ "server", dotnet $"build -tl -c Debug --no-restore -o {distPath}" serverPath
"client", dotnet (fable "-m development --minify false --sourcemap true") clientPath ]
|> runParallel
)

View File

@@ -1,4 +1,5 @@
FROM mcr.microsoft.com/dotnet/aspnet:9.0
FROM mcr.microsoft.com/dotnet/aspnet:9.0.6
RUN apt-get update \
&& apt-get install -y gcc-multilib libnetcdf19 libnetcdf-dev

View File

@@ -162,3 +162,9 @@ let cropStatsApi url =
|> Remoting.withRouteBuilder Api.Crop.routeBuilder
|> Remoting.withBinarySerialization
|> Remoting.buildProxy<Api.Crop.Stats>
let plumeApi () =
Remoting.createApi ()
|> Remoting.withCredentials true
|> Remoting.withRouteBuilder Api.authorizedRouteBuilder
|> Remoting.buildProxy<Api.Plume>

View File

@@ -606,9 +606,77 @@ type SimArchive =
}
type PlumeSimModel = {
name: string
fvcom: System.Guid
start: DateTime
stop: DateTime
timeIdx: int
} with
static member empty = {
name = ""
fvcom = System.Guid.Empty
start = DateTime.Now
stop = DateTime.Now.AddDays(4)
timeIdx = 0
}
type PlumeReleaseSite = {
position: float * float
radius: float
theta: float
depth: float
// ddepth: float
} with
static member empty = {
position = 0.0, 0.0
radius = 1.0
theta = 10.0
depth = -20.0
// ddepth = 10.0
}
type PlumeGroupModel = {
name: string option
sites: PlumeReleaseSite list
} with
static member empty i = {
name = $"Group-{i}" |> Some
sites = List.Empty
}
type PlumeTraits = {
temp: float
salt: float
transport: float
} with
static member empty = {
temp = 0.0
salt = 0.0
transport = 0.0
}
type PlumeType =
| DefaultPlume
member this.ToLabel() =
match this with
| DefaultPlume -> "Plume"
type SimControlKind =
| Drifters of SimType
| Plume of PlumeType
member this.ToLabel() =
match this with
| Drifters simType -> simType.ToLabel ()
| Plume plumeType -> plumeType.ToLabel ()
member this.simTypeOpt =
match this with
| Drifters simType -> Some simType
| Plume plumeType -> None
type SideNavMode =
| OceanControls
| SimControls of SimType
| SimControls of SimControlKind
| AnalysisControls of SimArchive
| ColorControls
| LayerControls
@@ -619,7 +687,7 @@ with
member this.ToLabel () =
match this with
| OceanControls -> "Ocean controls"
| SimControls simType -> $"Simulation controls {simType.ToLabel ()}"
| SimControls simControlKind -> $"Simulation controls {simControlKind.ToLabel ()}"
| AnalysisControls _ -> "Analysis controls"
| ColorControls -> "Color controls"
| LayerControls -> "Layer controls"

View File

@@ -214,12 +214,12 @@ let fetchDrifters (api: ArchivesApi) (aid: Guid) : SimArchive [] JS.Promise =
|> Array.map (fun (d, n, t) ->
let reverse =
match d.archiveType with
| Drifters (_, DriftersFormat.Particle) ->
| ArchiveType.Drifters (_, DriftersFormat.Particle) ->
decodeDriftersInput d
|> Option.map _.simulation.reverse
|> Option.flatten
|> Option.defaultValue false
| Drifters _ ->
| ArchiveType.Drifters _ ->
decodePostdriftInput d
|> Option.map _.analysis.reverse
|> Option.flatten

View File

@@ -712,7 +712,7 @@ let update cmd model =
| _ -> TransportSim
let m =
{ model with
sideNavMode = SideNavMode.SimControls kind
sideNavMode = SideNavMode.SimControls (Drifters kind)
mode = Mode.Simulation Placing
}
@@ -1561,9 +1561,12 @@ let update cmd model =
let n' = if n = abs(model.inboxUnread) then -model.inboxUnread else n
// console.log $"unread: {n'}"
{ model with inboxUnread = n' }, Cmd.none
| SelectPlume id ->
| SetPlumeModel modelOpt ->
console.debug("[Mapster] SetPlumeModel:", modelOpt)
{ model with plumeModelOpt = modelOpt }, Cmd.none
| SetSelectedPlume id ->
console.log $"Selected plume: {id}"
{ model with plume = id }, Cmd.none
{ model with selectedPlume = Some id }, Cmd.none
| HubMsg msg -> { model with hubAction = Some msg }, Cmd.none
| Noop _ -> model, Cmd.none
@@ -2042,7 +2045,7 @@ let MapAppElement () =
let selectInboxItem (id, type': MessageType) =
Hub.Action.Inbox (Hub.InboxMsg.MarkRead id) |> (HubMsg >> dispatch)
match type' with
| MessageType.Plume -> SelectPlume (Some id) |> dispatch
| MessageType.Plume -> SetSelectedPlume id |> dispatch
| MessageType.Drifters -> SetSelectedDrifter id |> dispatch
| _ -> ()

View File

@@ -20,6 +20,7 @@
<Compile Include="Timeline.fs" />
<Compile Include="Fiskeridir.fs" />
<Compile Include="BarentsWatch.fs" />
<Compile Include="Plume.fs" />
<Compile Include="Drifters.fs" />
<Compile Include="ContourModel.fs" />
<Compile Include="Postdrift.fs" />

View File

@@ -224,6 +224,16 @@ type PostdriftModel = {
openLayersMap: OlMap
}
type PlumeModel = {
kind: PlumeType
simStarted: bool * int option
simulation: PlumeSimModel
sites: PlumeReleaseSite list
traits: PlumeTraits
selectedSite: (int * PlumeReleaseSite) option
openLayersMap: OlMap
}
type ContourData = (float*float)[][]
type ContourModel = {
kind: ContourKind
@@ -334,7 +344,8 @@ type Model = {
identity: Api.UserIdentity
inboxUnread: int
hubAction: Hub.Action option
plume: System.Guid option
plumeModelOpt: PlumeModel option
selectedPlume: ArchiveId option
infectionNetwork: NetworkState
}
with
@@ -399,7 +410,8 @@ with
identity = Api.UserIdentity.empty
inboxUnread = 0
hubAction = None
plume = None
plumeModelOpt = None
selectedPlume = None
infectionNetwork = NetworkState.empty
}
@@ -504,7 +516,8 @@ type Msg =
| DeleteArchive of System.Guid
| SetIdentity of Api.UserIdentity
| SetUnread of int
| SelectPlume of System.Guid option
| SetPlumeModel of PlumeModel option
| SetSelectedPlume of System.Guid
| HubMsg of Hub.Action
| Noop of unit

View File

@@ -140,6 +140,8 @@ let simAccordion (dispatch: Msg -> unit) model =
// let canSubmit = model.simPolicies |> Array.fold (fun a x -> a || x.IsAllowed()) false
console.debug $"policies: %A{model.simPolicies}"
let disabled = model.archive.id = Guid.Empty
// TODO(mrtz): Set an actual condition on plumes
let disabledPlume = false
let disabledTransport = model.simPolicies |> Array.contains (DriftersPolicy.SubmitTransport false)
let disabledDeposition = model.simPolicies |> Array.contains (DriftersPolicy.SubmitSedimentation false)
let chooseMode kind _ =
@@ -178,38 +180,45 @@ let simAccordion (dispatch: Msg -> unit) model =
<sp-action-button
static="primary"
?disabled={disabledTransport}
@click={Ev(chooseMode TransportSim)}
@click={Ev(chooseMode (Drifters TransportSim))}
>
Transport
</sp-action-button>
<sp-action-button
static="primary"
?disabled={disabledTransport}
@click={Ev(chooseMode LiceSim)}
@click={Ev(chooseMode (Drifters LiceSim))}
>
Salmon lice
</sp-action-button>
<sp-action-button
static="primary"
?disabled={disabledTransport}
@click={Ev(chooseMode VirusSim)}
@click={Ev(chooseMode (Drifters VirusSim))}
>
Virus
</sp-action-button>
<sp-action-button
static="primary"
?disabled={disabledDeposition}
@click={Ev(chooseMode DepositionSim)}
@click={Ev(chooseMode (Drifters DepositionSim))}
>
Deposition
</sp-action-button>
<sp-action-button
static="primary"
?disabled={disabledTransport}
@click={Ev(chooseMode WaterContactSim)}
@click={Ev(chooseMode (Drifters WaterContactSim))}
>
Water contact
</sp-action-button>
<sp-action-button
static="primary"
?disabled={disabledPlume}
@click={Ev (chooseMode(Plume DefaultPlume))}
>
Plume
</sp-action-button>
</sp-action-group>
</div>
"""
@@ -572,8 +581,13 @@ let sideNav (dispatch: Msg -> unit) (model: Model) (navMode: SideNavMode) =
let simSelector (dispatch: Msg -> unit) (model: Model) =
match model.mode with
| Mode.Simulation Placing ->
console.log $"simControls : {kind}"
Drifters.simulationControls kind dispatch model
match kind.simTypeOpt with
| Some simType ->
console.log $"drifter simControls : {kind}"
Drifters.simulationControls simType dispatch model
| None ->
console.log $"plume simControls : {kind}"
Plume.simulationControls DefaultPlume dispatch model
| Mode.Moot
| Mode.Ocean
| Mode.Stats _
@@ -731,22 +745,21 @@ let toolboxNav setArchivesOpen setInboxOpen model dispatch =
| Mode.Ocean -> ()
| _ -> SetMode Mode.Ocean |> dispatch
let selectCompute (driftersModelOpt: DrifterModel option) ev =
let selectCompute (driftersModelOpt: DrifterModel option) (plumeModelOpt: PlumeModel option) ev =
if canSubmit then
match driftersModelOpt with
| Some d ->
console.debug($"We already have an ongoing sim : {d.simulation.kind}")
d.simulation.kind
|> SimControls
|> SetSideNavMode
|> dispatch
match driftersModelOpt, plumeModelOpt with
| Some d, _ ->
console.debug ($"We already have an ongoing driftser sim : {d.simulation.kind}")
(Drifters d.simulation.kind) |> SimControls |> SetSideNavMode |> dispatch
SimMode.Placing
|> Mode.Simulation
|> SetMode
|> dispatch
SimMode.Placing |> Mode.Simulation |> SetMode |> dispatch
| None, Some p ->
console.debug ($"We already have an ongoing plume sim")
(Plume p.kind) |> SimControls |> SetSideNavMode |> dispatch
SimMode.Placing |> Mode.Simulation |> SetMode |> dispatch
| _ ->
SimType.TransportSim
(Drifters SimType.TransportSim)
|> SimControls
|> SetSideNavMode
|> dispatch
@@ -804,7 +817,7 @@ let toolboxNav setArchivesOpen setInboxOpen model dispatch =
<overlay-trigger id="trigger" placement="right" offset="6">
<sp-tooltip slot="hover-content">Compute</sp-tooltip>
<div slot="trigger" class="toolbox-control{if canSubmit then " " + isSimControls else ".disabled" }">
<div class="toolboxIcon" @click={Ev(selectCompute model.drifterModelOpt)}>
<div class="toolboxIcon" @click={Ev(selectCompute model.drifterModelOpt model.plumeModelOpt)}>
{if model.drifterModelOpt.IsSome then statusIcon else Lit.nothing}
<i class="fas fa-server"></i>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -475,6 +475,80 @@ module Handlers =
|> Async.AwaitTask
}
let private runPlume
(ctx: HttpContext)
(plumeJob: PlumeJob)
=
task {
let user = getUserName ctx
let group = getGroup ctx
let id = ActorId user
try
let proxy = ActorProxy.Create<IPlumeActor> (id, "PlumeActor")
Log.Information ("runPlume: user {username}, fvcom={@fvcom}, name={@name}", user, plumeJob.fvcom, plumeJob.name)
match! proxy.Submit plumeJob with
| Ok info ->
Log.Debug $"jobid is {info.jobId} ({info.archiveId})"
return Some info
| Error e ->
Log.Warning $"job submit failed: {e}"
match SignalRHub.getHub ctx with
| Some h ->
do! h.Clients.User(user).Send (Hub.Response.Note (Note.failure $"Job submission failed: {e}"))
return None
| None ->
Log.Error "Could not get signalr hub context"
return None
with exn ->
Log.Error $"runPlume: {exn.Message}"
Log.Verbose $"runPlume: %A{exn}"
return None
}
|> Async.AwaitTask
let private getPlumeSimInfo (ctx: HttpContext) (jobId: int) =
task {
let user = getUserName ctx
let id = ActorId user
try
let proxy = ActorProxy.Create<IJobActor> (id, "PlumeActor")
let! job = getJobInfo proxy jobId
return job
with exn ->
Log.Error $"[Plume] getSimInfo: {exn.Message}"
Log.Verbose $"[Plume] getSimInfo: %A{exn}"
return None
}
|> Async.AwaitTask
let private getActivePlumeSims (ctx: HttpContext) (aId: Guid) =
task {
let user = getUserName ctx
let id = ActorId user
try
let proxy = ActorProxy.Create<IJobActor> (id, "PlumeActor")
let! jobs = proxy.GetActiveJobs aId
Log.Information ("[Plume] Active jobs for {username}@{aid}: {jobs}", user, aId, jobs.Length)
return jobs
with exn ->
Log.Error $"[Plume] getActiveSimulations: %s{exn.Message}"
Log.Verbose $"[Plume] getActiveSimulations: %A{exn}"
return [||]
}
|> Async.AwaitTask
let plumeApi (ctx: HttpContext) : Api.Plume = {
startPlume = runPlume ctx
getActiveJobs = getActivePlumeSims ctx
getSimInfo = getPlumeSimInfo ctx
}
module Endpoints =
let authEndpoints: HttpHandler =
Remoting.createApi ()
@@ -505,3 +579,9 @@ module Endpoints =
|> Remoting.fromContext Handlers.inboxApi
|> Remoting.withRouteBuilder Api.authorizedRouteBuilder
|> Remoting.buildHttpHandler
let plumeEndpoints: HttpHandler =
Remoting.createApi ()
|> Remoting.fromContext Handlers.plumeApi
|> Remoting.withRouteBuilder Api.authorizedRouteBuilder
|> Remoting.buildHttpHandler

View File

@@ -425,6 +425,20 @@ module Handlers =
val inboxApi: ctx: Microsoft.AspNetCore.Http.HttpContext -> Inbox
val private runPlume:
ctx: Microsoft.AspNetCore.Http.HttpContext ->
plumeJob: Hipster.Actors.PlumeJob -> Async<Hipster.Job.JobInfo option>
val private getPlumeSimInfo:
ctx: Microsoft.AspNetCore.Http.HttpContext ->
jobId: int -> Async<Hipster.Job.JobInfo option>
val private getActivePlumeSims:
ctx: Microsoft.AspNetCore.Http.HttpContext ->
aId: System.Guid -> Async<Hipster.Job.JobInfo array>
val plumeApi: ctx: Microsoft.AspNetCore.Http.HttpContext -> Plume
module Endpoints =
val authEndpoints: Giraffe.Core.HttpHandler
@@ -437,6 +451,8 @@ module Endpoints =
val inboxEndpoints: Giraffe.Core.HttpHandler
val plumeEndpoints: Giraffe.Core.HttpHandler
module Atlantis.Events

View File

@@ -26,8 +26,8 @@ type PlumeEnv = {
member this.toJson() = [
"PLUME_ID", Encode.guid this.aid
"PLUME_NAME", Encode.string this.job.name
"PLUME_LAT", Encode.tuple2 Encode.float Encode.float this.job.pos
"PLUME_LNG", Encode.tuple2 Encode.float Encode.float this.job.pos
"PLUME_LAT", Encode.float this.job.pos.Lat
"PLUME_LNG", Encode.float this.job.pos.Long
"PLUME_TEMP", Encode.float this.job.temp
"PLUME_SALT", Encode.float this.job.salt
"PLUME_DEPTH", Encode.float this.job.depth
@@ -128,10 +128,10 @@ type PlumeActor
let! allowed = fga.checkRelation tuple
if allowed then
Log.Debug $"DriftersActor.StartDrifters user validated: {this.myId}"
Log.Debug $"PlumeActor.StartPlume user validated: {this.myId}"
if this.validatePoints job.fvcom [ job.pos ] then
Log.Debug $"DriftersActor.StartDrifters release points validated: {this.myId}"
if this.validatePoints job.fvcom [ (job.pos.Lat, job.pos.Long) ] then
Log.Debug $"PlumeActor.StartPlumes release points validated: {this.myId}"
return! submitJob ()
else
let msg = "One or more release points outside boundary."

View File

@@ -4,6 +4,12 @@
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<Deterministic>true</Deterministic>
<NetCoreTargetingPackRoot>[UNDEFINED]</NetCoreTargetingPackRoot>
<DisableImplicitLibraryPacksFolder>true</DisableImplicitLibraryPacksFolder>
<DisableImplicitNuGetFallbackFolder>true</DisableImplicitNuGetFallbackFolder>
<!-- https://www.gresearch.co.uk/blog/article/improve-nuget-restores-with-static-graph-evaluation/ -->
<RestoreUseStaticGraphEvaluation>true</RestoreUseStaticGraphEvaluation>
<Version>2.102.0</Version>
<RootNamespace>Server</RootNamespace>
<OtherFlags>$(OtherFlags) --sig:Atlantis.fsi --test:GraphBasedChecking --test:ParallelOptimization --test:ParallelIlxGen --strict-indentation+</OtherFlags>

View File

@@ -54,6 +54,7 @@ export default defineConfig({
},
define: {
global: {},
'process.env': {},
},
build: {
rollupOptions: {

View File

@@ -61,3 +61,9 @@ module Api =
markRead: Guid -> Async<int>
getMessages: unit -> Async<InboxItem[]>
}
type Plume = {
startPlume: Hipster.Actors.PlumeJob -> Async<JobInfo option>
getSimInfo: int -> Async<JobInfo option>
getActiveJobs: Guid -> Async<JobInfo []>
}

View File

@@ -51,12 +51,19 @@ type PostdriftJob = {
type PlumeJob = {
name: string
fvcom: Guid
pos: float * float
pos: LatLong
// TODO: Functions, could be constants
// saltfunc: unit
// tempfunc: unit
/// Example 4 and 0
temp: float
salt: float
/// DOB Depth on Bottom
depth: float
/// Pipe Angle
theta: float
transport: float
/// Pipe radius
radius: float
start: DateTime
stop: DateTime
@@ -65,7 +72,7 @@ type PlumeJob = {
static member empty = {
name = ""
fvcom = System.Guid.Empty
pos = 0.0, 0.0
pos = {Lat = 0.0; Long = 0.0}
temp = 0.0
salt = 0.0
depth = 0.0

View File

@@ -12,14 +12,13 @@ type SlurmJobType =
| DriftersJob
| PlumeJob
| UnknownJob
with
override x.ToString() =
match x with
| DriftersJob -> "Drifters"
| PlumeJob -> "Plume"
| UnknownJob -> "Unknown"
static member FromString (s: string) =
match s.ToLower() with
static member FromString(s: string) =
match s.ToLower () with
| "drifters" -> DriftersJob
| "plume" -> PlumeJob
| _ -> UnknownJob
@@ -28,14 +27,13 @@ type SlurmMessageType =
| Status
| Progress
| Message
with
override x.ToString() =
match x with
| Status -> "status"
| Progress -> "progress"
| Message -> "message"
static member FromString (s: string) =
match s.ToLower() with
static member FromString(s: string) =
match s.ToLower () with
| "status" -> Status
| "progress" -> Progress
| _ -> Message
@@ -93,3 +91,6 @@ type DriftersPolicy =
match x with
| SubmitSedimentation v -> v
| SubmitTransport v -> v
type LatLong = { Lat: float; Long: float }