feat: Reinitialize frontend with SvelteKit and TypeScript

- Delete old Vite+Svelte frontend
- Initialize new SvelteKit project with TypeScript
- Configure Tailwind CSS v4 + DaisyUI
- Implement JWT authentication with auto-refresh
- Create login page with form validation (Zod)
- Add protected route guards
- Update Docker configuration for single-stage build
- Add E2E tests with Playwright (6/11 passing)
- Fix Svelte 5 reactivity with $state() runes

Known issues:
- 5 E2E tests failing (timing/async issues)
- Token refresh implementation needs debugging
- Validation error display timing
This commit is contained in:
2026-02-17 16:19:59 -05:00
parent 54df6018f5
commit de2d83092e
28274 changed files with 3816354 additions and 90 deletions

View File

@@ -0,0 +1,47 @@
import * as Chunk from "../../Chunk.js"
import type * as ScheduleDecision from "../../ScheduleDecision.js"
import type * as Interval from "../../ScheduleInterval.js"
import * as Intervals from "../../ScheduleIntervals.js"
/** @internal */
export const OP_CONTINUE = "Continue" as const
/** @internal */
export type OP_CONTINUE = typeof OP_CONTINUE
/** @internal */
export const OP_DONE = "Done" as const
/** @internal */
export type OP_DONE = typeof OP_DONE
/** @internal */
export const _continue = (intervals: Intervals.Intervals): ScheduleDecision.ScheduleDecision => {
return {
_tag: OP_CONTINUE,
intervals
}
}
/** @internal */
export const continueWith = (interval: Interval.Interval): ScheduleDecision.ScheduleDecision => {
return {
_tag: OP_CONTINUE,
intervals: Intervals.make(Chunk.of(interval))
}
}
/** @internal */
export const done: ScheduleDecision.ScheduleDecision = {
_tag: OP_DONE
}
/** @internal */
export const isContinue = (self: ScheduleDecision.ScheduleDecision): self is ScheduleDecision.Continue => {
return self._tag === OP_CONTINUE
}
/** @internal */
export const isDone = (self: ScheduleDecision.ScheduleDecision): self is ScheduleDecision.Done => {
return self._tag === OP_DONE
}

View File

@@ -0,0 +1,101 @@
import * as Duration from "../../Duration.js"
import { dual } from "../../Function.js"
import * as Option from "../../Option.js"
import type * as Interval from "../../ScheduleInterval.js"
/** @internal */
const IntervalSymbolKey = "effect/ScheduleInterval"
/** @internal */
export const IntervalTypeId: Interval.IntervalTypeId = Symbol.for(
IntervalSymbolKey
) as Interval.IntervalTypeId
/** @internal */
export const empty: Interval.Interval = {
[IntervalTypeId]: IntervalTypeId,
startMillis: 0,
endMillis: 0
}
/** @internal */
export const make = (startMillis: number, endMillis: number): Interval.Interval => {
if (startMillis > endMillis) {
return empty
}
return {
[IntervalTypeId]: IntervalTypeId,
startMillis,
endMillis
}
}
/** @internal */
export const lessThan = dual<
(that: Interval.Interval) => (self: Interval.Interval) => boolean,
(self: Interval.Interval, that: Interval.Interval) => boolean
>(2, (self, that) => min(self, that) === self)
/** @internal */
export const min = dual<
(that: Interval.Interval) => (self: Interval.Interval) => Interval.Interval,
(self: Interval.Interval, that: Interval.Interval) => Interval.Interval
>(2, (self, that) => {
if (self.endMillis <= that.startMillis) return self
if (that.endMillis <= self.startMillis) return that
if (self.startMillis < that.startMillis) return self
if (that.startMillis < self.startMillis) return that
if (self.endMillis <= that.endMillis) return self
return that
})
/** @internal */
export const max = dual<
(that: Interval.Interval) => (self: Interval.Interval) => Interval.Interval,
(self: Interval.Interval, that: Interval.Interval) => Interval.Interval
>(2, (self, that) => min(self, that) === self ? that : self)
/** @internal */
export const isEmpty = (self: Interval.Interval): boolean => {
return self.startMillis >= self.endMillis
}
/** @internal */
export const isNonEmpty = (self: Interval.Interval): boolean => {
return !isEmpty(self)
}
/** @internal */
export const intersect = dual<
(that: Interval.Interval) => (self: Interval.Interval) => Interval.Interval,
(self: Interval.Interval, that: Interval.Interval) => Interval.Interval
>(2, (self, that) => {
const start = Math.max(self.startMillis, that.startMillis)
const end = Math.min(self.endMillis, that.endMillis)
return make(start, end)
})
/** @internal */
export const size = (self: Interval.Interval): Duration.Duration => {
return Duration.millis(self.endMillis - self.startMillis)
}
/** @internal */
export const union = dual<
(that: Interval.Interval) => (self: Interval.Interval) => Option.Option<Interval.Interval>,
(self: Interval.Interval, that: Interval.Interval) => Option.Option<Interval.Interval>
>(2, (self, that) => {
const start = Math.max(self.startMillis, that.startMillis)
const end = Math.min(self.endMillis, that.endMillis)
return start < end ? Option.none() : Option.some(make(start, end))
})
/** @internal */
export const after = (startMilliseconds: number): Interval.Interval => {
return make(startMilliseconds, Number.POSITIVE_INFINITY)
}
/** @internal */
export const before = (endMilliseconds: number): Interval.Interval => {
return make(Number.NEGATIVE_INFINITY, endMilliseconds)
}

