diff --git a/src/Admin.elm b/src/Admin.elm index 3bff80b..636c350 100644 --- a/src/Admin.elm +++ b/src/Admin.elm @@ -39,6 +39,7 @@ backendModelCodec randomSeed = (Queue.codec (Codec.triple Codec.string Codec.string toBackendMsgCodec)) |> Codec.field "randomSeed" .randomSeed (Codec.succeed randomSeed) |> Codec.field "playerDataCache" .playerDataCache (Dict.ExtraExtra.codec Codec.string Codec.int) + |> Codec.field "isInMaintenance" .isInMaintenance Codec.bool |> Codec.buildObject @@ -173,7 +174,7 @@ toBackendMsgCodec = adminToBackendCodec : Codec AdminToBackend adminToBackendCodec = Codec.custom - (\exportJsonEncoder importJsonEncoder createNewWorldEncoder changeWorldSpeedEncoder value -> + (\exportJsonEncoder importJsonEncoder createNewWorldEncoder changeWorldSpeedEncoder switchMaintenanceEncoder value -> case value of ExportJson -> exportJsonEncoder @@ -186,6 +187,9 @@ adminToBackendCodec = ChangeWorldSpeed arg0 -> changeWorldSpeedEncoder arg0 + + SwitchMaintenance arg0 -> + switchMaintenanceEncoder arg0 ) |> Codec.variant0 "ExportJson" ExportJson |> Codec.variant1 "ImportJson" ImportJson Codec.string @@ -198,4 +202,11 @@ adminToBackendCodec = |> Codec.field "fast" .fast Codec.bool |> Codec.buildObject ) + |> Codec.variant1 + "SwitchMaintenance" + SwitchMaintenance + (Codec.object (\now -> { now = now }) + |> Codec.field "now" .now Codec.bool + |> Codec.buildObject + ) |> Codec.buildCustom diff --git a/src/Backend.elm b/src/Backend.elm index 77b4aaa..bfb937e 100644 --- a/src/Backend.elm +++ b/src/Backend.elm @@ -97,6 +97,7 @@ init = , lastTenToBackendMsgs = Queue.empty , randomSeed = Random.initialSeed 0 , playerDataCache = Dict.empty + , isInMaintenance = False } , Task.perform FirstTick Time.now ) @@ -306,7 +307,11 @@ update msg model = getWorlds model in ( model - , Lamdera.sendToFrontend clientId <| CurrentWorlds worlds + , Lamdera.sendToFrontend clientId <| + CurrentWorlds + { worlds = worlds + , isInMaintenance = model.isInMaintenance + } ) Disconnected _ clientId -> @@ -1095,14 +1100,22 @@ updateFromFrontend sessionId clientId msg model = getWorlds model in ( model - , Lamdera.sendToFrontend clientId <| CurrentWorlds worlds + , Lamdera.sendToFrontend clientId <| + CurrentWorlds + { worlds = worlds + , isInMaintenance = model.isInMaintenance + } ) RefreshPlease -> let loggedOut () = ( model - , Lamdera.sendToFrontend clientId <| CurrentWorlds <| getWorlds model + , Lamdera.sendToFrontend clientId <| + CurrentWorlds + { worlds = getWorlds model + , isInMaintenance = model.isInMaintenance + } ) in if isAdmin sessionId clientId model then @@ -1256,6 +1269,15 @@ updateAdmin clientId msg model = |> Cmd.with (refreshAdminData newModel) |> Cmd.andThen (refreshPlayersOnWorld r.world) + SwitchMaintenance r -> + ( { model + | isInMaintenance = r.now + , playerDataCache = Dict.empty + , loggedInPlayers = BiDict.empty + } + , Lamdera.broadcast <| MaintenanceModeChanged r + ) + refreshPlayersOnWorld : World.Name -> Model -> ( Model, Cmd BackendMsg ) refreshPlayersOnWorld worldName model = diff --git a/src/Frontend.elm b/src/Frontend.elm index c402a42..da01352 100644 --- a/src/Frontend.elm +++ b/src/Frontend.elm @@ -136,6 +136,7 @@ init url key = , loginForm = Auth.init , worlds = Nothing , worldData = worldData + , isInMaintenance = False -- mostly player frontend state , newChar = NewChar.init @@ -652,6 +653,11 @@ update msg ({ loginForm } as model) = , Lamdera.sendToBackend <| AdminToBackend <| ChangeWorldSpeed r ) + AskToSwitchMaintenance r -> + ( model + , Lamdera.sendToBackend <| AdminToBackend <| SwitchMaintenance r + ) + resetBarter : Model -> ( Model, Cmd FrontendMsg ) resetBarter model = @@ -793,16 +799,54 @@ updateFromBackend msg model = } |> update (GoToRoute (Route.loggedOut model.route)) - CurrentWorlds worlds -> + CurrentWorlds { worlds, isInMaintenance } -> ( { model | worlds = Just worlds , loginForm = model.loginForm |> Auth.selectDefaultWorld worlds + , isInMaintenance = isInMaintenance } , Cmd.none ) + MaintenanceModeChanged { now } -> + let + default () = + let + auth = + model.loginForm + + newAuth = + { auth | name = "" } + |> Auth.setPlaintextPassword "" + in + ( { model + | isInMaintenance = now + , worldData = NotLoggedIn + , loginForm = newAuth + , route = Route.News + } + , Cmd.none + ) + in + case model.worldData of + IsAdmin _ -> + ( { model | isInMaintenance = now } + , Cmd.none + ) + + NotLoggedIn -> + ( { model | isInMaintenance = now } + , Cmd.none + ) + + IsPlayer _ -> + default () + + IsPlayerSigningUp -> + default () + CurrentOtherPlayers otherPlayers -> ( { model | worldData = @@ -943,7 +987,7 @@ view model = IsAdmin _ -> [ alertMessageView model.alertMessage , H.div [ HA.class "flex flex-col gap-4" ] - [ adminLinksView model.route + [ adminLinksView model , commonLinksView model.route ] ] @@ -973,19 +1017,48 @@ view model = , commonLinksView model.route ] ] + + default () = + if Route.isStandalone model.route then + contentView model + :: preload model + + else + appView { leftNav = leftNav } model + :: preload model in { title = "NuAshworld " ++ Version.version , body = - if Route.isStandalone model.route then - contentView model - :: preload model + if model.isInMaintenance then + case model.worldData of + IsAdmin _ -> + default () + + NotLoggedIn -> + default () + + IsPlayer _ -> + [ maintenanceView ] + + IsPlayerSigningUp -> + [ maintenanceView ] else - appView { leftNav = leftNav } model - :: preload model + default () } +maintenanceView : Html FrontendMsg +maintenanceView = + H.div + [ HA.class "pt-8 px-10 pb-10 flex items-center justify-center min-h-screen min-w-screen" ] + [ H.div [ HA.class "flex flex-col gap-4" ] + [ H.span [ HA.class "text-lg" ] [ H.text "Under maintenance" ] + , H.span [] [ H.text "NuAshworld is under maintenance right now. Please wait a few minutes, this page will update automatically." ] + ] + ] + + preload : Model -> List (Html FrontendMsg) preload model = if model.hoveredGuideNavLink then @@ -1035,6 +1108,8 @@ leftNavView leftNav model = in H.div [ HA.class "bg-green-800 min-w-[28ch] max-w-[28ch] px-6 pb-10 pt-[26px] flex flex-col gap-10 items-center max-h-screen overflow-hidden fixed z-[2] left-0 top-0 bottom-0" ] [ logoView model + , H.viewIf model.isInMaintenance <| + H.div [ HA.class "text-yellow" ] [ H.text "Under maintenance" ] , H.div [ HA.class "flex flex-col items-center gap-6" ] (leftNav tickFrequency) ] @@ -1082,7 +1157,11 @@ contentView model = in H.div [ HA.class "pt-8 px-10 pb-10 flex flex-col items-start min-h-screen" - , HA.class "min-w-[calc(100vw_-_28ch)]" + , if isStandaloneRoute then + HA.class "min-w-screen" + + else + HA.class "min-w-[calc(100vw_-_28ch)]" , HA.classList [ ( "ml-[28ch]", not isStandaloneRoute ) ] ] (case ( model.route, model.worldData ) of @@ -5448,15 +5527,28 @@ loggedInLinksView p currentRoute = |> H.div [] -adminLinksView : Route -> Html FrontendMsg -adminLinksView currentRoute = +adminLinksView : Model -> Html FrontendMsg +adminLinksView model = [ LinkMsg { label = "Refresh", msg = Refresh, tooltip = Nothing, disabled = False } , LinkIn { label = "Worlds", route = AdminRoute AdminWorldsList, highlight = HNormal } + , LinkMsg + { label = + "Maint " + ++ (if model.isInMaintenance then + "Off" + + else + "On" + ) + , msg = AskToSwitchMaintenance { now = not model.isInMaintenance } + , tooltip = Nothing + , disabled = False + } , LinkMsg { label = "Import", msg = ImportButtonClicked, tooltip = Nothing, disabled = False } , LinkMsg { label = "Export", msg = AskForExport, tooltip = Nothing, disabled = False } , LinkMsg { label = "Logout", msg = Logout, tooltip = Nothing, disabled = False } ] - |> List.map (linkView currentRoute) + |> List.map (linkView model.route) |> H.div [] diff --git a/src/Types.elm b/src/Types.elm index 4a1f07d..13f6309 100644 --- a/src/Types.elm +++ b/src/Types.elm @@ -49,6 +49,7 @@ type alias FrontendModel = , loginForm : Auth Plaintext , worlds : Maybe (List WorldInfo) , worldData : WorldData + , isInMaintenance : Bool -- player frontend state: , alertMessage : Maybe String @@ -84,6 +85,7 @@ type alias BackendModel = , -- We don't want to send the same data to players over and over when -- Tick-ing. This lets us skip that work. playerDataCache : Dict ClientId Int + , isInMaintenance : Bool } @@ -149,6 +151,7 @@ type FrontendMsg | HoveredGuideNavLink | AskToRefuelCar ItemKind.Kind | AskToChangeWorldSpeed { world : World.Name, fast : Bool } + | AskToSwitchMaintenance { now : Bool } type BarterMsg @@ -205,6 +208,7 @@ type AdminToBackend | ImportJson String | CreateNewWorld String Bool | ChangeWorldSpeed { world : World.Name, fast : Bool } + | SwitchMaintenance { now : Bool } type BackendMsg @@ -226,10 +230,11 @@ type alias PlayerData_ = type ToFrontend = CurrentPlayer PlayerData | CurrentOtherPlayers (List COtherPlayer) - | CurrentWorlds (List WorldInfo) + | CurrentWorlds { worlds : List WorldInfo, isInMaintenance : Bool } | CurrentAdmin AdminData | CurrentAdminLoggedInPlayers (Dict World.Name (List PlayerName)) | CurrentAdminLastTenToBackendMsgs (List ( PlayerName, World.Name, ToBackend )) + | MaintenanceModeChanged { now : Bool } | YoureLoggedOut (List WorldInfo) | YourFightResult ( Fight.Info, PlayerData ) | YoureLoggedInSigningUp