diff --git a/ios/ChronoMind/ChronoMind.entitlements b/ios/ChronoMind/ChronoMind.entitlements new file mode 100644 index 0000000..cc45b00 --- /dev/null +++ b/ios/ChronoMind/ChronoMind.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.chronomind.shared + + + diff --git a/ios/ChronoMindTests/CascadeTests.swift b/ios/ChronoMindTests/CascadeTests.swift new file mode 100644 index 0000000..fea9714 --- /dev/null +++ b/ios/ChronoMindTests/CascadeTests.swift @@ -0,0 +1,183 @@ +// ── Cascade Tests ────────────────────────────────────────────── +// XCTest unit tests mirroring web cascade.test.ts + +import XCTest +@testable import ChronoMind + +final class CascadeTests: XCTestCase { + + // MARK: - Preset Intervals + + func testAggressivePreset() { + let intervals = CascadePreset.aggressive.defaultIntervals + XCTAssertEqual(intervals, [240, 180, 120, 90, 60, 30, 15, 5, 1]) + } + + func testStandardPreset() { + let intervals = CascadePreset.standard.defaultIntervals + XCTAssertEqual(intervals, [120, 60, 30, 15, 5]) + } + + func testLightPreset() { + let intervals = CascadePreset.light.defaultIntervals + XCTAssertEqual(intervals, [60, 15, 5]) + } + + func testMinimalPreset() { + let intervals = CascadePreset.minimal.defaultIntervals + XCTAssertEqual(intervals, [15]) + } + + func testNonePreset() { + let intervals = CascadePreset.none.defaultIntervals + XCTAssertTrue(intervals.isEmpty) + } + + // MARK: - Calculate Cascade Warnings + + func testCalculateWarnings() { + let now = Date() + let targetTime = now.addingTimeInterval(3600) // 1h from now + let intervals = [60, 30, 15, 5] + + let warnings = calculateCascadeWarnings(targetTime: targetTime, intervals: intervals, now: now) + + XCTAssertEqual(warnings.count, 4) + // Should be sorted largest first + XCTAssertEqual(warnings[0].minutesBefore, 60) + XCTAssertEqual(warnings[1].minutesBefore, 30) + XCTAssertEqual(warnings[2].minutesBefore, 15) + XCTAssertEqual(warnings[3].minutesBefore, 5) + } + + func testWarningsInPastAreFired() { + let now = Date() + let targetTime = now.addingTimeInterval(600) // 10 min from now + let intervals = [120, 60, 15, 5] // 2h and 1h are in the past + + let warnings = calculateCascadeWarnings(targetTime: targetTime, intervals: intervals, now: now) + + // 120min and 60min warnings should be marked as fired (they're in the past) + let fired = warnings.filter(\.fired) + XCTAssertEqual(fired.count, 2) + XCTAssertTrue(warnings[0].fired) // 120m + XCTAssertTrue(warnings[1].fired) // 60m + XCTAssertFalse(warnings[2].fired) // 15m — still in the future + XCTAssertFalse(warnings[3].fired) // 5m — still in the future + } + + func testWarningScheduledTimes() { + let now = Date() + let targetTime = now.addingTimeInterval(7200) // 2h from now + let intervals = [60, 30] + + let warnings = calculateCascadeWarnings(targetTime: targetTime, intervals: intervals, now: now) + + // 60min warning should be at targetTime - 60min + let expected60 = targetTime.addingTimeInterval(-3600) + XCTAssertEqual(warnings[0].scheduledTime.timeIntervalSince1970, + expected60.timeIntervalSince1970, accuracy: 1.0) + + // 30min warning should be at targetTime - 30min + let expected30 = targetTime.addingTimeInterval(-1800) + XCTAssertEqual(warnings[1].scheduledTime.timeIntervalSince1970, + expected30.timeIntervalSince1970, accuracy: 1.0) + } + + func testEmptyIntervals() { + let now = Date() + let targetTime = now.addingTimeInterval(3600) + let warnings = calculateCascadeWarnings(targetTime: targetTime, intervals: [], now: now) + + XCTAssertTrue(warnings.isEmpty) + } + + // MARK: - Get Next Warning + + func testGetNextWarning() { + let now = Date() + let targetTime = now.addingTimeInterval(3600) + let warnings = calculateCascadeWarnings( + targetTime: targetTime, + intervals: [60, 30, 15, 5], + now: now + ) + + let next = getNextWarning(warnings) + XCTAssertNotNil(next) + XCTAssertEqual(next?.minutesBefore, 60) + } + + func testGetNextWarningAllFired() { + let now = Date() + let targetTime = now.addingTimeInterval(60) // 1 min from now + var warnings = calculateCascadeWarnings( + targetTime: targetTime, + intervals: [120, 60, 30], + now: now + ) + // Mark all as fired + for i in warnings.indices { warnings[i].fired = true } + + let next = getNextWarning(warnings) + XCTAssertNil(next) + } + + // MARK: - Check Warnings + + func testCheckWarnings() { + let now = Date() + let targetTime = now.addingTimeInterval(3600) + var warnings = calculateCascadeWarnings( + targetTime: targetTime, + intervals: [60, 30, 15, 5], + now: now + ) + + // Check at a time when 60m warning should fire (targetTime - 60min = now) + let checkTime = targetTime.addingTimeInterval(-3600) // exactly at 60m warning + let fired = checkWarnings(&warnings, now: checkTime.addingTimeInterval(1)) // 1 second after + + // The 60m warning should fire + XCTAssertTrue(fired.count >= 1) + } + + func testCheckWarningsNoneFire() { + let now = Date() + let targetTime = now.addingTimeInterval(7200) // 2h from now + var warnings = calculateCascadeWarnings( + targetTime: targetTime, + intervals: [60, 30, 15], + now: now + ) + + // Check at current time — no warnings should fire yet (earliest is in 1h) + let fired = checkWarnings(&warnings, now: now) + XCTAssertTrue(fired.isEmpty) + } + + // MARK: - Get Cascade Intervals + + func testGetCascadeIntervalsPreset() { + let config = CascadeConfig(preset: .standard, intervals: []) + let intervals = getCascadeIntervals(config) + XCTAssertEqual(intervals, [120, 60, 30, 15, 5]) + } + + func testGetCascadeIntervalsCustom() { + let config = CascadeConfig(preset: .custom, intervals: [10, 45, 5, 30]) + let intervals = getCascadeIntervals(config) + XCTAssertEqual(intervals, [45, 30, 10, 5]) // sorted descending + } + + // MARK: - Format Minutes Before + + func testFormatMinutesBefore() { + XCTAssertEqual(formatMinutesBefore(5), "5m") + XCTAssertEqual(formatMinutesBefore(15), "15m") + XCTAssertEqual(formatMinutesBefore(60), "1h") + XCTAssertEqual(formatMinutesBefore(90), "1h 30m") + XCTAssertEqual(formatMinutesBefore(120), "2h") + XCTAssertEqual(formatMinutesBefore(240), "4h") + } +} diff --git a/ios/ChronoMindTests/FormatTests.swift b/ios/ChronoMindTests/FormatTests.swift new file mode 100644 index 0000000..e0fd24b --- /dev/null +++ b/ios/ChronoMindTests/FormatTests.swift @@ -0,0 +1,112 @@ +// ── Format & Time Blindness Tests ────────────────────────────── + +import XCTest +@testable import ChronoMind + +final class FormatTests: XCTestCase { + + // MARK: - Format Duration + + func testFormatDurationZero() { + XCTAssertEqual(formatDuration(0), "00:00") + XCTAssertEqual(formatDuration(-5), "00:00") + } + + func testFormatDurationSeconds() { + XCTAssertEqual(formatDuration(30), "00:30") + XCTAssertEqual(formatDuration(59), "00:59") + } + + func testFormatDurationMinutes() { + XCTAssertEqual(formatDuration(60), "01:00") + XCTAssertEqual(formatDuration(90), "01:30") + XCTAssertEqual(formatDuration(600), "10:00") + XCTAssertEqual(formatDuration(1500), "25:00") + } + + func testFormatDurationHours() { + XCTAssertEqual(formatDuration(3600), "01:00:00") + XCTAssertEqual(formatDuration(5400), "01:30:00") + XCTAssertEqual(formatDuration(7200), "02:00:00") + } + + // MARK: - Format Duration Compact + + func testFormatDurationCompactZero() { + XCTAssertEqual(formatDurationCompact(0), "0s") + XCTAssertEqual(formatDurationCompact(-1), "0s") + } + + func testFormatDurationCompactSeconds() { + XCTAssertEqual(formatDurationCompact(30), "30s") + XCTAssertEqual(formatDurationCompact(59), "59s") + } + + func testFormatDurationCompactMinutes() { + XCTAssertEqual(formatDurationCompact(60), "1m") + XCTAssertEqual(formatDurationCompact(150), "2m 30s") + XCTAssertEqual(formatDurationCompact(600), "10m") + XCTAssertEqual(formatDurationCompact(1500), "25m") + } + + func testFormatDurationCompactHours() { + XCTAssertEqual(formatDurationCompact(3600), "1h") + XCTAssertEqual(formatDurationCompact(5400), "1h 30m") + XCTAssertEqual(formatDurationCompact(7200), "2h") + } + + // MARK: - Format Relative Time + + func testFormatRelativeTimeNow() { + let now = Date() + XCTAssertEqual(formatRelativeTime(now, now: now), "now") + XCTAssertEqual(formatRelativeTime(now.addingTimeInterval(10), now: now), "now") + } + + func testFormatRelativeTimeFuture() { + let now = Date() + let result = formatRelativeTime(now.addingTimeInterval(300), now: now) // 5 min + XCTAssertTrue(result.hasPrefix("in ")) + } + + func testFormatRelativeTimePast() { + let now = Date() + let result = formatRelativeTime(now.addingTimeInterval(-300), now: now) // 5 min ago + XCTAssertTrue(result.hasSuffix(" ago")) + } + + // MARK: - Time Blindness + + func testTimeReferenceZero() { + XCTAssertNil(getTimeReference(minutes: 0)) + XCTAssertNil(getTimeReference(minutes: -1)) + } + + func testTimeReferenceShort() { + let ref = getTimeReference(minutes: 1) + XCTAssertNotNil(ref) + XCTAssertTrue(ref!.contains("deep breath")) + } + + func testTimeReferencePomodoro() { + let ref = getTimeReference(minutes: 25) + XCTAssertNotNil(ref) + XCTAssertTrue(ref!.contains("Pomodoro")) + } + + func testTimeReferenceLong() { + let ref = getTimeReference(minutes: 90) + XCTAssertNotNil(ref) + XCTAssertTrue(ref!.contains("movie")) + } + + func testTimeReferenceTooLong() { + XCTAssertNil(getTimeReference(minutes: 1000)) + } + + func testTimeReferenceFromSeconds() { + let ref = getTimeReference(seconds: 1500) // 25 min + XCTAssertNotNil(ref) + XCTAssertTrue(ref!.contains("Pomodoro")) + } +} diff --git a/ios/ChronoMindTests/TimerEngineTests.swift b/ios/ChronoMindTests/TimerEngineTests.swift new file mode 100644 index 0000000..fa702b6 --- /dev/null +++ b/ios/ChronoMindTests/TimerEngineTests.swift @@ -0,0 +1,307 @@ +// ── Timer Engine Tests ───────────────────────────────────────── +// XCTest unit tests mirroring web Vitest tests + +import XCTest +@testable import ChronoMind + +final class TimerEngineTests: XCTestCase { + + // MARK: - Create Alarm + + func testCreateAlarm() { + let target = Date().addingTimeInterval(3600) // 1h from now + let timer = createAlarm(CreateAlarmParams(label: "Test Alarm", targetTime: target)) + + XCTAssertEqual(timer.type, .alarm) + XCTAssertEqual(timer.label, "Test Alarm") + XCTAssertEqual(timer.urgency, .standard) + XCTAssertEqual(timer.state, .active) + XCTAssertNotNil(timer.startedAt) + XCTAssertEqual(timer.snoozeCount, 0) + XCTAssertFalse(timer.id.isEmpty) + } + + func testCreateAlarmWithUrgency() { + let target = Date().addingTimeInterval(7200) + let timer = createAlarm(CreateAlarmParams( + label: "Critical Meeting", + targetTime: target, + urgency: .critical, + cascade: CascadeConfig(preset: .aggressive, intervals: []) + )) + + XCTAssertEqual(timer.urgency, .critical) + XCTAssertEqual(timer.cascade.preset, .aggressive) + XCTAssertFalse(timer.warnings.isEmpty) + } + + // MARK: - Create Countdown + + func testCreateCountdown() { + let timer = createCountdown(CreateCountdownParams( + label: "Pasta", + durationSeconds: 600 // 10 min + )) + + XCTAssertEqual(timer.type, .countdown) + XCTAssertEqual(timer.label, "Pasta") + XCTAssertEqual(timer.duration, 600) + XCTAssertEqual(timer.state, .active) + XCTAssert(timer.targetTime > Date()) + } + + func testCreateCountdownWithCustomCascade() { + let timer = createCountdown(CreateCountdownParams( + label: "Test", + durationSeconds: 3600, + cascade: CascadeConfig(preset: .custom, intervals: [30, 15, 5]) + )) + + // Custom intervals should produce warnings + let unfired = timer.warnings.filter { !$0.fired } + XCTAssertEqual(unfired.count, 3) + } + + // MARK: - Create Pomodoro + + func testCreatePomodoro() { + let timer = createPomodoro() + + XCTAssertEqual(timer.type, .pomodoro) + XCTAssertEqual(timer.label, "Focus Session") + XCTAssertEqual(timer.state, .active) + XCTAssertEqual(timer.pomodoroConfig?.workMinutes, 25) + XCTAssertEqual(timer.pomodoroConfig?.breakMinutes, 5) + XCTAssertEqual(timer.pomodoroConfig?.rounds, 4) + XCTAssertEqual(timer.pomodoroState?.currentRound, 1) + XCTAssertEqual(timer.pomodoroState?.isBreak, false) + XCTAssertEqual(timer.pomodoroState?.completedRounds, 0) + } + + func testCreatePomodoroCustomConfig() { + let config = PomodoroConfig(workMinutes: 50, breakMinutes: 10, longBreakMinutes: 30, rounds: 3) + let timer = createPomodoro(CreatePomodoroParams( + label: "Deep Work", + config: config + )) + + XCTAssertEqual(timer.label, "Deep Work") + XCTAssertEqual(timer.pomodoroConfig?.workMinutes, 50) + XCTAssertEqual(timer.pomodoroConfig?.rounds, 3) + XCTAssertEqual(timer.duration, 3000) // 50 * 60 + } + + // MARK: - State Transitions + + func testPauseTimer() { + let timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) + let paused = pauseTimer(timer) + + XCTAssertEqual(paused.state, .paused) + XCTAssertNotNil(paused.pausedAt) + } + + func testPauseOnlyActiveOrWarning() { + var timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) + timer.state = .dismissed + let result = pauseTimer(timer) + + XCTAssertEqual(result.state, .dismissed) // unchanged + } + + func testResumeTimer() { + let timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) + let paused = pauseTimer(timer) + let resumed = resumeTimer(paused) + + XCTAssertEqual(resumed.state, .active) + XCTAssertNil(resumed.pausedAt) + XCTAssertNotNil(resumed.startedAt) + XCTAssert(resumed.targetTime > Date()) + } + + func testResumeOnlyPaused() { + let timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) + let result = resumeTimer(timer) // already active, not paused + + XCTAssertEqual(result.state, .active) // unchanged + } + + func testFireTimer() { + let timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) + let fired = fireTimer(timer) + + XCTAssertEqual(fired.state, .firing) + XCTAssertNotNil(fired.firedAt) + } + + func testFireIgnoresDismissed() { + var timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) + timer = dismissTimer(timer) + let result = fireTimer(timer) + + XCTAssertEqual(result.state, .dismissed) + } + + func testSnoozeTimer() { + var timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) + timer = fireTimer(timer) + let snoozed = snoozeTimer(timer, snoozeMinutes: 5) + + XCTAssertEqual(snoozed.state, .snoozed) + XCTAssertEqual(snoozed.snoozeCount, 1) + XCTAssertNotNil(snoozed.snoozedUntil) + + // Snooze until should be ~5 minutes from now + let diff = snoozed.snoozedUntil!.timeIntervalSinceNow + XCTAssert(diff > 290 && diff < 310) // ~300 seconds with tolerance + } + + func testSnoozeOnlyFiring() { + let timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) + let result = snoozeTimer(timer, snoozeMinutes: 5) + + XCTAssertEqual(result.state, .active) // unchanged, not firing + } + + func testDismissTimer() { + let timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) + let dismissed = dismissTimer(timer) + + XCTAssertEqual(dismissed.state, .dismissed) + XCTAssertNotNil(dismissed.dismissedAt) + } + + func testCompleteTimer() { + let timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) + let completed = completeTimer(timer) + + XCTAssertEqual(completed.state, .completed) + XCTAssertNotNil(completed.completedAt) + } + + // MARK: - Pomodoro Transitions + + func testAdvancePomodoroWorkToBreak() { + let timer = createPomodoro() + guard let advanced = advancePomodoro(timer) else { + XCTFail("Should advance") + return + } + + XCTAssertEqual(advanced.state, .active) + XCTAssertEqual(advanced.pomodoroState?.isBreak, true) + XCTAssertEqual(advanced.pomodoroState?.completedRounds, 1) + XCTAssertEqual(advanced.duration, 300) // 5 min break + } + + func testAdvancePomodoroBreakToWork() { + var timer = createPomodoro() + // Advance to break + timer = advancePomodoro(timer)! + // Advance back to work + guard let next = advancePomodoro(timer) else { + XCTFail("Should advance") + return + } + + XCTAssertEqual(next.pomodoroState?.isBreak, false) + XCTAssertEqual(next.pomodoroState?.currentRound, 2) + XCTAssertEqual(next.duration, 1500) // 25 min work + } + + func testAdvancePomodoroToLongBreak() { + var timer = createPomodoro() + // Go through all 4 work rounds + for _ in 1...4 { + timer = advancePomodoro(timer)! // work → break (or long break) + if timer.pomodoroState?.isLongBreak == true || timer.state == .completed { + break + } + if timer.pomodoroState?.isBreak == true { + timer = advancePomodoro(timer)! // break → next work + } + } + + // After 4 rounds, should be on long break + XCTAssertTrue(timer.pomodoroState?.isLongBreak == true || timer.state == .completed) + } + + func testAdvancePomodoroCompletion() { + var timer = createPomodoro(CreatePomodoroParams( + config: PomodoroConfig(workMinutes: 1, breakMinutes: 1, longBreakMinutes: 1, rounds: 1) + )) + + // Work → long break (only 1 round) + timer = advancePomodoro(timer)! + XCTAssertTrue(timer.pomodoroState?.isLongBreak == true) + + // Long break → complete + guard let completed = advancePomodoro(timer) else { + XCTFail("Should complete") + return + } + XCTAssertEqual(completed.state, .completed) + } + + func testAdvancePomodoroReturnsNilForNonPomodoro() { + let timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) + XCTAssertNil(advancePomodoro(timer)) + } + + // MARK: - Utility Functions + + func testGetRemainingSeconds() { + let timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) + let remaining = getRemainingSeconds(timer) + + XCTAssert(remaining > 598 && remaining <= 600) // within 2 seconds + } + + func testGetRemainingSecondsPaused() { + var timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) + // Simulate some elapsed time + timer.elapsedBeforePause = 100 + timer.state = .paused + let remaining = getRemainingSeconds(timer) + + XCTAssertEqual(remaining, 500) // 600 - 100 + } + + func testIsTimerActive() { + var timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) + XCTAssertTrue(isTimerActive(timer)) + + timer.state = .warning + XCTAssertTrue(isTimerActive(timer)) + + timer.state = .snoozed + XCTAssertTrue(isTimerActive(timer)) + + timer.state = .paused + XCTAssertFalse(isTimerActive(timer)) + + timer.state = .dismissed + XCTAssertFalse(isTimerActive(timer)) + } + + func testShouldTimerFire() { + var timer = createCountdown(CreateCountdownParams(label: "Test", durationSeconds: 600)) + let now = Date() + + // Not yet + XCTAssertFalse(shouldTimerFire(timer, now: now)) + + // Past target time + XCTAssertTrue(shouldTimerFire(timer, now: timer.targetTime.addingTimeInterval(1))) + + // Snoozed and past snooze time + timer.state = .snoozed + timer.snoozedUntil = now.addingTimeInterval(-10) + XCTAssertTrue(shouldTimerFire(timer, now: now)) + + // Snoozed but not past snooze time + timer.snoozedUntil = now.addingTimeInterval(300) + XCTAssertFalse(shouldTimerFire(timer, now: now)) + } +} diff --git a/ios/project.yml b/ios/project.yml new file mode 100644 index 0000000..ed70d84 --- /dev/null +++ b/ios/project.yml @@ -0,0 +1,82 @@ +name: ChronoMind +options: + bundleIdPrefix: com.chronomind + deploymentTarget: + iOS: "17.0" + watchOS: "10.0" + macOS: "14.0" + xcodeVersion: "16.0" + createIntermediateGroups: true + generateEmptyDirectories: true + groupSortPosition: top + +settings: + base: + SWIFT_VERSION: "5.9" + TARGETED_DEVICE_FAMILY: "1,2" + DEVELOPMENT_TEAM: 748N7QPX7J + CODE_SIGN_STYLE: Automatic + +targets: + ChronoMind: + type: application + platform: iOS + deploymentTarget: "17.0" + sources: + - path: ChronoMind + excludes: + - "**/.DS_Store" + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: com.chronomind.app + INFOPLIST_GENERATION_MODE: GeneratedFile + MARKETING_VERSION: "1.0.0" + CURRENT_PROJECT_VERSION: "1" + GENERATE_INFOPLIST_FILE: true + INFOPLIST_KEY_UIApplicationSceneManifest_Generation: true + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents: true + INFOPLIST_KEY_UILaunchScreen_Generation: true + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad: "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight" + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone: "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight" + INFOPLIST_KEY_CFBundleDisplayName: ChronoMind + SUPPORTS_MACCATALYST: false + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD: false + entitlements: + path: ChronoMind/ChronoMind.entitlements + properties: + com.apple.security.application-groups: + - group.com.chronomind.shared + + ChronoMindTests: + type: bundle.unit-test + platform: iOS + deploymentTarget: "17.0" + sources: + - path: ChronoMindTests + excludes: + - "**/.DS_Store" + dependencies: + - target: ChronoMind + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: com.chronomind.tests + GENERATE_INFOPLIST_FILE: true + +schemes: + ChronoMind: + build: + targets: + ChronoMind: all + ChronoMindTests: [test] + run: + config: Debug + test: + config: Debug + targets: + - ChronoMindTests + profile: + config: Release + analyze: + config: Debug + archive: + config: Release diff --git a/web/next.config.ts b/web/next.config.ts index e9ffa30..5710675 100644 --- a/web/next.config.ts +++ b/web/next.config.ts @@ -1,7 +1,13 @@ import type { NextConfig } from "next"; +import withSerwistInit from "@serwist/next"; + +const withSerwist = withSerwistInit({ + swSrc: "src/app/sw.ts", + swDest: "public/sw.js", +}); const nextConfig: NextConfig = { /* config options here */ }; -export default nextConfig; +export default withSerwist(nextConfig); diff --git a/web/package-lock.json b/web/package-lock.json index fdc2852..eabe8ea 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8,12 +8,14 @@ "name": "web", "version": "0.1.0", "dependencies": { + "@serwist/next": "^9.5.6", "date-fns": "^4.1.0", "idb": "^8.0.3", "lucide-react": "^0.575.0", "next": "16.1.6", "react": "19.2.3", "react-dom": "19.2.3", + "serwist": "^9.5.6", "uuid": "^13.0.0", "zod": "^4.3.6", "zustand": "^5.0.11" @@ -1672,6 +1674,23 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1927,6 +1946,16 @@ "node": ">=12.4.0" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.59.0", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", @@ -2284,6 +2313,137 @@ "dev": true, "license": "MIT" }, + "node_modules/@serwist/build": { + "version": "9.5.6", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/@serwist/build/-/build-9.5.6.tgz", + "integrity": "sha512-/YUi2BKrvnIkYg8k/PW5N/lAR4N0h/F8eBaqCaDNOy2fdOiNCkvRaWq/ZaoYN5tocvNsMc7OSm7+m1aJqR7trQ==", + "license": "MIT", + "dependencies": { + "@serwist/utils": "9.5.6", + "common-tags": "1.8.2", + "glob": "10.5.0", + "pretty-bytes": "6.1.1", + "source-map": "0.8.0-beta.0", + "zod": "4.3.6" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@serwist/next": { + "version": "9.5.6", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/@serwist/next/-/next-9.5.6.tgz", + "integrity": "sha512-xObhrC3ctSgLMXeDiAypJr9smetEKTKLd79Z5GrgVzh+xjCIOqsdr2f/FrlzDxKX9SO8TMjRt7BjIjv4RrcOBg==", + "license": "MIT", + "dependencies": { + "@serwist/build": "9.5.6", + "@serwist/utils": "9.5.6", + "@serwist/webpack-plugin": "9.5.6", + "@serwist/window": "9.5.6", + "browserslist": "4.28.1", + "glob": "10.5.0", + "kolorist": "1.8.0", + "semver": "7.7.3", + "serwist": "9.5.6", + "zod": "4.3.6" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@serwist/cli": "^9.5.6", + "next": ">=14.0.0", + "react": ">=18.0.0", + "typescript": ">=5.0.0" + }, + "peerDependenciesMeta": { + "@serwist/cli": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@serwist/next/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@serwist/utils": { + "version": "9.5.6", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/@serwist/utils/-/utils-9.5.6.tgz", + "integrity": "sha512-WV5hAZd/Oo8hyHv7Pd39EDZu3bIhKe0lW39lyMlKVHm5gIGEnPdrH3DojlXAFHiV18nz/bLeqkVo6rK82kBGHw==", + "license": "MIT", + "peerDependencies": { + "browserslist": ">=4" + }, + "peerDependenciesMeta": { + "browserslist": { + "optional": true + } + } + }, + "node_modules/@serwist/webpack-plugin": { + "version": "9.5.6", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/@serwist/webpack-plugin/-/webpack-plugin-9.5.6.tgz", + "integrity": "sha512-kdDqe4AVDJMcS3zTCpV42p+WjJRKb4t0P3flqmceMXfKUDrvhZR3kUWN6yCFi8TLSbHd4hnZZ0cyKa5bCHaa+Q==", + "license": "MIT", + "dependencies": { + "@serwist/build": "9.5.6", + "@serwist/utils": "9.5.6", + "pretty-bytes": "6.1.1", + "zod": "4.3.6" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "typescript": ">=5.0.0", + "webpack": "4.4.0 || ^5.9.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/@serwist/window": { + "version": "9.5.6", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/@serwist/window/-/window-9.5.6.tgz", + "integrity": "sha512-/RztZ97HxiEFlDSCpiLd/6nGz3oDQkKMSDF8epJcta7xdUTAZVwMksEodl3x9Y7jyGItF6T/jY7OBCPrN5IVqQ==", + "license": "MIT", + "dependencies": { + "@types/trusted-types": "2.0.7", + "serwist": "9.5.6" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -2746,6 +2906,12 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, "node_modules/@types/uuid": { "version": "10.0.0", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/@types/uuid/-/uuid-10.0.0.tgz", @@ -3484,7 +3650,6 @@ "version": "5.0.1", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3494,7 +3659,6 @@ "version": "4.3.0", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -3750,7 +3914,6 @@ "version": "1.0.2", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/baseline-browser-mapping": { @@ -3803,7 +3966,6 @@ "version": "4.28.1", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/browserslist/-/browserslist-4.28.1.tgz", "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "dev": true, "funding": [ { "type": "opencollective", @@ -3951,7 +4113,6 @@ "version": "2.0.1", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -3964,9 +4125,17 @@ "version": "1.1.4", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/concat-map/-/concat-map-0.0.1.tgz", @@ -3985,7 +4154,6 @@ "version": "7.0.6", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -4258,18 +4426,22 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.302", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", - "dev": true, "license": "ISC" }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, "license": "MIT" }, "node_modules/enhanced-resolve": { @@ -4529,7 +4701,6 @@ "version": "3.2.0", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5141,6 +5312,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/fsevents/-/fsevents-2.3.3.tgz", @@ -5287,6 +5474,27 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/glob-parent/-/glob-parent-6.0.2.tgz", @@ -5300,6 +5508,30 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/globals/-/globals-14.0.0.tgz", @@ -5754,6 +5986,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -6003,7 +6244,6 @@ "version": "2.0.0", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/iterator.prototype": { @@ -6024,6 +6264,21 @@ "node": ">= 0.4" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/jiti/-/jiti-2.6.1.tgz", @@ -6169,6 +6424,12 @@ "json-buffer": "3.0.1" } }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "license": "MIT" + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -6487,6 +6748,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/loose-envify/-/loose-envify-1.4.0.tgz", @@ -6613,6 +6880,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/ms/-/ms-2.1.3.tgz", @@ -6765,7 +7041,6 @@ "version": "2.0.27", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/node-releases/-/node-releases-2.0.27.tgz", "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "dev": true, "license": "MIT" }, "node_modules/object-assign": { @@ -6970,6 +7245,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/parent-module/-/parent-module-1.0.1.tgz", @@ -7010,7 +7291,6 @@ "version": "3.1.1", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7023,6 +7303,28 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/pathe/-/pathe-2.0.3.tgz", @@ -7098,6 +7400,18 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-bytes": { + "version": "6.1.1", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/pretty-bytes/-/pretty-bytes-6.1.1.tgz", + "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", + "license": "MIT", + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/pretty-format/-/pretty-format-27.5.1.tgz", @@ -7149,7 +7463,6 @@ "version": "2.3.1", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -7479,6 +7792,24 @@ "semver": "bin/semver.js" } }, + "node_modules/serwist": { + "version": "9.5.6", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/serwist/-/serwist-9.5.6.tgz", + "integrity": "sha512-WoseghF1DUevNGnEqsmyXzVyk1KT18S3CJoFZmzav4vEqGWit+I7ErFav+ocrYq2IUoDhJpbTg15a68UdZy0Vw==", + "license": "MIT", + "dependencies": { + "@serwist/utils": "9.5.6", + "idb": "8.0.3" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/set-function-length/-/set-function-length-1.2.2.tgz", @@ -7590,7 +7921,6 @@ "version": "2.0.0", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -7603,7 +7933,6 @@ "version": "3.0.0", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7692,6 +8021,31 @@ "dev": true, "license": "ISC" }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "deprecated": "The work that was done in this beta branch won't be included in future versions", + "license": "BSD-3-Clause", + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/source-map-js/-/source-map-js-1.2.1.tgz", @@ -7701,6 +8055,32 @@ "node": ">=0.10.0" } }, + "node_modules/source-map/node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "license": "MIT", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/source-map/node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "license": "BSD-2-Clause" + }, + "node_modules/source-map/node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/stable-hash/-/stable-hash-0.0.5.tgz", @@ -7736,6 +8116,56 @@ "node": ">= 0.4" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -7849,6 +8279,46 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/strip-bom/-/strip-bom-3.0.0.tgz", @@ -8237,7 +8707,7 @@ "version": "5.9.3", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "peer": true, "bin": { @@ -8347,7 +8817,6 @@ "version": "1.2.3", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, "funding": [ { "type": "opencollective", @@ -8403,6 +8872,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -8647,7 +9117,6 @@ "version": "2.0.2", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -8775,6 +9244,85 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/xml-name-validator": { "version": "5.0.0", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/xml-name-validator/-/xml-name-validator-5.0.0.tgz", diff --git a/web/package.json b/web/package.json index 9139d22..bd1c7b2 100644 --- a/web/package.json +++ b/web/package.json @@ -12,12 +12,14 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@serwist/next": "^9.5.6", "date-fns": "^4.1.0", "idb": "^8.0.3", "lucide-react": "^0.575.0", "next": "16.1.6", "react": "19.2.3", "react-dom": "19.2.3", + "serwist": "^9.5.6", "uuid": "^13.0.0", "zod": "^4.3.6", "zustand": "^5.0.11" diff --git a/web/src/app/sw.ts b/web/src/app/sw.ts new file mode 100644 index 0000000..eb30c2e --- /dev/null +++ b/web/src/app/sw.ts @@ -0,0 +1,21 @@ +import { defaultCache } from '@serwist/next/worker'; +import type { PrecacheEntry, SerwistGlobalConfig } from 'serwist'; +import { Serwist } from 'serwist'; + +declare global { + interface WorkerGlobalScope extends SerwistGlobalConfig { + __SW_MANIFEST: (PrecacheEntry | string)[] | undefined; + } +} + +declare const self: WorkerGlobalScope & typeof globalThis; + +const serwist = new Serwist({ + precacheEntries: self.__SW_MANIFEST, + skipWaiting: true, + clientsClaim: true, + navigationPreload: true, + runtimeCaching: defaultCache, +}); + +serwist.addEventListeners();