diff --git a/DESIGNPLAN.md b/DESIGNPLAN.md
index 754f227..b69627e 100644
--- a/DESIGNPLAN.md
+++ b/DESIGNPLAN.md
@@ -84,23 +84,6 @@ A test scheduler for the NJTH which compiles a best schedule according to the ru
- **Estimated Completion Date**
- **Settings**
- Paths to target and result directories
- - Rotation definition
- - Let user define rotation rules, two different fields, one for P2P/COE tests, one for P3P tests, then display the rules with two tables Ex:
- - P2P/COE
-
- | Rotation | 5G | 6G | 2G |
- |----------|----|----|----|
- | ROT1 | STA56 | STA63 | STA4 |
- | ROT2 | STA4 | STA56 | STA63 |
- | ROT3 | STA63 | | STA56 |
-
- - P3P
-
- | Rotation | 5G | 6G | 2G |
- |----------|----|----|----|
- | ROT1 | STA56/58/59 | STA63/64/65 | STA4/5/6 |
- | ROT2 | STA4/5/6 | STA56/58/59 | STA63/64/65 |
- | ROT3 | STA63/64/65 | | STA56/58/59 |
- Test exclusion
- User can enter rules to exclude tests that will not be run
- Manual priority override
diff --git a/backend/app.py b/backend/app.py
index 0583be4..31eafed 100644
--- a/backend/app.py
+++ b/backend/app.py
@@ -1,9 +1,11 @@
+from contextlib import asynccontextmanager
from pathlib import Path
from typing import Any
from datetime import date, datetime
import os
from fastapi import FastAPI, HTTPException
+from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
import db
@@ -15,9 +17,7 @@ from scheduler import SchedulerTest, compile_schedule, remove_from_active, reset
APP_ROOT = Path(__file__).resolve().parent
DB_PATH = APP_ROOT / "scheduler.db"
DUT = os.getenv("DUT", "CGW453").strip()
-REF = os.getenv("REF", "CGW452").strip()
-
-app = FastAPI(title="Scheduler API", version="0.1.0")
+REF = os.getenv("REF", "CGW452").strip()
class LoadTestsRequest(BaseModel):
@@ -40,10 +40,25 @@ class RemoveActiveTestsRequest(BaseModel):
test_ids: list[str] = Field(default_factory=list)
-@app.on_event("startup")
-def on_startup() -> None:
+class SaveHolidaysRequest(BaseModel):
+ dates: list[str] = Field(default_factory=list)
+
+
+@asynccontextmanager
+async def lifespan(application: FastAPI):
db.init_db(DB_PATH)
graph.reset_graph_state()
+ yield
+
+
+app = FastAPI(title="Scheduler API", version="0.1.0", lifespan=lifespan)
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
@app.get("/api/health")
@@ -154,6 +169,18 @@ def compile_schedule_endpoint(request: CompileScheduleRequest) -> dict[str, Any]
}
+@app.post("/api/holidays")
+def save_holidays(request: SaveHolidaysRequest) -> dict[str, Any]:
+ dates = [d.strip() for d in request.dates if d.strip()]
+ db.upsert_holidays(dates, DB_PATH)
+ return {"status": "saved", "count": len(dates)}
+
+
+@app.get("/api/holidays")
+def get_holidays() -> dict[str, Any]:
+ return {"dates": sorted(db.list_holidays(DB_PATH))}
+
+
@app.get("/api/schedule/week")
def get_schedule_week(start: str | None = None) -> dict[str, Any]:
week_start = start or date.today().isoformat()
@@ -174,6 +201,7 @@ def get_schedule_week(start: str | None = None) -> dict[str, Any]:
"sequence_in_shift": row.sequence_in_shift,
"test_type": row.test_type,
"rotation": row.rotation,
+ "config": row.config,
"status": row.status,
"priority": row.priority,
"estimated_minutes": row.estimated_minutes,
@@ -181,3 +209,8 @@ def get_schedule_week(start: str | None = None) -> dict[str, Any]:
for row in rows
],
}
+
+if __name__ == "__main__":
+ import uvicorn
+ uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True)
+
diff --git a/backend/db.py b/backend/db.py
index b9259f3..2c08919 100644
--- a/backend/db.py
+++ b/backend/db.py
@@ -47,6 +47,7 @@ class ScheduleRow:
sequence_in_shift: int
test_type: str
rotation: str | None
+ config: dict[str, dict[str, str | None]]
status: str
priority: int
estimated_minutes: int
@@ -479,11 +480,12 @@ def get_schedule_week(start_date: str, db_path: str | Path = DB_PATH) -> list[Sc
s.sequence_in_shift,
t.test_type,
t.rotation,
+ t.config_json,
t.status,
t.priority,
t.estimated_minutes
FROM schedules s
- JOIN tests t ON t.test_id = s.test_id
+ JOIN tests t ON t.test_id = s.test_id AND t.device = s.device
WHERE s.schedule_version = ?
AND s.scheduled_date >= ?
AND s.scheduled_date < date(?, '+7 day')
@@ -503,6 +505,7 @@ def get_schedule_week(start_date: str, db_path: str | Path = DB_PATH) -> list[Sc
sequence_in_shift=int(row["sequence_in_shift"]),
test_type=row["test_type"],
rotation=row["rotation"],
+ config=json.loads(row["config_json"] or "{}"),
status=row["status"],
priority=int(row["priority"]),
estimated_minutes=int(row["estimated_minutes"]),
@@ -527,3 +530,14 @@ def mark_tests_completed(test_ids_with_device: list[tuple[str, str]], db_path: s
test_ids_with_device,
)
+
+def upsert_holidays(dates: list[str], db_path: str | Path = DB_PATH) -> None:
+ """Replace all holidays with the provided list of YYYY-MM-DD date strings."""
+ with get_connection(db_path) as conn:
+ conn.execute("DELETE FROM holidays")
+ if dates:
+ conn.executemany(
+ "INSERT OR IGNORE INTO holidays(date) VALUES (?)",
+ [(d,) for d in dates],
+ )
+
diff --git a/backend/graph.py b/backend/graph.py
index 8442370..615f5a2 100644
--- a/backend/graph.py
+++ b/backend/graph.py
@@ -49,6 +49,10 @@ def _build_graph(tests: dict[str, Any]) -> dict[str, set[str]]:
def _compatible(a: Any, b: Any) -> bool:
+ # Hard conflict: same testpoint but different STA assignment means they cannot share a window.
+ if _has_testpoint_sta_conflict(a.config, b.config):
+ return False
+
# Compatible when same rotation, or both non-P3P and overlap has identical testpoints.
if a.rotation == b.rotation:
return True
@@ -57,6 +61,20 @@ def _compatible(a: Any, b: Any) -> bool:
return False
+def _has_testpoint_sta_conflict(
+ config_a: dict[str, dict[str, str | None]],
+ config_b: dict[str, dict[str, str | None]],
+) -> bool:
+ testpoint_to_sta_a = _build_testpoint_sta_map(config_a)
+ testpoint_to_sta_b = _build_testpoint_sta_map(config_b)
+
+ overlap = set(testpoint_to_sta_a.keys()) & set(testpoint_to_sta_b.keys())
+ for testpoint in overlap:
+ if testpoint_to_sta_a[testpoint] != testpoint_to_sta_b[testpoint]:
+ return True
+ return False
+
+
def _same_test_points_for_overlap(
config_a: dict[str, dict[str, str | None]],
config_b: dict[str, dict[str, str | None]],
@@ -95,6 +113,30 @@ def _build_station_testpoint_map(config: dict[str, dict[str, str | None]]) -> di
return station_to_testpoint
+def _build_testpoint_sta_map(config: dict[str, dict[str, str | None]]) -> dict[str, set[str]]:
+ testpoint_to_sta: dict[str, set[str]] = {}
+
+ def _add_entry(entry: dict[str, str | None]) -> None:
+ testpoint = _norm(entry.get("test_point") or entry.get("Testpoint"))
+ sta_raw = _norm(entry.get("sta") or entry.get("STA"))
+ if not testpoint or not sta_raw:
+ return
+ for sta in sta_raw.split(","):
+ sta_clean = _norm(sta)
+ if sta_clean:
+ testpoint_to_sta.setdefault(testpoint, set()).add(sta_clean)
+
+ for band in ("5G", "6G", "2G"):
+ entry = config.get(band) or {}
+ _add_entry(entry)
+
+ for station_key in ("Station 1", "Station 2", "Station 3"):
+ entry = config.get(station_key) or {}
+ _add_entry(entry)
+
+ return testpoint_to_sta
+
+
def _norm(value: Any) -> str:
if value is None:
return ""
diff --git a/backend/scheduler.py b/backend/scheduler.py
index 04a3713..7ae9bff 100644
--- a/backend/scheduler.py
+++ b/backend/scheduler.py
@@ -136,25 +136,37 @@ def compile_schedule(
ref_active_priority: dict[TestKey, int] = dict(_ACTIVE_REF)
entries: list[ScheduleEntry] = []
- current_date = _parse_date(start_date)
+ window_start_date = _parse_date(start_date)
+ current_date = window_start_date
last_date: str | None = None
+ daytime_shift2_window_pending = daytime_testing_today
while dut_active_priority or ref_active_priority:
+ is_special_daytime_shift2_window = (
+ daytime_shift2_window_pending
+ and current_date == window_start_date
+ and current_date.weekday() < 5
+ and current_date.isoformat() not in holiday_dates
+ )
+
# Get the shift sequence for current date (respects Mon/Fri/weekend rules)
- shift_sequence = _get_shift_sequence(current_date, holiday_dates)
+ shift_sequence = _get_shift_sequence(
+ current_date,
+ holiday_dates,
+ daytime_shift2_only=is_special_daytime_shift2_window,
+ )
if not shift_sequence:
break # No valid shift sequence
dut_active_test_ids: set[TestKey] = set(dut_active_priority.keys())
ref_active_test_ids: set[TestKey] = set(ref_active_priority.keys())
- bundles = _create_bundles(dut_active_test_ids, ref_active_test_ids, _TESTS_BY_ID)
+ bundles = _create_bundles(dut_active_test_ids, ref_active_test_ids, _TESTS_BY_ID, top_priority_tests)
shift_capacities = {
(date_obj, shift_idx): _shift_capacity_for_date(
current_date=date_obj,
holiday_dates=holiday_dates,
- start_date_override=start_date,
- daytime_testing_today=daytime_testing_today,
+ daytime_shift2_only=is_special_daytime_shift2_window and date_obj == current_date,
).get(shift_idx, 0)
for date_obj, shift_idx in shift_sequence
}
@@ -173,6 +185,9 @@ def compile_schedule(
if placed_last_date is not None:
last_date = placed_last_date
+ # Daytime testing special handling applies only to the first scheduling window.
+ daytime_shift2_window_pending = False
+
# Move to next scheduling window start date
# After shift 1 (1am-9am), there's shift 2 if daytime testing, then shift 3 (5pm)
# After shift 3 (5pm), shift 1 is next day (1am)
@@ -347,6 +362,7 @@ def _create_bundles(
dut_active_test_ids: set[TestKey],
ref_active_test_ids: set[TestKey],
tests: dict[TestKey, SchedulerTest],
+ top_priority_tests: set[str] | None = None,
) -> list[TestBundle]:
"""Create bundles of tests that must run together, sorted by priority tier and efficiency.
@@ -358,6 +374,9 @@ def _create_bundles(
Returned list is sorted by (priority_tier, total_minutes) to place small/high-priority bundles first.
"""
+ if top_priority_tests is None:
+ top_priority_tests = set()
+
bundles: list[TestBundle] = []
processed: set[TestKey] = set()
@@ -393,7 +412,12 @@ def _create_bundles(
if not bundle_test_ids:
continue
processed.update(bundle_test_ids)
- priority_tier = BUNDLE_PRIORITY_P2P_WITH_COE
+ # Check if any test in bundle is top priority
+ bundle_test_ids_only = {key[0] for key in bundle_test_ids}
+ if bundle_test_ids_only & top_priority_tests:
+ priority_tier = BUNDLE_PRIORITY_FAILED # Highest priority (0)
+ else:
+ priority_tier = BUNDLE_PRIORITY_P2P_WITH_COE
bundles.append(TestBundle(
test_ids=bundle_test_ids,
priority_tier=priority_tier,
@@ -415,7 +439,12 @@ def _create_bundles(
if not bundle_test_ids:
continue
processed.update(bundle_test_ids)
- priority_tier = BUNDLE_PRIORITY_P2P_ONLY
+ # Check if any test in bundle is top priority
+ bundle_test_ids_only = {key[0] for key in bundle_test_ids}
+ if bundle_test_ids_only & top_priority_tests:
+ priority_tier = BUNDLE_PRIORITY_FAILED # Highest priority (0)
+ else:
+ priority_tier = BUNDLE_PRIORITY_P2P_ONLY
bundles.append(TestBundle(
test_ids=bundle_test_ids,
priority_tier=priority_tier,
@@ -425,7 +454,12 @@ def _create_bundles(
bundle_test_ids = [key for key in (_active_key(test_id, DUT), _active_key(test_id, REF)) if key is not None and key not in processed]
if not bundle_test_ids:
continue
- priority_tier = BUNDLE_PRIORITY_P3P
+ # Check if any test in bundle is top priority
+ bundle_test_ids_only = {key[0] for key in bundle_test_ids}
+ if bundle_test_ids_only & top_priority_tests:
+ priority_tier = BUNDLE_PRIORITY_FAILED # Highest priority (0)
+ else:
+ priority_tier = BUNDLE_PRIORITY_P3P
bundles.append(TestBundle(
test_ids=bundle_test_ids,
priority_tier=priority_tier,
@@ -439,8 +473,12 @@ def _create_bundles(
if key in processed:
continue
test = tests[key]
- priority_tier = BUNDLE_PRIORITY_COE_ONLY
bundle_test_ids = [key]
+ # Check if this test is top priority
+ if key[0] in top_priority_tests:
+ priority_tier = BUNDLE_PRIORITY_FAILED # Highest priority (0)
+ else:
+ priority_tier = BUNDLE_PRIORITY_COE_ONLY
bundles.append(TestBundle(
test_ids=bundle_test_ids,
priority_tier=priority_tier,
@@ -466,8 +504,7 @@ def _rx_tx_pair_id(test_id: str, active: set[str]) -> str | None:
def _shift_capacity_for_date(
current_date: date,
holiday_dates: set[str],
- start_date_override: str | None,
- daytime_testing_today: bool,
+ daytime_shift2_only: bool,
) -> dict[int, int]:
iso = current_date.isoformat()
if iso in holiday_dates:
@@ -477,8 +514,8 @@ def _shift_capacity_for_date(
if is_weekend:
return {1: 480, 2: 480, 3: 480}
- if start_date_override and iso == _parse_date(start_date_override).isoformat() and daytime_testing_today:
- return {1: 480, 2: 480, 3: 480}
+ if daytime_shift2_only:
+ return {1: 0, 2: 480, 3: 0}
return {1: 600, 2: 0, 3: 420}
@@ -489,15 +526,20 @@ def _parse_date(value: str | None) -> date:
return datetime.strptime(value, "%Y-%m-%d").date()
-def _get_shift_sequence(start_date: date, holiday_dates: set[str]) -> list[tuple[date, int]]:
+def _get_shift_sequence(
+ start_date: date,
+ holiday_dates: set[str],
+ daytime_shift2_only: bool = False,
+) -> list[tuple[date, int]]:
"""Generate the sequence of (date, shift_index) tuples for a scheduling window.
Rules per design:
+ - Daytime testing first window (weekday only): [2] (shift 2 today only)
- Monday-Thursday: [3, 1] (shift 3 today, shift 1 next day) = 16 hours
- Friday: [3, 1, 2, 3, 1, 2, 3, 1] (Fri-Mon) = 24 hours continuous
- Saturday/Sunday/Holiday weekday: all 3 shifts
- Always starts on shift 3 for weekdays.
+ Weekday default starts on shift 3 unless daytime testing shift-2-only is enabled.
Returns ordered list of (date, shift_index) pairs.
"""
iso = start_date.isoformat()
@@ -505,6 +547,10 @@ def _get_shift_sequence(start_date: date, holiday_dates: set[str]) -> list[tuple
weekday = start_date.weekday() # 0=Mon, 4=Fri, 5=Sat, 6=Sun
shifts: list[tuple[date, int]] = []
+
+ if daytime_shift2_only and weekday < 5 and not is_holiday:
+ shifts.append((start_date, 2))
+ return shifts
# If holiday on a weekday, treat as weekend (all 3 shifts)
if is_holiday and weekday < 5:
diff --git a/frontend/index.html b/frontend/index.html
index 7b228d7..9819f3a 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -4,7 +4,7 @@
-
my-react-app
+ CGW453 Scheduler
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index b2c5a26..3ff37a0 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -8,8 +8,10 @@
"name": "my-react-app",
"version": "0.0.0",
"dependencies": {
+ "@tailwindcss/vite": "^4.3.1",
"react": "^19.2.6",
- "react-dom": "^19.2.6"
+ "react-dom": "^19.2.6",
+ "tailwindcss": "^4.3.1"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
@@ -267,7 +269,6 @@
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
- "dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -279,7 +280,6 @@
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
- "dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -290,7 +290,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
- "dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -495,7 +494,6 @@
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
@@ -506,7 +504,6 @@
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
@@ -517,7 +514,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -527,14 +523,12 @@
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
- "dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -545,7 +539,6 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
- "dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -564,7 +557,6 @@
"version": "0.133.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
- "dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/Boshen"
@@ -577,7 +569,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -594,7 +585,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -611,7 +601,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -628,7 +617,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -645,7 +633,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -662,7 +649,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"libc": [
"glibc"
],
@@ -682,7 +668,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"libc": [
"musl"
],
@@ -702,7 +687,6 @@
"cpu": [
"ppc64"
],
- "dev": true,
"libc": [
"glibc"
],
@@ -722,7 +706,6 @@
"cpu": [
"s390x"
],
- "dev": true,
"libc": [
"glibc"
],
@@ -742,7 +725,6 @@
"cpu": [
"x64"
],
- "dev": true,
"libc": [
"glibc"
],
@@ -762,7 +744,6 @@
"cpu": [
"x64"
],
- "dev": true,
"libc": [
"musl"
],
@@ -782,7 +763,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -799,7 +779,6 @@
"cpu": [
"wasm32"
],
- "dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -818,7 +797,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -835,7 +813,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -849,14 +826,281 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
- "dev": true,
"license": "MIT"
},
+ "node_modules/@tailwindcss/node": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.1.tgz",
+ "integrity": "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "enhanced-resolve": "5.21.6",
+ "jiti": "^2.7.0",
+ "lightningcss": "1.32.0",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.3.1"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.1.tgz",
+ "integrity": "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.3.1",
+ "@tailwindcss/oxide-darwin-arm64": "4.3.1",
+ "@tailwindcss/oxide-darwin-x64": "4.3.1",
+ "@tailwindcss/oxide-freebsd-x64": "4.3.1",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.3.1",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.3.1",
+ "@tailwindcss/oxide-linux-x64-musl": "4.3.1",
+ "@tailwindcss/oxide-wasm32-wasi": "4.3.1",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.3.1"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz",
+ "integrity": "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz",
+ "integrity": "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz",
+ "integrity": "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz",
+ "integrity": "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz",
+ "integrity": "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz",
+ "integrity": "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz",
+ "integrity": "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz",
+ "integrity": "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz",
+ "integrity": "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz",
+ "integrity": "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.10.0",
+ "@emnapi/runtime": "^1.10.0",
+ "@emnapi/wasi-threads": "^1.2.1",
+ "@napi-rs/wasm-runtime": "^1.1.4",
+ "@tybys/wasm-util": "^0.10.2",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz",
+ "integrity": "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz",
+ "integrity": "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/vite": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.1.tgz",
+ "integrity": "sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tailwindcss/node": "4.3.1",
+ "@tailwindcss/oxide": "4.3.1",
+ "tailwindcss": "4.3.1"
+ },
+ "peerDependencies": {
+ "vite": "^5.2.0 || ^6 || ^7 || ^8"
+ }
+ },
"node_modules/@tybys/wasm-util": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
- "dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -1119,7 +1363,6 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
- "dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
@@ -1132,6 +1375,19 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/enhanced-resolve": {
+ "version": "5.21.6",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz",
+ "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -1362,7 +1618,6 @@
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
@@ -1431,7 +1686,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -1478,6 +1732,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "license": "ISC"
+ },
"node_modules/hermes-estree": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
@@ -1545,6 +1805,15 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/jiti": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -1627,7 +1896,6 @@
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
- "dev": true,
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
@@ -1660,7 +1928,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -1681,7 +1948,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -1702,7 +1968,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -1723,7 +1988,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -1744,7 +2008,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -1765,7 +2028,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"libc": [
"glibc"
],
@@ -1789,7 +2051,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"libc": [
"musl"
],
@@ -1813,7 +2074,6 @@
"cpu": [
"x64"
],
- "dev": true,
"libc": [
"glibc"
],
@@ -1837,7 +2097,6 @@
"cpu": [
"x64"
],
- "dev": true,
"libc": [
"musl"
],
@@ -1861,7 +2120,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -1882,7 +2140,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -1922,6 +2179,15 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
"node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
@@ -1949,7 +2215,6 @@
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -2055,14 +2320,12 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -2075,7 +2338,6 @@
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
- "dev": true,
"funding": [
{
"type": "opencollective",
@@ -2145,7 +2407,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.133.0",
@@ -2218,17 +2479,34 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
- "dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
+ "node_modules/tailwindcss": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.1.tgz",
+ "integrity": "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==",
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
"node_modules/tinyglobby": {
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
- "dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
@@ -2245,7 +2523,6 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "dev": true,
"license": "0BSD",
"optional": true
},
@@ -2307,7 +2584,6 @@
"version": "8.0.16",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
diff --git a/frontend/package.json b/frontend/package.json
index b80f45f..e630b00 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -10,8 +10,10 @@
"preview": "vite preview"
},
"dependencies": {
+ "@tailwindcss/vite": "^4.3.1",
"react": "^19.2.6",
- "react-dom": "^19.2.6"
+ "react-dom": "^19.2.6",
+ "tailwindcss": "^4.3.1"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
diff --git a/frontend/src/App.css b/frontend/src/App.css
index f90339d..8b13789 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -1,184 +1 @@
-.counter {
- font-size: 16px;
- padding: 5px 10px;
- border-radius: 5px;
- color: var(--accent);
- background: var(--accent-bg);
- border: 2px solid transparent;
- transition: border-color 0.3s;
- margin-bottom: 24px;
- &:hover {
- border-color: var(--accent-border);
- }
- &:focus-visible {
- outline: 2px solid var(--accent);
- outline-offset: 2px;
- }
-}
-
-.hero {
- position: relative;
-
- .base,
- .framework,
- .vite {
- inset-inline: 0;
- margin: 0 auto;
- }
-
- .base {
- width: 170px;
- position: relative;
- z-index: 0;
- }
-
- .framework,
- .vite {
- position: absolute;
- }
-
- .framework {
- z-index: 1;
- top: 34px;
- height: 28px;
- transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
- scale(1.4);
- }
-
- .vite {
- z-index: 0;
- top: 107px;
- height: 26px;
- width: auto;
- transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
- scale(0.8);
- }
-}
-
-#center {
- display: flex;
- flex-direction: column;
- gap: 25px;
- place-content: center;
- place-items: center;
- flex-grow: 1;
-
- @media (max-width: 1024px) {
- padding: 32px 20px 24px;
- gap: 18px;
- }
-}
-
-#next-steps {
- display: flex;
- border-top: 1px solid var(--border);
- text-align: left;
-
- & > div {
- flex: 1 1 0;
- padding: 32px;
- @media (max-width: 1024px) {
- padding: 24px 20px;
- }
- }
-
- .icon {
- margin-bottom: 16px;
- width: 22px;
- height: 22px;
- }
-
- @media (max-width: 1024px) {
- flex-direction: column;
- text-align: center;
- }
-}
-
-#docs {
- border-right: 1px solid var(--border);
-
- @media (max-width: 1024px) {
- border-right: none;
- border-bottom: 1px solid var(--border);
- }
-}
-
-#next-steps ul {
- list-style: none;
- padding: 0;
- display: flex;
- gap: 8px;
- margin: 32px 0 0;
-
- .logo {
- height: 18px;
- }
-
- a {
- color: var(--text-h);
- font-size: 16px;
- border-radius: 6px;
- background: var(--social-bg);
- display: flex;
- padding: 6px 12px;
- align-items: center;
- gap: 8px;
- text-decoration: none;
- transition: box-shadow 0.3s;
-
- &:hover {
- box-shadow: var(--shadow);
- }
- .button-icon {
- height: 18px;
- width: 18px;
- }
- }
-
- @media (max-width: 1024px) {
- margin-top: 20px;
- flex-wrap: wrap;
- justify-content: center;
-
- li {
- flex: 1 1 calc(50% - 8px);
- }
-
- a {
- width: 100%;
- justify-content: center;
- box-sizing: border-box;
- }
- }
-}
-
-#spacer {
- height: 88px;
- border-top: 1px solid var(--border);
- @media (max-width: 1024px) {
- height: 48px;
- }
-}
-
-.ticks {
- position: relative;
- width: 100%;
-
- &::before,
- &::after {
- content: '';
- position: absolute;
- top: -4.5px;
- border: 5px solid transparent;
- }
-
- &::before {
- left: 0;
- border-left-color: var(--border);
- }
- &::after {
- right: 0;
- border-right-color: var(--border);
- }
-}
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 4f03aa1..07a145d 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1,122 +1,249 @@
-import { useState } from 'react'
-import reactLogo from './assets/react.svg'
-import viteLogo from './assets/vite.svg'
-import heroImg from './assets/hero.png'
-import './App.css'
+import { useState, useEffect, useCallback, useMemo } from 'react'
+import Header from './components/Header'
+import FailedBanner from './components/FailedBanner'
+import Calendar from './components/Calendar'
+import RightPanel from './components/RightPanel'
+import SettingsModal from './components/SettingsModal'
+import { api, groupScheduleItems } from './api'
-function App() {
- const [count, setCount] = useState(0)
-
- return (
- <>
-
-
-
-
Get started
-
- Edit src/App.jsx and save to test HMR
-
-
-
-
-
-
-
-
-
-
-
Documentation
-
Your questions, answered
-
-
-
-
-
Connect with us
-
Join the Vite community
-
-
-
-
-
-
- >
- )
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+function getMondayOfWeek(date) {
+ const d = new Date(date)
+ const day = d.getDay()
+ d.setDate(d.getDate() + (day === 0 ? -6 : 1 - day))
+ d.setHours(0, 0, 0, 0)
+ return d
}
-export default App
+function toKey(d) { return d.toISOString().slice(0, 10) }
+
+function addDays(date, n) {
+ const d = new Date(date)
+ d.setDate(d.getDate() + n)
+ return d
+}
+
+function getTonightWindowKeys() {
+ const now = new Date()
+ const hour = now.getHours()
+ const today = new Date(now)
+ today.setHours(0, 0, 0, 0)
+
+ if (hour < 1) {
+ return {
+ shift3Date: toKey(addDays(today, -1)),
+ shift1Date: toKey(today),
+ }
+ }
+
+ return {
+ shift3Date: toKey(today),
+ shift1Date: toKey(addDays(today, 1)),
+ }
+}
+
+function toTonightConfigRows(scheduleData) {
+ const { shift3Date, shift1Date } = getTonightWindowKeys()
+ const tonightTests = [
+ ...((scheduleData[shift3Date]?.shift3) ?? []),
+ ...((scheduleData[shift1Date]?.shift1) ?? []),
+ ]
+
+ const bySta = new Map()
+
+ for (const test of tonightTests) {
+ const config = test.config ?? {}
+ for (const entry of Object.values(config)) {
+ const staRaw = entry?.sta
+ const testPoint = entry?.test_point
+ if (!staRaw || !testPoint) continue
+
+ for (const staPart of String(staRaw).split(',')) {
+ const sta = staPart.trim().toUpperCase()
+ if (!sta) continue
+
+ if (!bySta.has(sta)) {
+ bySta.set(sta, { sta, points: new Set() })
+ }
+ bySta.get(sta).points.add(testPoint)
+ }
+ }
+ }
+
+ return Array.from(bySta.values())
+ .map((row) => ({
+ sta: row.sta,
+ testPoints: Array.from(row.points).sort(),
+ }))
+ .sort((a, b) => a.sta.localeCompare(b.sta))
+}
+
+const DEFAULT_SETTINGS = {
+ p2pCoeCsvPath: '',
+ p3pCsvPath: '',
+ refResultDir: '',
+ dutResultDir: '',
+ testExclusion: '',
+ holidays: '',
+ startDateOverride: '',
+}
+
+// ---------------------------------------------------------------------------
+
+export default function App() {
+ const [settingsOpen, setSettingsOpen] = useState(false)
+ const [settings, setSettings] = useState(DEFAULT_SETTINGS)
+ const [daytimeEnabled, setDaytimeEnabled] = useState(false)
+ const [topPriority, setTopPriority] = useState('')
+ const [lowestPriority, setLowestPriority] = useState('')
+ const [failedTests, setFailedTests] = useState([])
+ const [scheduleData, setScheduleData] = useState({})
+ const [completionDate, setCompletionDate] = useState(null)
+ const [weekStart, setWeekStart] = useState(() => getMondayOfWeek(new Date()))
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState(null)
+ const tonightConfigRows = useMemo(() => toTonightConfigRows(scheduleData), [scheduleData])
+
+ // Fetch schedule for the given weekStart (Monday)
+ const fetchSchedule = useCallback(async (start) => {
+ try {
+ const data = await api.getScheduleWeek(toKey(start))
+ setScheduleData(groupScheduleItems(data.items))
+ } catch (e) {
+ console.error('Failed to fetch schedule:', e)
+ }
+ }, [])
+
+ // On mount: load settings + holidays + schedule for today's week
+ useEffect(() => {
+ async function init() {
+ try {
+ const [saved, holidayData] = await Promise.all([
+ api.getSettings(),
+ api.getHolidays(),
+ ])
+ setSettings(prev => ({
+ ...prev,
+ ...saved,
+ holidays: (holidayData.dates ?? []).join(', '),
+ }))
+ } catch (e) {
+ console.warn('Backend not reachable on load:', e.message)
+ }
+ await fetchSchedule(getMondayOfWeek(new Date()))
+ }
+ init()
+ }, [fetchSchedule])
+
+ // Refetch whenever the displayed week changes
+ useEffect(() => {
+ fetchSchedule(weekStart)
+ }, [weekStart, fetchSchedule])
+
+ // -------------------------------------------------------------------------
+
+ async function handleSaveSettings(newSettings) {
+ setLoading(true)
+ setError(null)
+ try {
+ await api.saveSettings(newSettings)
+
+ const holidayDates = (newSettings.holidays ?? '')
+ .split(',').map(s => s.trim()).filter(Boolean)
+ await api.saveHolidays(holidayDates)
+
+ if (newSettings.p2pCoeCsvPath?.trim()) {
+ await api.loadCsv(newSettings.p2pCoeCsvPath.trim())
+ }
+ if (newSettings.p3pCsvPath?.trim()) {
+ await api.loadCsv(newSettings.p3pCsvPath.trim())
+ }
+
+ setSettings(newSettings)
+ } catch (e) {
+ setError(e.message)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ async function handleRemakeSchedule() {
+ setLoading(true)
+ setError(null)
+ try {
+ const result = await api.compileSchedule({
+ start_date: settings.startDateOverride?.trim() || null,
+ daytime_testing_today: daytimeEnabled,
+ top_priority_tests: topPriority.split(',').map(s => s.trim()).filter(Boolean),
+ lowest_priority_tests: lowestPriority.split(',').map(s => s.trim()).filter(Boolean),
+ rule: settings.testExclusion ?? '',
+ })
+ setCompletionDate(result.completion_date ?? null)
+ await fetchSchedule(weekStart)
+ } catch (e) {
+ setError(e.message)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ function handleRerunDecision(rerunDuringDay) {
+ // TODO: POST /api/failed-tests/rerun-decision once backend endpoint exists
+ console.log('Rerun during day:', rerunDuringDay)
+ setFailedTests([])
+ }
+
+ // -------------------------------------------------------------------------
+
+ return (
+
+ )
+}
diff --git a/frontend/src/api.js b/frontend/src/api.js
new file mode 100644
index 0000000..8a74040
--- /dev/null
+++ b/frontend/src/api.js
@@ -0,0 +1,47 @@
+const BASE = '/api'
+
+async function request(method, path, body) {
+ const res = await fetch(`${BASE}${path}`, {
+ method,
+ headers: body !== undefined ? { 'Content-Type': 'application/json' } : {},
+ body: body !== undefined ? JSON.stringify(body) : undefined,
+ })
+ if (!res.ok) {
+ const err = await res.json().catch(() => ({ detail: res.statusText }))
+ throw new Error(err.detail || `HTTP ${res.status}`)
+ }
+ return res.json()
+}
+
+export const api = {
+ // Health
+ health: () => request('GET', '/health'),
+
+ // Settings
+ getSettings: () => request('GET', '/settings'),
+ saveSettings: (settings) => request('POST', '/settings', { settings }),
+
+ // Holidays
+ getHolidays: () => request('GET', '/holidays'),
+ saveHolidays: (dates) => request('POST', '/holidays', { dates }),
+
+ // Tests
+ loadCsv: (csv_path) => request('POST', '/tests/load', { csv_path }),
+
+ // Schedule
+ compileSchedule: (opts) => request('POST', '/schedule/compile', opts),
+ getScheduleWeek: (start) => request('GET', `/schedule/week?start=${start}`),
+}
+
+// Transform the flat items array from GET /api/schedule/week into the
+// { "YYYY-MM-DD": { shift1: [], shift2: [], shift3: [] } } shape the Calendar expects.
+export function groupScheduleItems(items = []) {
+ const out = {}
+ for (const item of items) {
+ const key = item.scheduled_date
+ if (!out[key]) out[key] = { shift1: [], shift2: [], shift3: [] }
+ const shiftKey = `shift${item.shift_index}`
+ out[key][shiftKey].push(item)
+ }
+ return out
+}
diff --git a/frontend/src/components/Calendar.jsx b/frontend/src/components/Calendar.jsx
new file mode 100644
index 0000000..e86d14e
--- /dev/null
+++ b/frontend/src/components/Calendar.jsx
@@ -0,0 +1,130 @@
+import DayColumn from './DayColumn'
+
+function addDays(date, n) {
+ const d = new Date(date)
+ d.setDate(d.getDate() + n)
+ return d
+}
+
+function formatDateRange(monday) {
+ const sunday = addDays(monday, 6)
+ const opts = { month: 'short', day: 'numeric' }
+ const startStr = monday.toLocaleDateString('en-US', opts)
+ const endStr = sunday.toLocaleDateString('en-US', {
+ month: sunday.getMonth() !== monday.getMonth() ? 'short' : undefined,
+ day: 'numeric',
+ })
+ return `${startStr} – ${endStr}`
+}
+
+function toDateKey(date) {
+ return date.toISOString().slice(0, 10)
+}
+
+function getMondayOfWeek(date) {
+ const d = new Date(date)
+ const day = d.getDay()
+ d.setDate(d.getDate() + (day === 0 ? -6 : 1 - day))
+ d.setHours(0, 0, 0, 0)
+ return d
+}
+
+// Returns the two shifts that form the next/active test window.
+// Window = Shift 3 of day D → Shift 1 of day D+1
+function getNextTestWindow() {
+ const now = new Date()
+ const hour = now.getHours()
+ const tod = new Date(now); tod.setHours(0, 0, 0, 0)
+
+ // midnight–1AM: still inside shift 3 that started yesterday
+ if (hour < 1) {
+ return { shift3Date: toDateKey(addDays(tod, -1)), shift1Date: toDateKey(tod) }
+ }
+ // 1AM–5PM: shift 1 finished or in daytime gap — next window starts at 5PM today
+ // 5PM–midnight: currently inside shift 3 of today
+ // Both cases: upcoming/active window is shift 3 today + shift 1 tomorrow
+ return { shift3Date: toDateKey(tod), shift1Date: toDateKey(addDays(tod, 1)) }
+}
+
+// scheduleData: { "YYYY-MM-DD": { shift1: [...], shift2: [...], shift3: [...] }, ... }
+export default function Calendar({ scheduleData = {}, daytimeDateKey = null, weekStart, onWeekChange }) {
+ const today = new Date()
+ today.setHours(0, 0, 0, 0)
+
+ const activeWindow = getNextTestWindow()
+
+ const days = Array.from({ length: 7 }, (_, i) => addDays(weekStart, i))
+
+ // Reorder columns: Sun Mon Tue Wed Thu Fri Sat
+ // weekStart is Monday, so days[0]=Mon ... days[6]=Sun → put Sunday first
+ const orderedDays = [ ...days.slice(0, 7)]
+
+ return (
+
+ {/* Week navigation */}
+
+
+
+ {formatDateRange(weekStart)}
+
+
+
+
+
+ {/* 7-day grid */}
+
+ {orderedDays.map((date) => {
+ const key = toDateKey(date)
+ const shifts = scheduleData[key] ?? { shift1: [], shift2: [], shift3: [] }
+ const isToday = date.getTime() === today.getTime()
+ const isWeekend = date.getDay() === 0 || date.getDay() === 6
+ // Which shifts on this date are part of the active test window?
+ const activeShifts = new Set()
+ if (key === activeWindow.shift3Date) activeShifts.add(3)
+ if (key === activeWindow.shift1Date) activeShifts.add(1)
+ return (
+
+ )
+ })}
+
+
+ {/* Legend */}
+
+ Pending
+ Completed
+ Failed
+ Invalid
+
+
+ )
+}
+
diff --git a/frontend/src/components/DayColumn.jsx b/frontend/src/components/DayColumn.jsx
new file mode 100644
index 0000000..1c5a6aa
--- /dev/null
+++ b/frontend/src/components/DayColumn.jsx
@@ -0,0 +1,38 @@
+import ShiftSlot from './ShiftSlot'
+
+const DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
+
+export default function DayColumn({ date, isToday, activeShifts = new Set(), shifts = {}, showShift2 = false }) {
+ const dayName = DAY_NAMES[date.getDay()]
+ const dayNum = date.getDate()
+ const isWeekend = date.getDay() === 0 || date.getDay() === 6
+ const hasActive = activeShifts.size > 0
+
+ // Shift 2 is visible if: weekend, holiday (showShift2), or daytime testing on
+ const shift2Visible = isWeekend || showShift2
+
+ return (
+
+ {/* Day header — subtle today ring, no full-column highlight */}
+
+
+ {dayName}
+
+
+ {dayNum}
+
+
+
+ {/* Shifts */}
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/components/FailedBanner.jsx b/frontend/src/components/FailedBanner.jsx
new file mode 100644
index 0000000..b80fc2b
--- /dev/null
+++ b/frontend/src/components/FailedBanner.jsx
@@ -0,0 +1,51 @@
+export default function FailedBanner({ failedTests, estimatedMinutes, onDecision }) {
+ if (!failedTests || failedTests.length === 0) return null
+
+ const hours = Math.floor(estimatedMinutes / 60)
+ const mins = estimatedMinutes % 60
+ const timeStr = hours > 0 ? `${hours}h ${mins}m` : `${mins}m`
+
+ return (
+
+
+
+
+ {failedTests.length} test{failedTests.length !== 1 ? 's' : ''}
+ {' '}failed last night and require a rerun —{' '}
+ {failedTests.join(', ')}
+
+
+ Estimated rerun time: {timeStr}
+ {' '}— Rerun these tests during the day today?
+
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx
new file mode 100644
index 0000000..846a79b
--- /dev/null
+++ b/frontend/src/components/Header.jsx
@@ -0,0 +1,35 @@
+export default function Header({ onOpenSettings }) {
+ return (
+
+
+ CGW453 Scheduler
+
+
+
+ )
+}
diff --git a/frontend/src/components/RightPanel.jsx b/frontend/src/components/RightPanel.jsx
new file mode 100644
index 0000000..a3942fc
--- /dev/null
+++ b/frontend/src/components/RightPanel.jsx
@@ -0,0 +1,117 @@
+function formatCompletionDate(value) {
+ if (!value) return null
+
+ const parsed = new Date(`${value}T00:00:00`)
+ if (Number.isNaN(parsed.getTime())) return String(value)
+
+ return parsed.toLocaleDateString('en-US', {
+ weekday: 'short',
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric',
+ })
+}
+
+export default function RightPanel({
+ completionDate,
+ daytimeEnabled,
+ onDaytimeEnabledChange,
+ topPriority,
+ onTopPriorityChange,
+ lowestPriority,
+ onLowestPriorityChange,
+ onRemakeSchedule,
+ tonightConfigRows = [],
+ loading = false,
+}) {
+ const formattedCompletionDate = formatCompletionDate(completionDate)
+
+ return (
+
+ )
+}
diff --git a/frontend/src/components/SettingsModal.jsx b/frontend/src/components/SettingsModal.jsx
new file mode 100644
index 0000000..5d48217
--- /dev/null
+++ b/frontend/src/components/SettingsModal.jsx
@@ -0,0 +1,183 @@
+import { useState, useEffect } from 'react'
+
+function Field({ label, hint, children }) {
+ return (
+
+
+ {children}
+
+ )
+}
+
+const INPUT_CLS =
+ 'w-full bg-gray-900 border border-gray-600 rounded-md px-3 py-1.5 text-sm text-gray-200 placeholder-gray-600 focus:outline-none focus:border-blue-500 font-mono'
+
+const TEXTAREA_CLS = INPUT_CLS + ' resize-none'
+
+export default function SettingsModal({ isOpen, onClose, settings, onSave }) {
+ const [form, setForm] = useState({ ...settings })
+
+ // Sync if parent settings change while modal is closed
+ useEffect(() => {
+ if (!isOpen) setForm({ ...settings })
+ }, [isOpen, settings])
+
+ if (!isOpen) return null
+
+ function set(key, value) {
+ setForm((f) => ({ ...f, [key]: value }))
+ }
+
+ function handleSave() {
+ onSave(form)
+ onClose()
+ }
+
+ function handleBackdropClick(e) {
+ if (e.target === e.currentTarget) onClose()
+ }
+
+ return (
+
+
+ {/* Modal header */}
+
+
+ {/* Scrollable body */}
+
+
+ {/* Section: Paths */}
+
+
+
+
+ {/* Section: Test Exclusion */}
+
+
+ Test Exclusion
+
+
+ set('testExclusion', e.target.value)}
+ />
+
+
+
+
+
+ {/* Section: Holidays */}
+
+
+ Holidays
+
+
+ set('holidays', e.target.value)}
+ />
+
+
+
+
+
+ {/* Section: Schedule */}
+
+
+ Schedule
+
+
+ set('startDateOverride', e.target.value)}
+ />
+
+
+
+
+ {/* Footer */}
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/components/ShiftSlot.jsx b/frontend/src/components/ShiftSlot.jsx
new file mode 100644
index 0000000..c92aea0
--- /dev/null
+++ b/frontend/src/components/ShiftSlot.jsx
@@ -0,0 +1,24 @@
+import TestCard from './TestCard'
+
+export default function ShiftSlot({ label, tests = [], visible = true, active = false }) {
+ if (!visible) return null
+
+ return (
+
+
+ {label}
+
+
+ {tests.length === 0 ? (
+ —
+ ) : (
+ tests.map((test) => )
+ )}
+
+
+ )
+}
diff --git a/frontend/src/components/TestCard.jsx b/frontend/src/components/TestCard.jsx
new file mode 100644
index 0000000..16534ea
--- /dev/null
+++ b/frontend/src/components/TestCard.jsx
@@ -0,0 +1,29 @@
+const STATUS_STYLES = {
+ pending: 'bg-gray-600/60 text-gray-200 border-gray-500',
+ completed: 'bg-green-800/60 text-green-200 border-green-600',
+ failed: 'bg-red-800/60 text-red-200 border-red-600',
+ invalid: 'bg-yellow-800/60 text-yellow-200 border-yellow-600',
+}
+
+export default function TestCard({ test }) {
+ const style = STATUS_STYLES[test.status] ?? STATUS_STYLES.pending
+
+ return (
+
+
+ {test.test_id}
+
+ {/* Hover tooltip */}
+
+
+
Type: {test.test_type ?? '—'}
+
Rotation: {test.rotation ?? '—'}
+
Device: {test.device ?? '—'}
+
+
+
+ )
+}
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 2c84af0..f1d8c73 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -1,111 +1 @@
-:root {
- --text: #6b6375;
- --text-h: #08060d;
- --bg: #fff;
- --border: #e5e4e7;
- --code-bg: #f4f3ec;
- --accent: #aa3bff;
- --accent-bg: rgba(170, 59, 255, 0.1);
- --accent-border: rgba(170, 59, 255, 0.5);
- --social-bg: rgba(244, 243, 236, 0.5);
- --shadow:
- rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
-
- --sans: system-ui, 'Segoe UI', Roboto, sans-serif;
- --heading: system-ui, 'Segoe UI', Roboto, sans-serif;
- --mono: ui-monospace, Consolas, monospace;
-
- font: 18px/145% var(--sans);
- letter-spacing: 0.18px;
- color-scheme: light dark;
- color: var(--text);
- background: var(--bg);
- font-synthesis: none;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-
- @media (max-width: 1024px) {
- font-size: 16px;
- }
-}
-
-@media (prefers-color-scheme: dark) {
- :root {
- --text: #9ca3af;
- --text-h: #f3f4f6;
- --bg: #16171d;
- --border: #2e303a;
- --code-bg: #1f2028;
- --accent: #c084fc;
- --accent-bg: rgba(192, 132, 252, 0.15);
- --accent-border: rgba(192, 132, 252, 0.5);
- --social-bg: rgba(47, 48, 58, 0.5);
- --shadow:
- rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
- }
-
- #social .button-icon {
- filter: invert(1) brightness(2);
- }
-}
-
-body {
- margin: 0;
-}
-
-#root {
- width: 1126px;
- max-width: 100%;
- margin: 0 auto;
- text-align: center;
- border-inline: 1px solid var(--border);
- min-height: 100svh;
- display: flex;
- flex-direction: column;
- box-sizing: border-box;
-}
-
-h1,
-h2 {
- font-family: var(--heading);
- font-weight: 500;
- color: var(--text-h);
-}
-
-h1 {
- font-size: 56px;
- letter-spacing: -1.68px;
- margin: 32px 0;
- @media (max-width: 1024px) {
- font-size: 36px;
- margin: 20px 0;
- }
-}
-h2 {
- font-size: 24px;
- line-height: 118%;
- letter-spacing: -0.24px;
- margin: 0 0 8px;
- @media (max-width: 1024px) {
- font-size: 20px;
- }
-}
-p {
- margin: 0;
-}
-
-code,
-.counter {
- font-family: var(--mono);
- display: inline-flex;
- border-radius: 4px;
- color: var(--text-h);
-}
-
-code {
- font-size: 15px;
- line-height: 135%;
- padding: 4px 8px;
- background: var(--code-bg);
-}
+@import "tailwindcss";
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
index 8b0f57b..1100cfc 100644
--- a/frontend/vite.config.js
+++ b/frontend/vite.config.js
@@ -1,7 +1,16 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
+import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
- plugins: [react()],
+ plugins: [react(), tailwindcss()],
+ server: {
+ proxy: {
+ '/api': {
+ target: 'http://localhost:8000',
+ changeOrigin: true,
+ },
+ },
+ },
})
Connect with us
-Join the Vite community
---
-
-
- GitHub
-
-
- -
-
-
- Discord
-
-
- -
-
-
- X.com
-
-
- -
-
-
- Bluesky
-
-
-
-