diff --git a/mosogepsch/build.gradle.kts b/mosogepsch/build.gradle.kts
index 0ce87ec459c91cac481376f1eba18cba054ae013..a53bac27b903776928e91d3646726aab60988890 100644
--- a/mosogepsch/build.gradle.kts
+++ b/mosogepsch/build.gradle.kts
@@ -35,6 +35,8 @@ kotlin {
 
         val jsMain by getting {
             dependencies {
+                val ktor_version = "1.6.5"
+
                 implementation(compose.web.core)
                 implementation(compose.runtime)
                 implementation("app.softwork:bootstrap-compose:0.0.49")
@@ -42,6 +44,9 @@ kotlin {
                 implementation(npm("@js-joda/timezone", "2.3.0"))
                 implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.2.1")
                 implementation(npm("no-darkreader", "1.0.1"))
+                implementation("io.ktor:ktor-client-core:$ktor_version")
+                implementation("io.ktor:ktor-client-js:$ktor_version")
+                implementation("io.ktor:ktor-client-serialization:$ktor_version")
             }
         }
         val jsTest by getting {
diff --git a/mosogepsch/src/jsMain/kotlin/Main.kt b/mosogepsch/src/jsMain/kotlin/Main.kt
index f698cd5d3678c205c108e3d88410df44e26acd0d..a4ca2aa90118c6b86ba27faa9bf1a042a52eb620 100644
--- a/mosogepsch/src/jsMain/kotlin/Main.kt
+++ b/mosogepsch/src/jsMain/kotlin/Main.kt
@@ -39,6 +39,7 @@ fun main() {
         Style(mosogepStyle)
         Style(CardStyle)
         Style(FloorStyle)
+        Style(SpinnerStyle)
         CompositionLocalProvider(LocalStyle provides mosogepStyle, LocalLang provides lang) {
             Header {
                 Navbar(
diff --git a/mosogepsch/src/jsMain/kotlin/api/Api.kt b/mosogepsch/src/jsMain/kotlin/api/Api.kt
index 72ecb6bf62d5d48ae8fcb954aa909873f07603a1..d88764b4c341e243dd7eb000c87872c8d0cec5e0 100644
--- a/mosogepsch/src/jsMain/kotlin/api/Api.kt
+++ b/mosogepsch/src/jsMain/kotlin/api/Api.kt
@@ -1,198 +1,46 @@
 package api
 
+import io.ktor.client.*
+import io.ktor.client.engine.js.*
+import io.ktor.client.features.json.*
+import io.ktor.client.features.json.serializer.*
+import io.ktor.client.request.*
+import io.ktor.client.response.*
+import kotlinx.browser.window
+import kotlinx.coroutines.delay
+import kotlinx.datetime.Clock
+import kotlinx.datetime.Instant
 import kotlinx.serialization.ExperimentalSerializationApi
-import kotlinx.serialization.decodeFromString
 import kotlinx.serialization.json.Json
 
 
 object Api {
+    val client = HttpClient(Js) {
+        install(JsonFeature) { serializer = KotlinxSerializer() }
+    }
+
     private val json = Json {
         ignoreUnknownKeys = true
     }
 
-    @OptIn(ExperimentalSerializationApi::class)
-    fun getOnce(): Response {
-        val jsonStr = """        
-{
-  "floors": [
-    {
-      "id": 13,
-      "machines": [
-        {
-          "id": 1301,
-          "kindOf": "Dryer",
-          "status": "NotAvailable",
-          "lastQueryTime": "Thu Feb 17 12:55:38 UTC 2022",
-          "unixTimeStamp": 1645102538897
-        },
-        {
-          "id": 1302,
-          "kindOf": "Washer",
-          "status": "Available",
-          "lastQueryTime": "Thu Feb 17 12:55:38 UTC 2022",
-          "unixTimeStamp": 1645102538897
-        }
-      ]
-    },
-    {
-      "id": 3,
-      "machines": [
-        {
-          "id": 301,
-          "kindOf": "Dryer",
-          "status": "Available",
-          "lastQueryTime": "Thu Feb 17 12:55:38 UTC 2022",
-          "unixTimeStamp": 1645102538966
-        },
-        {
-          "id": 302,
-          "kindOf": "Washer",
-          "status": "NotAvailable",
-          "lastQueryTime": "Thu Feb 17 12:55:38 UTC 2022",
-          "unixTimeStamp": 1645102538966
-        }
-      ]
-    },
-    {
-      "id": 15,
-      "machines": [
-        {
-          "id": 1501,
-          "kindOf": "Dryer",
-          "status": "NotAvailable",
-          "lastQueryTime": "Thu Feb 17 12:55:38 UTC 2022",
-          "unixTimeStamp": 1645102538951
-        },
-        {
-          "id": 1502,
-          "kindOf": "Washer",
-          "status": "NotAvailable",
-          "lastQueryTime": "Thu Feb 17 12:55:38 UTC 2022",
-          "unixTimeStamp": 1645102538951
-        }
-      ]
-    },
-    {
-      "id": 17,
-      "machines": [
-        {
-          "id": 1701,
-          "kindOf": "Dryer",
-          "status": "NotAvailable",
-          "lastQueryTime": "Thu Feb 17 12:55:38 UTC 2022",
-          "unixTimeStamp": 1645102538804
-        },
-        {
-          "id": 1702,
-          "kindOf": "Washer",
-          "status": "Available",
-          "lastQueryTime": "Thu Feb 17 12:55:38 UTC 2022",
-          "unixTimeStamp": 1645102538804
-        }
-      ]
-    },
-    {
-      "id": 2,
-      "machines": [
-        {
-          "id": 201,
-          "kindOf": "Dryer",
-          "status": "Available",
-          "lastQueryTime": "Thu Feb 17 12:55:37 UTC 2022",
-          "unixTimeStamp": 1645102537554
-        },
-        {
-          "id": 202,
-          "kindOf": "Washer",
-          "status": "Available",
-          "lastQueryTime": "Thu Feb 17 12:55:37 UTC 2022",
-          "unixTimeStamp": 1645102537555
-        }
-      ]
-    },
-    {
-      "id": 9,
-      "machines": [
-        {
-          "id": 901,
-          "kindOf": "Dryer",
-          "status": "Available",
-          "lastQueryTime": "Thu Feb 17 12:55:38 UTC 2022",
-          "unixTimeStamp": 1645102538994
-        },
-        {
-          "id": 902,
-          "kindOf": "Washer",
-          "status": "NotAvailable",
-          "lastQueryTime": "Thu Feb 17 12:55:38 UTC 2022",
-          "unixTimeStamp": 1645102538994
-        }
-      ]
-    },
-    {
-      "id": 7,
-      "machines": [
-        {
-          "id": 701,
-          "kindOf": "Dryer",
-          "status": "Available",
-          "lastQueryTime": "Thu Feb 17 12:55:38 UTC 2022",
-          "unixTimeStamp": 1645102538998
-        },
-        {
-          "id": 702,
-          "kindOf": "Washer",
-          "status": "Available",
-          "lastQueryTime": "Thu Feb 17 12:55:38 UTC 2022",
-          "unixTimeStamp": 1645102538998
-        }
-      ]
-    },
-    {
-      "id": 11,
-      "machines": [
-        {
-          "id": 1101,
-          "kindOf": "Dryer",
-          "status": "NotAvailable",
-          "lastQueryTime": "Thu Feb 17 12:55:38 UTC 2022",
-          "unixTimeStamp": 1645102538516
-        },
-        {
-          "id": 1102,
-          "kindOf": "Washer",
-          "status": "NotAvailable",
-          "lastQueryTime": "Thu Feb 17 12:55:38 UTC 2022",
-          "unixTimeStamp": 1645102538516
-        }
-      ]
-    },
-    {
-      "id": 10,
-      "machines": [
-        {
-          "id": 1001,
-          "kindOf": "Dryer",
-          "status": "Available",
-          "lastQueryTime": "Thu Feb 17 12:55:37 UTC 2022",
-          "unixTimeStamp": 1645102537429
-        },
-        {
-          "id": 1002,
-          "kindOf": "Washer",
-          "status": "Available",
-          "lastQueryTime": "Thu Feb 17 12:55:37 UTC 2022",
-          "unixTimeStamp": 1645102537429
-        }
-      ]
+    var lastRequestTime: Instant = Instant.DISTANT_PAST
+        private set
+
+    private val url = if (window.location.host.contains("localhost")) {
+        "https://mosogep-ng.sch.bme.hu/api/v2"
+    } else {
+        "/api/v2"
     }
-  ]
-}
-    """.trimIndent()
-        val dec = json.decodeFromString<Response>(jsonStr)
-        val floors = dec.floors.map { floor ->
+
+    @OptIn(ExperimentalSerializationApi::class)
+    suspend fun getOnce(): Response {
+        lastRequestTime = Clock.System.now()
+
+        val resp: Response = client.request(url)
+
+        val floors = resp.floors.map { floor ->
             floor.copy(machines = floor.machines.sortedBy { it.kindOf })
         }.sortedBy { it.id }
-        return dec.copy(floors = floors)
+        return resp.copy(floors = floors)
     }
 }
\ No newline at end of file
diff --git a/mosogepsch/src/jsMain/kotlin/api/Types.kt b/mosogepsch/src/jsMain/kotlin/api/Types.kt
index 3388deed41cbdfdd3847e3b7bafca7773158159f..af7e945076be9f195173bf1542cd3d87dd516bf1 100644
--- a/mosogepsch/src/jsMain/kotlin/api/Types.kt
+++ b/mosogepsch/src/jsMain/kotlin/api/Types.kt
@@ -1,14 +1,7 @@
 package api
 
 import kotlinx.datetime.Instant
-import kotlinx.serialization.KSerializer
-import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
-import kotlinx.serialization.descriptors.PrimitiveKind
-import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
-import kotlinx.serialization.descriptors.SerialDescriptor
-import kotlinx.serialization.encoding.Decoder
-import kotlinx.serialization.encoding.Encoder
 
 @Serializable
 data class Response(
@@ -26,10 +19,8 @@ data class Machine(
     val id: Int,
     val kindOf: MachineKind,
     var status: MachineStatus,
-
-    @SerialName("unixTimeStamp")
-    @Serializable(with = UnixDateSerializer::class)
     var lastQueryTime: Instant,
+    var lastChanged: Instant,
 )
 
 @Serializable
@@ -37,10 +28,8 @@ enum class MachineKind { Dryer, Washer }
 @Serializable
 enum class MachineStatus { NotAvailable, Available }
 
-object UnixDateSerializer : KSerializer<Instant> {
-    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.LONG)
-
-    override fun serialize(encoder: Encoder, value: Instant) = encoder.encodeLong(value.epochSeconds)
-
-    override fun deserialize(decoder: Decoder): Instant = Instant.fromEpochMilliseconds(decoder.decodeLong())
+@OptIn(kotlin.time.ExperimentalTime::class)
+val Machine.isLostInSpace: Boolean get() {
+    val diff = Api.lastRequestTime - lastQueryTime
+    return diff.inWholeMinutes > 10
 }
\ No newline at end of file
diff --git a/mosogepsch/src/jsMain/kotlin/components/Content.kt b/mosogepsch/src/jsMain/kotlin/components/Content.kt
index 472beb7e2475660e1abcd7dc09acd74a85624710..a755213c896c753409c309d1ed9ec974b015b97e 100644
--- a/mosogepsch/src/jsMain/kotlin/components/Content.kt
+++ b/mosogepsch/src/jsMain/kotlin/components/Content.kt
@@ -3,15 +3,23 @@ package components
 import androidx.compose.runtime.*
 import api.Api
 import api.Machine
+import api.Response
 import app.softwork.bootstrapcompose.Container
+import kotlinx.coroutines.launch
 import org.jetbrains.compose.web.css.*
 import org.jetbrains.compose.web.dom.Div
+import org.jetbrains.compose.web.dom.Text
 
 @Composable
 fun content() {
-    val data by remember { mutableStateOf(Api.getOnce()) }
+    var data by remember { mutableStateOf<Response?>(null) }
     var machine by remember { mutableStateOf<Machine?>(null) }
 
+    val scope = rememberCoroutineScope()
+    LaunchedEffect(Unit) {
+        scope.launch { data = Api.getOnce() }
+    }
+
     Container(attrs = {
         style {
             display(DisplayStyle.Flex)
@@ -21,9 +29,13 @@ fun content() {
             paddingTop(1.em)
         }
     }) {
-        data.floors.forEach {
-            floor(it, machine) { m ->
-                machine = m
+        if (data == null ) {
+            spinner()
+        } else {
+            data!!.floors.forEach {
+                floor(it, machine) { m ->
+                    machine = m
+                }
             }
         }
     }
diff --git a/mosogepsch/src/jsMain/kotlin/components/FloorCard.kt b/mosogepsch/src/jsMain/kotlin/components/FloorCard.kt
index b37c80aaf3e6935cf387511473f2cab17f2ca0e6..e6c43510bb1fed3ea4a344fea24752ebe775ce33 100644
--- a/mosogepsch/src/jsMain/kotlin/components/FloorCard.kt
+++ b/mosogepsch/src/jsMain/kotlin/components/FloorCard.kt
@@ -25,7 +25,7 @@ fun FloorCard(floor: Floor, selectedMachine: Machine?, setMachine: (Machine?)->U
             classes(FloorStyle.floorNum)
         }) { Text("${floor.id}.") }
         floor.machines.forEach { machine ->
-            val cardColor = machine.status.toColor()
+            val cardColor = machine.toColor()
             card(attrs = {
                 classes(FloorStyle.machine)
                 style {
diff --git a/mosogepsch/src/jsMain/kotlin/components/Spinner.kt b/mosogepsch/src/jsMain/kotlin/components/Spinner.kt
new file mode 100644
index 0000000000000000000000000000000000000000..bd9adbc73994c4b983d1bb167b5284197815df3f
--- /dev/null
+++ b/mosogepsch/src/jsMain/kotlin/components/Spinner.kt
@@ -0,0 +1,51 @@
+package components
+
+import androidx.compose.runtime.Composable
+import org.jetbrains.compose.web.ExperimentalComposeWebApi
+import org.jetbrains.compose.web.css.*
+import org.jetbrains.compose.web.dom.Div
+import styles.ColorPair
+import styles.LocalStyle
+
+@OptIn(ExperimentalComposeWebApi::class)
+object SpinnerStyle: StyleSheet() {
+    val spin by keyframes {
+        from {
+            transform { rotate(0.deg) }
+        }
+        to {
+            transform { rotate(360.deg) }
+        }
+    }
+    val spinner by style {
+        borderRadius(50.percent)
+        property("border", ".5em solid #f3f3f3")
+        property("border-top", ".5em solid #3498db")
+        width(3.em)
+        height(3.em)
+        animation(spin) {
+            duration(1.s)
+            timingFunction(AnimationTimingFunction.Linear)
+            iterationCount(null)
+        }
+    }
+}
+
+@Composable
+fun spinner() {
+    val dark = LocalStyle.current.dark
+    val colors = LocalStyle.current.colors
+
+    val color = ColorPair(
+        bg = if (dark) colors.onPrimary else colors.surfaceVariant,
+        fg = colors.primary
+    )
+
+    Div(attrs = {
+        classes(SpinnerStyle.spinner)
+        style {
+            property("border", ".5em solid ${color.bg}")
+            property("border-top", ".5em solid ${color.fg}")
+        }
+    })
+}
\ No newline at end of file
diff --git a/mosogepsch/src/jsMain/kotlin/components/UnderCard.kt b/mosogepsch/src/jsMain/kotlin/components/UnderCard.kt
index aa675675ff8c87425be0e8341c86493800f7d609..c5238565bd3a5380a33ebec38c5597709d4e0901 100644
--- a/mosogepsch/src/jsMain/kotlin/components/UnderCard.kt
+++ b/mosogepsch/src/jsMain/kotlin/components/UnderCard.kt
@@ -19,9 +19,8 @@ import styles.applyColorPair
 @Composable
 fun underCard(floor: Floor, machine: Machine, selectedMachine: Machine?) {
     val lang = LocalLang.current
-    val cardColor = machine.status.toUnderColor()
-    val subbedColor = subbedColor()
-    val unsubbedColor = unsubbedColor()
+    val cardColor = machine.toUnderColor()
+    val btnColor = machine.toColor()
 
     card(attrs = {
         classes(FloorStyle.underflowCard)
@@ -34,17 +33,23 @@ fun underCard(floor: Floor, machine: Machine, selectedMachine: Machine?) {
     }) {
         H5 { Text("${lang.machineKind(machine.kindOf)} ${lang.floorFormat(floor.id)}") }
         P { Text("${lang.lastUpdated} ${lang.formatInstant(machine.lastQueryTime)}") }
+        P { Text("${lang.lastChanged} ${lang.formatInstant(machine.lastChanged)}") }
         P { Text(lang.machineStatus(machine.status)) }
         var subbed by remember { mutableStateOf(false) }
         card(attrs = {
             classes(FloorStyle.subBtn)
             onClick { subbed = !subbed }
             style {
-                applyColorPair(if (subbed) subbedColor else unsubbedColor)
+                applyColorPair(if (!subbed) btnColor else btnColor.invert())
             }
         }) {
             // TODO place icon
             Text(if (subbed) lang.unsub else lang.sub)
         }
     }
-}
\ No newline at end of file
+}
+
+fun ColorPair.invert(): ColorPair = ColorPair(
+    bg = fg,
+    fg = bg,
+)
\ No newline at end of file
diff --git a/mosogepsch/src/jsMain/kotlin/components/Utils.kt b/mosogepsch/src/jsMain/kotlin/components/Utils.kt
index 187121d7231e2e671919b819f3a3c0953d496b32..d3c30a329bf03f68751be94ce44424e44b90463f 100644
--- a/mosogepsch/src/jsMain/kotlin/components/Utils.kt
+++ b/mosogepsch/src/jsMain/kotlin/components/Utils.kt
@@ -1,24 +1,32 @@
 package components
 
 import androidx.compose.runtime.Composable
+import api.Machine
 import api.MachineKind
 import api.MachineStatus
+import api.isLostInSpace
 import styles.ColorPair
 import styles.LocalStyle
 
 @Composable
-fun MachineStatus.toColor(): ColorPair {
+fun Machine.toColor(): ColorPair {
     val colors = LocalStyle.current.colors
-    return when (this) {
+    if (isLostInSpace) {
+        return ColorPair(colors.background, colors.onBackground)
+    }
+    return when (status) {
         MachineStatus.Available -> ColorPair(colors.primary, colors.onPrimary)
         MachineStatus.NotAvailable -> ColorPair(colors.error, colors.onError)
     }
 }
 
 @Composable
-fun MachineStatus.toUnderColor(): ColorPair {
+fun Machine.toUnderColor(): ColorPair {
     val colors = LocalStyle.current.colors
-    return when (this) {
+    if (isLostInSpace) {
+        return ColorPair(colors.surfaceVariant, colors.onSurfaceVariant)
+    }
+    return when (status) {
         MachineStatus.Available -> ColorPair(colors.primaryContainer, colors.onPrimaryContainer)
         MachineStatus.NotAvailable -> ColorPair(colors.errorContainer, colors.onErrorContainer)
     }
diff --git a/mosogepsch/src/jsMain/kotlin/localization/English.kt b/mosogepsch/src/jsMain/kotlin/localization/English.kt
index 01faadf3f0b0e9ffc1e6f4fc0fb00b746012c4cc..f420af110e477370fb8e62774bce3abd5fb9485b 100644
--- a/mosogepsch/src/jsMain/kotlin/localization/English.kt
+++ b/mosogepsch/src/jsMain/kotlin/localization/English.kt
@@ -15,6 +15,7 @@ abstract class Localization {
     open val notAvailable = "Currently in use"
 
     open val lastUpdated = "Last updated"
+    open val lastChanged = "Last changed"
     open val daysAgo = "days ago"
 
     open val sub = "Subscribe"
diff --git a/mosogepsch/src/jsMain/kotlin/localization/Hungarian.kt b/mosogepsch/src/jsMain/kotlin/localization/Hungarian.kt
index f91015df24095f55e88dceb19532251a67a1a9a5..f68ed56f735d3b2339af433eef5bd83874e22cb0 100644
--- a/mosogepsch/src/jsMain/kotlin/localization/Hungarian.kt
+++ b/mosogepsch/src/jsMain/kotlin/localization/Hungarian.kt
@@ -10,6 +10,7 @@ class Hungarian: Localization() {
     override val notAvailable = "Jelenleg használatban"
 
     override val lastUpdated = "Utoljára frissítve"
+    override val lastChanged = "Utoljára megváltozott"
     override val daysAgo = "napja"
 
     override val sub = "Feliratkozás"
diff --git a/mosogepsch/src/jsMain/kotlin/styles/Style.kt b/mosogepsch/src/jsMain/kotlin/styles/Style.kt
index 20199d2666cf580191e04eb28381e76d151132e6..3d2ddae75daca08a68bcf5e58402bfaaeeddb3e8 100644
--- a/mosogepsch/src/jsMain/kotlin/styles/Style.kt
+++ b/mosogepsch/src/jsMain/kotlin/styles/Style.kt
@@ -8,7 +8,7 @@ class Style(val dark: Boolean = false): StyleSheet() {
     val colors = if (dark) DarkThemeColors else LightThemeColors
 
     companion object {
-        val baseTransition = "background-color 0.75s, color 0.75s"
+        val baseTransition = "background-color 0.75s, color 0.75s, border 0.75s, border-top 0.75s"
         private val footHeight = 4.em
     }