diff --git a/composeApp/src/commonMain/kotlin/com/derakkuma/data/MaintenanceMode.kt b/composeApp/src/commonMain/kotlin/com/derakkuma/data/MaintenanceMode.kt index 874a7a3..ddf97db 100644 --- a/composeApp/src/commonMain/kotlin/com/derakkuma/data/MaintenanceMode.kt +++ b/composeApp/src/commonMain/kotlin/com/derakkuma/data/MaintenanceMode.kt @@ -2,37 +2,64 @@ package com.derakkuma.data import kotlin.time.Clock import kotlin.time.Instant +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.FixedOffsetTimeZone +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.LocalTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.UtcOffset +import kotlinx.datetime.toInstant +import kotlinx.datetime.toLocalDateTime /** - * maimai DX NET maintenance window: 19:00–22:00 UTC daily. + * maimai DX NET maintenance window. + * + * Before 2026-09-01: 19:00–22:00 UTC daily. + * From 2026-09-01: 8:00–9:00 AM PST every day except Tuesdays, + * and 8:00–11:00 AM PST on Tuesdays (PST = fixed UTC-8). + * * During this time the server returns maintenance pages; * the app should show cached (read-only) data and disable all actions. */ object MaintenanceMode { - private const val START_HOUR = 19 // 19:00 UTC - private const val END_HOUR = 22 // 22:00 UTC + private const val LEGACY_START_HOUR_UTC = 19 + private const val LEGACY_END_HOUR_UTC = 22 - fun isActive(): Boolean { - val hour = utcHour() - return hour in START_HOUR until END_HOUR - } + private val PST: TimeZone = FixedOffsetTimeZone(UtcOffset(hours = -8)) + private val NEW_SCHEDULE_START_DATE = LocalDate(2026, 9, 1) + + private const val NEW_START_HOUR = 8 + private const val NEW_END_HOUR = 9 + private const val NEW_END_HOUR_TUESDAY = 11 + + fun isActive(): Boolean = maintenanceEnd(Clock.System.now()) != null /** Seconds remaining until maintenance ends. 0 if not in maintenance. */ fun secondsRemaining(): Long { - if (!isActive()) return 0L val now = Clock.System.now() - val epochSeconds = now.epochSeconds - // Seconds since midnight UTC - val secondsSinceMidnight = epochSeconds % 86400 - // End of maintenance in seconds since midnight - val endSeconds = END_HOUR * 3600L - return (endSeconds - secondsSinceMidnight).coerceAtLeast(0) + val end = maintenanceEnd(now) ?: return 0L + return (end.epochSeconds - now.epochSeconds).coerceAtLeast(0) } - /** Current UTC hour (0–23). */ - private fun utcHour(): Int { - val epochSeconds = Clock.System.now().epochSeconds - // UTC = epoch seconds; hour = (seconds / 3600) % 24 - return ((epochSeconds / 3600) % 24).toInt() + /** The Instant the current maintenance window ends, or null if not in maintenance. */ + private fun maintenanceEnd(now: Instant): Instant? { + val pstNow = now.toLocalDateTime(PST) + return if (pstNow.date >= NEW_SCHEDULE_START_DATE) { + val endHour = + if (pstNow.dayOfWeek == DayOfWeek.TUESDAY) NEW_END_HOUR_TUESDAY else NEW_END_HOUR + val start = LocalDateTime(pstNow.date, LocalTime(NEW_START_HOUR, 0)).toInstant(PST) + val end = LocalDateTime(pstNow.date, LocalTime(endHour, 0)).toInstant(PST) + if (now >= start && now < end) end else null + } else { + val secondsSinceMidnightUtc = now.epochSeconds % 86400 + val startSeconds = LEGACY_START_HOUR_UTC * 3600L + val endSeconds = LEGACY_END_HOUR_UTC * 3600L + if (secondsSinceMidnightUtc in startSeconds until endSeconds) { + Instant.fromEpochSeconds(now.epochSeconds - secondsSinceMidnightUtc + endSeconds) + } else { + null + } + } } }