Skip to content

Commit

Permalink
fix(ui): small ui fixes in connection display, tracking now creating …
Browse files Browse the repository at this point in the history
…connections properly and less noise in logs from GOAWAY messages in ESI client
  • Loading branch information
updraft0 committed Jun 12, 2024
1 parent 2050285 commit ac0d80c
Show file tree
Hide file tree
Showing 13 changed files with 167 additions and 51 deletions.
3 changes: 3 additions & 0 deletions server/src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ logger {
// ESI client
"org.updraft0.esi.client": INFO

// Auth/Location, etc tracking
"org.updraft0.controltower.server.tracking": DEBUG

// dependencies
"io.netty": INFO
"org.flywaydb": INFO
Expand Down
3 changes: 3 additions & 0 deletions server/src/main/resources/application.dev.conf
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ logger {
// ESI client
"org.updraft0.esi.client": INFO

// Auth/Location, etc tracking
"org.updraft0.controltower.server.tracking": TRACE

// dependencies
"io.netty": INFO
"org.flywaydb": INFO
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,19 @@ private[map] case class MapState(

def getSystem(id: SystemId): Option[MapSystemWithAll] = systems.get(id)
def hasSystem(id: SystemId): Boolean = systems.get(id).exists(_.display.nonEmpty)
def hasConnection(fromSystem: SystemId, toSystem: SystemId): Boolean =

private inline def hasConnectionInternal(fromSystem: SystemId, toSystem: SystemId): Boolean =
systems.get(fromSystem).exists(_.connections.exists(c => c.toSystemId == toSystem || c.fromSystemId == toSystem))

def hasConnection(fromSystem: SystemId, toSystem: SystemId): Boolean =
val res = hasConnectionInternal(fromSystem, toSystem)
val otherSide = hasConnectionInternal(toSystem, fromSystem)
if (res != otherSide)
throw new IllegalStateException(
s"Inconsistent state: ${fromSystem}-->${toSystem} @ $res but ${toSystem}-->${fromSystem} @ $otherSide"
)
res

def getConnection(fromSystem: SystemId, toSystem: SystemId): MapWormholeConnection =
systems(fromSystem).connections.find(c => c.toSystemId == toSystem || c.fromSystemId == toSystem).head
def hasGateBetween(fromSystem: SystemId, toSystem: SystemId): Boolean =
Expand Down Expand Up @@ -666,15 +677,23 @@ object MapEntity extends ReactiveEntity[MapEnv, MapId, MapState, Identified[MapR
)
else Chunk.empty
ZIO.foldLeft(changes)((nextState, locationsUpdate)):
case ((st, responses), asm: LocationUpdateAction.AddMapSystem) if !st.hasSystem(asm.system) =>
case ((st, responses), asm: LocationUpdateAction.AddMapSystem)
if asm.adjacentTo.isEmpty && !st.hasSystem(asm.system) =>
addSystemFromLocation(mapId, st, responses, asm).orDie
case ((st, responses), asm: LocationUpdateAction.AddMapSystem)
if asm.adjacentTo.exists(fromSystemId =>
!st
.hasConnection(fromSystemId, asm.system) && isPotentialWormholeJump(st, fromSystemId, asm.system)
) && !st.hasSystem(asm.system) =>
addSystemFromLocation(mapId, st, responses, asm).orDie
case ((st, responses), amc: LocationUpdateAction.AddMapConnection)
if !st
.hasConnection(amc.fromSystem, amc.toSystem) && isPotentialWormholeJump(st, amc.fromSystem, amc.toSystem) =>
addMapConnectionFromLocation(mapId, st, responses, amc).orDie
case ((st, responses), aj: LocationUpdateAction.AddJump) if st.hasConnection(aj.fromSystem, aj.toSystem) =>
addMapConnectionJump(mapId, st, responses, st.getConnection(aj.fromSystem, aj.toSystem).id, aj).orDie
case (prev, _) => ZIO.succeed(prev)
case (prev, msg) =>
ZIO.succeed(prev)

private def addSystemFromLocation(
mapId: MapId,
Expand Down Expand Up @@ -759,6 +778,7 @@ object MapEntity extends ReactiveEntity[MapEnv, MapId, MapState, Identified[MapR
case (Some(p), Some(n)) if online && p.system != n.system && prev.role != model.MapRole.Viewer =>
// character has changed location and is online - add the system and a connection to the map
Chunk(
LocationUpdateAction.AddMapSystem(p.system, charId, prev.role, Some(n.system), n.updatedAt),
LocationUpdateAction.AddMapSystem(n.system, charId, prev.role, Some(p.system), n.updatedAt),
LocationUpdateAction.AddMapConnection(p.system, n.system, charId, prev.role, n),
LocationUpdateAction.AddJump(charId, p.system, n.system, n)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ object MapSession:
// process any session messages
_ <- sessionMessages.take.flatMap(handleMapSessionMessage(character.id, mapRole, close, _)).forever.forkScoped
// ping out every ping interval to keep connection open
_ <- chan.send(ChannelEvent.Read(WebSocketFrame.Ping)).schedule(Schedule.fixed(PingInterval)).forkDaemon
_ <- chan.send(ChannelEvent.Read(WebSocketFrame.Ping)).schedule(Schedule.fixed(PingInterval)).ignore.forkDaemon
// join on the remaining loops
_ <- recv.join
_ <- send.join
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ object CharacterAuthTracker:
_ <- state.update(_ => all.map(ca => ca.characterId -> CharacterState.Active(ca)).toMap)
// wait for initial refresh
_ <- refreshPending(esi, state, hub)
_ <- state.get.flatMap(sm =>
ZIO.logDebug(s"Have active tokens for ${sm.count {
case (_, _: CharacterState.Active) => true
case _ => false
}} characters")
)
// start the refresh timer
_ <- refreshPending(esi, state, hub).repeat(Schedule.fixed(PollInterval)).forkScoped
// start the snapshot timer
Expand Down Expand Up @@ -112,8 +118,8 @@ object CharacterAuthTracker:
now <- ZIO.clockWith(_.instant)
nowExp = now.plus(PollInterval)
curr <- state.get
activeEntries = curr.values.collect {
case CharacterState.Active(token) if token.expiry.isAfter(nowExp) => token
activeEntries = curr.values.collect { case CharacterState.Active(token) =>
token
}
_ <- q.offer(Chunk.from(activeEntries))
yield ()
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,17 @@ trait LocationTracker:
object LocationTracker:
private val InCapacity = 64
private val InternalHubCapacity = 64
private val OnlineUpdateSeconds = 60

// Ensure that the token is at least this much before expiry
private val AuthTooOld = 10.seconds

// Update the 'online' status of characters with this period
private val OnlineUpdateInterval = 1.minute

// Error if the ESI call takes more than this period
private val EsiCallTimeout = 3.seconds

private val FakeEsiTimeout = EsiError.Timeout("Internal location tracker timeout", None)

case class Config(interval: Duration, maxParallel: Int)

Expand Down Expand Up @@ -145,38 +155,43 @@ object LocationTracker:
m.updatedWith(s.charId):
case None => Some(s)
case Some(p) =>
Some(s.copy(auth = p.auth.orElse(s.auth))) // update auth in case it was refreshed during checking locations
Some(s.copy(auth = p.auth)) // update auth in case it was refreshed during checking locations

private def refreshLocation(esi: EsiClient, now: Instant, st: CharacterState): UIO[CharacterState] =
st match
case CharacterState(_, _, None, _, _) =>
// no-op - with no auth there is nothing to update
ZIO.succeed(st.copy(state = CharacterLocationState.NoAuth, updatedAt = now))
case CharacterState(_, CharacterLocationState.Offline, _, prevAt, _)
if now.isBefore(prevAt.plusSeconds(OnlineUpdateSeconds)) =>
if now.isBefore(prevAt.plus(OnlineUpdateInterval)) =>
// no-op - with the character offline within the endpoint cache window there is nothing to update
ZIO.succeed(st)
case CharacterState(charId, _, Some(auth), _, _) if auth.expiry.isBefore(now.plusSeconds(OnlineUpdateSeconds)) =>
case CharacterState(charId, _, Some(auth), _, _) if auth.expiry.isBefore(now.plus(AuthTooOld)) =>
// cannot use a token that is expired (but character auth tracker should give us an update)
ZIO
.logWarning("Not refreshing character due to expired auth token")
.logWarning("Not refreshing character due to expiring/expired auth token")
.as(st.copy(auth = None, state = CharacterLocationState.NoAuth, updatedAt = now))
case CharacterState(charId, prevState, Some(auth), prevAt, _) =>
// refresh with previous state
doRefresh(esi, now, charId, prevState, auth)
.tapError(ex => ZIO.logError(s"ESI error while refreshing character status: ${ex}"))
.fold(
.foldZIO(
{
case EsiError.BadGateway => st // ignore bad gateway errors
case _: EsiError.Timeout => st // ignore gateway timeouts
case _ => st.copy(state = CharacterLocationState.ApiError, prevState = Some(prevState), updatedAt = now)
case EsiError.BadGateway => ZIO.succeed(st) // ignore bad gateway errors
case t: EsiError.Timeout =>
ZIO.logTrace(s"Timed out during ESI call: ${t.error}").as(st) // ignore gateway timeouts
case e =>
ZIO
.logError(s"ESI error while refreshing character status, ignoring: ${e}")
.as(
st.copy(state = CharacterLocationState.ApiError, prevState = Some(prevState), updatedAt = now)
)
},
identity
ZIO.succeed
)
.resurrect
.foldZIO(
{
case iox: java.io.IOException if iox.getMessage.contains("GOAWAY received") =>
case scx: sttp.client3.SttpClientException if scx.cause.getMessage.contains("GOAWAY received") =>
// TODO look into using a different client that handles errors more gracefully?
// ignore HTTP/2 GOAWAY as a transient error similar to the 502 errors above
ZIO.succeed(st)
Expand All @@ -195,9 +210,9 @@ object LocationTracker:
prevState: CharacterLocationState,
auth: CharacterAuth
) =
(esi.getCharacterOnline(auth.token)(charId) <&>
esi.getCharacterLocation(auth.token)(charId) <&>
esi.getCharacterShip(auth.token)(charId))
(esi.getCharacterOnline(auth.token)(charId).timeoutFail(FakeEsiTimeout)(EsiCallTimeout) <&>
esi.getCharacterLocation(auth.token)(charId).timeoutFail(FakeEsiTimeout)(EsiCallTimeout) <&>
esi.getCharacterShip(auth.token)(charId).timeoutFail(FakeEsiTimeout)(EsiCallTimeout))
.map: (online, location, ship) =>
val prevSystemId = prevState match
case is: CharacterLocationState.InSystem => Some(is.system)
Expand Down
27 changes: 26 additions & 1 deletion ui/src/main/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,8 @@ $box-height: 40px;
/*flex: unset; !* TODO not sure ? *!*/
/*width: 70%;*/
/*width: 500px;*/
min-height: 600px;
min-height: 60vh;
height: 60vh;
/*height: 500px;*/
overflow: auto;
box-sizing: content-box;
Expand Down Expand Up @@ -271,6 +272,30 @@ mark.system-online-chars {

span.wormhole-connection-option {
margin: 1px 0 1px 0;

/* TODO this is a mess - use @mixin wormhole-type-cell */
}

span.wormhole-connection-option.wormhole-eol {
& :first-child {
border-top: 2px solid $pink-dark;
border-bottom: 2px solid $pink-dark;
}
}

td.signature-type {
/* FIXME why a div and lowercase ... */
& div[data-wormhole-type="unknown"] {
color: $red;
text-decoration: $red-dark dotted underline;
}
}

td.signature-target {
& span[data-connection-type="Unknown"] {
color: $red;
text-decoration: $red-dark dotted underline;
}
}

span.connection-system-name {
Expand Down
4 changes: 2 additions & 2 deletions ui/src/main/css/components/option-dropdown.css
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,11 @@ div.option-dropdown {
}

& li:hover, & input:focus ~ label {
background-color: $gray-lighter;
background-color: $gray-light;
}

& input:checked ~ label {
background-color: $orange-dark;
background-color: $orange-darkest;
}

& li label {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,14 @@ class MapController(rds: ReferenceDataStore, val clock: Signal[Instant])(using O
override def now = self.clock
override def userPreferences = self.userPreferences

override def systemName(systemId: Long) = self.allSystems.signal.map(_.get(systemId).flatMap(_.system.name))
override def connection(id: ConnectionId) = self.allConnections.signal.map(_.get(id))

def clear(): Unit =
Var.set(
allSystems.current -> Map.empty,
allConnections.current -> Map.empty,
allLocations.current -> Map.empty,
selectedSystemId -> Option.empty,
selectedConnectionId -> Option.empty
)
Expand Down Expand Up @@ -274,7 +278,7 @@ class MapController(rds: ReferenceDataStore, val clock: Signal[Instant])(using O
)

object MapController:
private val DefaultMapSettings = MapSettings(staleScanThreshold = Duration.ofHours(24))
private val DefaultMapSettings = MapSettings(staleScanThreshold = Duration.ofHours(6))

private inline def updateConnectionById(
connections: Array[MapWormholeConnection],
Expand Down
3 changes: 1 addition & 2 deletions ui/src/main/scala/controltower/page/map/view/MapView.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import org.updraft0.controltower.constant
import org.updraft0.controltower.protocol.*

import java.time.Instant
import scala.annotation.unused
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.concurrent.duration.*
Expand All @@ -24,7 +23,7 @@ given equalEventTarget[El <: org.scalajs.dom.Element]: CanEqual[org.scalajs.dom.
private class MapView(
viewId: Int,
page: Page.Map,
@unused ct: ControlTowerBackend,
ct: ControlTowerBackend,
rds: ReferenceDataStore,
ws: WebSocket[MapMessage, MapRequest],
time: Signal[Instant]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package controltower.page.map.view

import com.raquo.laminar.api.L.*
import controltower.page.map.MapAction
import org.updraft0.controltower.constant.CharacterId
import org.updraft0.controltower.protocol.{MapRole, UserPreferences}
import org.updraft0.controltower.constant.{SystemId as _, *}
import org.updraft0.controltower.protocol.{MapRole, MapWormholeConnectionWithSigs, SystemId, UserPreferences}

import java.time.Instant

Expand All @@ -14,3 +14,5 @@ trait MapViewContext:
def staticData: SystemStaticData
def now: Signal[Instant]
def userPreferences: Signal[UserPreferences]
def systemName(id: SystemId): Signal[Option[String]]
def connection(id: ConnectionId): Signal[Option[MapWormholeConnectionWithSigs]]
Loading

0 comments on commit ac0d80c

Please sign in to comment.