View File

@@ -0,0 +1,180 @@
import * as Chunk from "../../Chunk.js"
import { dual, pipe } from "../../Function.js"
import * as Option from "../../Option.js"
import * as Interval from "../../ScheduleInterval.js"
import type * as Intervals from "../../ScheduleIntervals.js"
import { getBugErrorMessage } from "../errors.js"
/** @internal */
const IntervalsSymbolKey = "effect/ScheduleIntervals"
/** @internal */
export const IntervalsTypeId: Intervals.IntervalsTypeId = Symbol.for(
IntervalsSymbolKey
) as Intervals.IntervalsTypeId
/** @internal */
export const make = (intervals: Chunk.Chunk<Interval.Interval>): Intervals.Intervals => {
return {
[IntervalsTypeId]: IntervalsTypeId,
intervals
}
}
/** @internal */
export const empty: Intervals.Intervals = make(Chunk.empty())
/** @internal */
export const fromIterable = (intervals: Iterable<Interval.Interval>): Intervals.Intervals =>
Array.from(intervals).reduce(
(intervals, interval) => pipe(intervals, union(make(Chunk.of(interval)))),
empty
)
/** @internal */
export const union = dual<
(that: Intervals.Intervals) => (self: Intervals.Intervals) => Intervals.Intervals,
(self: Intervals.Intervals, that: Intervals.Intervals) => Intervals.Intervals
>(2, (self, that) => {
if (!Chunk.isNonEmpty(that.intervals)) {
return self
}
if (!Chunk.isNonEmpty(self.intervals)) {
return that
}
if (Chunk.headNonEmpty(self.intervals).startMillis < Chunk.headNonEmpty(that.intervals).startMillis) {
return unionLoop(
Chunk.tailNonEmpty(self.intervals),
that.intervals,
Chunk.headNonEmpty(self.intervals),
Chunk.empty()
)
}
return unionLoop(
self.intervals,
Chunk.tailNonEmpty(that.intervals),
Chunk.headNonEmpty(that.intervals),
Chunk.empty()
)
})
/** @internal */
const unionLoop = (
_self: Chunk.Chunk<Interval.Interval>,
_that: Chunk.Chunk<Interval.Interval>,
_interval: Interval.Interval,
_acc: Chunk.Chunk<Interval.Interval>
): Intervals.Intervals => {
let self = _self
let that = _that
let interval = _interval
let acc = _acc
while (Chunk.isNonEmpty(self) || Chunk.isNonEmpty(that)) {
if (!Chunk.isNonEmpty(self) && Chunk.isNonEmpty(that)) {
if (interval.endMillis < Chunk.headNonEmpty(that).startMillis) {
acc = pipe(acc, Chunk.prepend(interval))
interval = Chunk.headNonEmpty(that)
that = Chunk.tailNonEmpty(that)
self = Chunk.empty()
} else {
interval = Interval.make(interval.startMillis, Chunk.headNonEmpty(that).endMillis)
that = Chunk.tailNonEmpty(that)
self = Chunk.empty()
}
} else if (Chunk.isNonEmpty(self) && Chunk.isEmpty(that)) {
if (interval.endMillis < Chunk.headNonEmpty(self).startMillis) {
acc = pipe(acc, Chunk.prepend(interval))
interval = Chunk.headNonEmpty(self)
that = Chunk.empty()
self = Chunk.tailNonEmpty(self)
} else {
interval = Interval.make(interval.startMillis, Chunk.headNonEmpty(self).endMillis)
that = Chunk.empty()
self = Chunk.tailNonEmpty(self)
}
} else if (Chunk.isNonEmpty(self) && Chunk.isNonEmpty(that)) {
if (Chunk.headNonEmpty(self).startMillis < Chunk.headNonEmpty(that).startMillis) {
if (interval.endMillis < Chunk.headNonEmpty(self).startMillis) {
acc = pipe(acc, Chunk.prepend(interval))
interval = Chunk.headNonEmpty(self)
self = Chunk.tailNonEmpty(self)
} else {
interval = Interval.make(interval.startMillis, Chunk.headNonEmpty(self).endMillis)
self = Chunk.tailNonEmpty(self)
}
} else if (interval.endMillis < Chunk.headNonEmpty(that).startMillis) {
acc = pipe(acc, Chunk.prepend(interval))
interval = Chunk.headNonEmpty(that)
that = Chunk.tailNonEmpty(that)
} else {
interval = Interval.make(interval.startMillis, Chunk.headNonEmpty(that).endMillis)
that = Chunk.tailNonEmpty(that)
}
} else {
throw new Error(getBugErrorMessage("Intervals.unionLoop"))
}
}
return make(pipe(acc, Chunk.prepend(interval), Chunk.reverse))
}
/** @internal */
export const intersect = dual<
(that: Intervals.Intervals) => (self: Intervals.Intervals) => Intervals.Intervals,
(self: Intervals.Intervals, that: Intervals.Intervals) => Intervals.Intervals
>(2, (self, that) => intersectLoop(self.intervals, that.intervals, Chunk.empty()))
/** @internal */
const intersectLoop = (
_left: Chunk.Chunk<Interval.Interval>,
_right: Chunk.Chunk<Interval.Interval>,
_acc: Chunk.Chunk<Interval.Interval>
): Intervals.Intervals => {
let left = _left
let right = _right
let acc = _acc
while (Chunk.isNonEmpty(left) && Chunk.isNonEmpty(right)) {
const interval = pipe(Chunk.headNonEmpty(left), Interval.intersect(Chunk.headNonEmpty(right)))
const intervals = Interval.isEmpty(interval) ? acc : pipe(acc, Chunk.prepend(interval))
if (pipe(Chunk.headNonEmpty(left), Interval.lessThan(Chunk.headNonEmpty(right)))) {
left = Chunk.tailNonEmpty(left)
} else {
right = Chunk.tailNonEmpty(right)
}
acc = intervals
}
return make(Chunk.reverse(acc))
}
/** @internal */
export const start = (self: Intervals.Intervals): number => {
return pipe(
self.intervals,
Chunk.head,
Option.getOrElse(() => Interval.empty)
).startMillis
}
/** @internal */
export const end = (self: Intervals.Intervals): number => {
return pipe(
self.intervals,
Chunk.head,
Option.getOrElse(() => Interval.empty)
).endMillis
}
/** @internal */
export const lessThan = dual<
(that: Intervals.Intervals) => (self: Intervals.Intervals) => boolean,
(self: Intervals.Intervals, that: Intervals.Intervals) => boolean
>(2, (self, that) => start(self) < start(that))
/** @internal */
export const isNonEmpty = (self: Intervals.Intervals): boolean => {
return Chunk.isNonEmpty(self.intervals)
}
/** @internal */
export const max = dual<
(that: Intervals.Intervals) => (self: Intervals.Intervals) => Intervals.Intervals,
(self: Intervals.Intervals, that: Intervals.Intervals) => Intervals.Intervals
>(2, (self, that) => lessThan(self, that) ? that : self)