frontend
This commit is contained in:
@@ -84,23 +84,6 @@ A test scheduler for the NJTH which compiles a best schedule according to the ru
|
|||||||
- **Estimated Completion Date**
|
- **Estimated Completion Date**
|
||||||
- **Settings**
|
- **Settings**
|
||||||
- Paths to target and result directories
|
- 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
|
- Test exclusion
|
||||||
- User can enter rules to exclude tests that will not be run
|
- User can enter rules to exclude tests that will not be run
|
||||||
- Manual priority override
|
- Manual priority override
|
||||||
|
|||||||
+37
-4
@@ -1,9 +1,11 @@
|
|||||||
|
from contextlib import asynccontextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import FastAPI, HTTPException
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
import db
|
import db
|
||||||
@@ -17,8 +19,6 @@ DB_PATH = APP_ROOT / "scheduler.db"
|
|||||||
DUT = os.getenv("DUT", "CGW453").strip()
|
DUT = os.getenv("DUT", "CGW453").strip()
|
||||||
REF = os.getenv("REF", "CGW452").strip()
|
REF = os.getenv("REF", "CGW452").strip()
|
||||||
|
|
||||||
app = FastAPI(title="Scheduler API", version="0.1.0")
|
|
||||||
|
|
||||||
|
|
||||||
class LoadTestsRequest(BaseModel):
|
class LoadTestsRequest(BaseModel):
|
||||||
csv_path: str = Field(..., description="Absolute or backend-relative path to target CSV")
|
csv_path: str = Field(..., description="Absolute or backend-relative path to target CSV")
|
||||||
@@ -40,10 +40,25 @@ class RemoveActiveTestsRequest(BaseModel):
|
|||||||
test_ids: list[str] = Field(default_factory=list)
|
test_ids: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
class SaveHolidaysRequest(BaseModel):
|
||||||
def on_startup() -> None:
|
dates: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(application: FastAPI):
|
||||||
db.init_db(DB_PATH)
|
db.init_db(DB_PATH)
|
||||||
graph.reset_graph_state()
|
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")
|
@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")
|
@app.get("/api/schedule/week")
|
||||||
def get_schedule_week(start: str | None = None) -> dict[str, Any]:
|
def get_schedule_week(start: str | None = None) -> dict[str, Any]:
|
||||||
week_start = start or date.today().isoformat()
|
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,
|
"sequence_in_shift": row.sequence_in_shift,
|
||||||
"test_type": row.test_type,
|
"test_type": row.test_type,
|
||||||
"rotation": row.rotation,
|
"rotation": row.rotation,
|
||||||
|
"config": row.config,
|
||||||
"status": row.status,
|
"status": row.status,
|
||||||
"priority": row.priority,
|
"priority": row.priority,
|
||||||
"estimated_minutes": row.estimated_minutes,
|
"estimated_minutes": row.estimated_minutes,
|
||||||
@@ -181,3 +209,8 @@ def get_schedule_week(start: str | None = None) -> dict[str, Any]:
|
|||||||
for row in rows
|
for row in rows
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True)
|
||||||
|
|
||||||
|
|||||||
+15
-1
@@ -47,6 +47,7 @@ class ScheduleRow:
|
|||||||
sequence_in_shift: int
|
sequence_in_shift: int
|
||||||
test_type: str
|
test_type: str
|
||||||
rotation: str | None
|
rotation: str | None
|
||||||
|
config: dict[str, dict[str, str | None]]
|
||||||
status: str
|
status: str
|
||||||
priority: int
|
priority: int
|
||||||
estimated_minutes: 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,
|
s.sequence_in_shift,
|
||||||
t.test_type,
|
t.test_type,
|
||||||
t.rotation,
|
t.rotation,
|
||||||
|
t.config_json,
|
||||||
t.status,
|
t.status,
|
||||||
t.priority,
|
t.priority,
|
||||||
t.estimated_minutes
|
t.estimated_minutes
|
||||||
FROM schedules s
|
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 = ?
|
WHERE s.schedule_version = ?
|
||||||
AND s.scheduled_date >= ?
|
AND s.scheduled_date >= ?
|
||||||
AND s.scheduled_date < date(?, '+7 day')
|
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"]),
|
sequence_in_shift=int(row["sequence_in_shift"]),
|
||||||
test_type=row["test_type"],
|
test_type=row["test_type"],
|
||||||
rotation=row["rotation"],
|
rotation=row["rotation"],
|
||||||
|
config=json.loads(row["config_json"] or "{}"),
|
||||||
status=row["status"],
|
status=row["status"],
|
||||||
priority=int(row["priority"]),
|
priority=int(row["priority"]),
|
||||||
estimated_minutes=int(row["estimated_minutes"]),
|
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,
|
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],
|
||||||
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ def _build_graph(tests: dict[str, Any]) -> dict[str, set[str]]:
|
|||||||
|
|
||||||
|
|
||||||
def _compatible(a: Any, b: Any) -> bool:
|
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.
|
# Compatible when same rotation, or both non-P3P and overlap has identical testpoints.
|
||||||
if a.rotation == b.rotation:
|
if a.rotation == b.rotation:
|
||||||
return True
|
return True
|
||||||
@@ -57,6 +61,20 @@ def _compatible(a: Any, b: Any) -> bool:
|
|||||||
return False
|
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(
|
def _same_test_points_for_overlap(
|
||||||
config_a: dict[str, dict[str, str | None]],
|
config_a: dict[str, dict[str, str | None]],
|
||||||
config_b: 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
|
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:
|
def _norm(value: Any) -> str:
|
||||||
if value is None:
|
if value is None:
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
+61
-15
@@ -136,25 +136,37 @@ def compile_schedule(
|
|||||||
ref_active_priority: dict[TestKey, int] = dict(_ACTIVE_REF)
|
ref_active_priority: dict[TestKey, int] = dict(_ACTIVE_REF)
|
||||||
entries: list[ScheduleEntry] = []
|
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
|
last_date: str | None = None
|
||||||
|
daytime_shift2_window_pending = daytime_testing_today
|
||||||
|
|
||||||
while dut_active_priority or ref_active_priority:
|
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)
|
# 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:
|
if not shift_sequence:
|
||||||
break # No valid shift sequence
|
break # No valid shift sequence
|
||||||
|
|
||||||
dut_active_test_ids: set[TestKey] = set(dut_active_priority.keys())
|
dut_active_test_ids: set[TestKey] = set(dut_active_priority.keys())
|
||||||
ref_active_test_ids: set[TestKey] = set(ref_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 = {
|
shift_capacities = {
|
||||||
(date_obj, shift_idx): _shift_capacity_for_date(
|
(date_obj, shift_idx): _shift_capacity_for_date(
|
||||||
current_date=date_obj,
|
current_date=date_obj,
|
||||||
holiday_dates=holiday_dates,
|
holiday_dates=holiday_dates,
|
||||||
start_date_override=start_date,
|
daytime_shift2_only=is_special_daytime_shift2_window and date_obj == current_date,
|
||||||
daytime_testing_today=daytime_testing_today,
|
|
||||||
).get(shift_idx, 0)
|
).get(shift_idx, 0)
|
||||||
for date_obj, shift_idx in shift_sequence
|
for date_obj, shift_idx in shift_sequence
|
||||||
}
|
}
|
||||||
@@ -173,6 +185,9 @@ def compile_schedule(
|
|||||||
if placed_last_date is not None:
|
if placed_last_date is not None:
|
||||||
last_date = placed_last_date
|
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
|
# 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 1 (1am-9am), there's shift 2 if daytime testing, then shift 3 (5pm)
|
||||||
# After shift 3 (5pm), shift 1 is next day (1am)
|
# After shift 3 (5pm), shift 1 is next day (1am)
|
||||||
@@ -347,6 +362,7 @@ def _create_bundles(
|
|||||||
dut_active_test_ids: set[TestKey],
|
dut_active_test_ids: set[TestKey],
|
||||||
ref_active_test_ids: set[TestKey],
|
ref_active_test_ids: set[TestKey],
|
||||||
tests: dict[TestKey, SchedulerTest],
|
tests: dict[TestKey, SchedulerTest],
|
||||||
|
top_priority_tests: set[str] | None = None,
|
||||||
) -> list[TestBundle]:
|
) -> list[TestBundle]:
|
||||||
"""Create bundles of tests that must run together, sorted by priority tier and efficiency.
|
"""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.
|
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] = []
|
bundles: list[TestBundle] = []
|
||||||
processed: set[TestKey] = set()
|
processed: set[TestKey] = set()
|
||||||
|
|
||||||
@@ -393,7 +412,12 @@ def _create_bundles(
|
|||||||
if not bundle_test_ids:
|
if not bundle_test_ids:
|
||||||
continue
|
continue
|
||||||
processed.update(bundle_test_ids)
|
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(
|
bundles.append(TestBundle(
|
||||||
test_ids=bundle_test_ids,
|
test_ids=bundle_test_ids,
|
||||||
priority_tier=priority_tier,
|
priority_tier=priority_tier,
|
||||||
@@ -415,7 +439,12 @@ def _create_bundles(
|
|||||||
if not bundle_test_ids:
|
if not bundle_test_ids:
|
||||||
continue
|
continue
|
||||||
processed.update(bundle_test_ids)
|
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(
|
bundles.append(TestBundle(
|
||||||
test_ids=bundle_test_ids,
|
test_ids=bundle_test_ids,
|
||||||
priority_tier=priority_tier,
|
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]
|
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:
|
if not bundle_test_ids:
|
||||||
continue
|
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(
|
bundles.append(TestBundle(
|
||||||
test_ids=bundle_test_ids,
|
test_ids=bundle_test_ids,
|
||||||
priority_tier=priority_tier,
|
priority_tier=priority_tier,
|
||||||
@@ -439,8 +473,12 @@ def _create_bundles(
|
|||||||
if key in processed:
|
if key in processed:
|
||||||
continue
|
continue
|
||||||
test = tests[key]
|
test = tests[key]
|
||||||
priority_tier = BUNDLE_PRIORITY_COE_ONLY
|
|
||||||
bundle_test_ids = [key]
|
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(
|
bundles.append(TestBundle(
|
||||||
test_ids=bundle_test_ids,
|
test_ids=bundle_test_ids,
|
||||||
priority_tier=priority_tier,
|
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(
|
def _shift_capacity_for_date(
|
||||||
current_date: date,
|
current_date: date,
|
||||||
holiday_dates: set[str],
|
holiday_dates: set[str],
|
||||||
start_date_override: str | None,
|
daytime_shift2_only: bool,
|
||||||
daytime_testing_today: bool,
|
|
||||||
) -> dict[int, int]:
|
) -> dict[int, int]:
|
||||||
iso = current_date.isoformat()
|
iso = current_date.isoformat()
|
||||||
if iso in holiday_dates:
|
if iso in holiday_dates:
|
||||||
@@ -477,8 +514,8 @@ def _shift_capacity_for_date(
|
|||||||
if is_weekend:
|
if is_weekend:
|
||||||
return {1: 480, 2: 480, 3: 480}
|
return {1: 480, 2: 480, 3: 480}
|
||||||
|
|
||||||
if start_date_override and iso == _parse_date(start_date_override).isoformat() and daytime_testing_today:
|
if daytime_shift2_only:
|
||||||
return {1: 480, 2: 480, 3: 480}
|
return {1: 0, 2: 480, 3: 0}
|
||||||
|
|
||||||
return {1: 600, 2: 0, 3: 420}
|
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()
|
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.
|
"""Generate the sequence of (date, shift_index) tuples for a scheduling window.
|
||||||
|
|
||||||
Rules per design:
|
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
|
- 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
|
- Friday: [3, 1, 2, 3, 1, 2, 3, 1] (Fri-Mon) = 24 hours continuous
|
||||||
- Saturday/Sunday/Holiday weekday: all 3 shifts
|
- 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.
|
Returns ordered list of (date, shift_index) pairs.
|
||||||
"""
|
"""
|
||||||
iso = start_date.isoformat()
|
iso = start_date.isoformat()
|
||||||
@@ -506,6 +548,10 @@ def _get_shift_sequence(start_date: date, holiday_dates: set[str]) -> list[tuple
|
|||||||
|
|
||||||
shifts: list[tuple[date, int]] = []
|
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 holiday on a weekday, treat as weekend (all 3 shifts)
|
||||||
if is_holiday and weekday < 5:
|
if is_holiday and weekday < 5:
|
||||||
shifts.append((start_date, 1))
|
shifts.append((start_date, 1))
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>my-react-app</title>
|
<title>CGW453 Scheduler</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
Generated
+328
-52
@@ -8,8 +8,10 @@
|
|||||||
"name": "my-react-app",
|
"name": "my-react-app",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.3.1",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.6"
|
"react-dom": "^19.2.6",
|
||||||
|
"tailwindcss": "^4.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
@@ -267,7 +269,6 @@
|
|||||||
"version": "1.10.0",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
||||||
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -279,7 +280,6 @@
|
|||||||
"version": "1.10.0",
|
"version": "1.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
||||||
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -290,7 +290,6 @@
|
|||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
||||||
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -495,7 +494,6 @@
|
|||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||||
@@ -506,7 +504,6 @@
|
|||||||
"version": "2.3.5",
|
"version": "2.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||||
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/gen-mapping": "^0.3.5",
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
@@ -517,7 +514,6 @@
|
|||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
@@ -527,14 +523,12 @@
|
|||||||
"version": "1.5.5",
|
"version": "1.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
"version": "0.3.31",
|
"version": "0.3.31",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/resolve-uri": "^3.1.0",
|
"@jridgewell/resolve-uri": "^3.1.0",
|
||||||
@@ -545,7 +539,6 @@
|
|||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
||||||
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
|
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -564,7 +557,6 @@
|
|||||||
"version": "0.133.0",
|
"version": "0.133.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
|
||||||
"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
|
"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/Boshen"
|
"url": "https://github.com/sponsors/Boshen"
|
||||||
@@ -577,7 +569,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -594,7 +585,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -611,7 +601,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -628,7 +617,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -645,7 +633,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -662,7 +649,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"libc": [
|
"libc": [
|
||||||
"glibc"
|
"glibc"
|
||||||
],
|
],
|
||||||
@@ -682,7 +668,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"libc": [
|
"libc": [
|
||||||
"musl"
|
"musl"
|
||||||
],
|
],
|
||||||
@@ -702,7 +687,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"libc": [
|
"libc": [
|
||||||
"glibc"
|
"glibc"
|
||||||
],
|
],
|
||||||
@@ -722,7 +706,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"libc": [
|
"libc": [
|
||||||
"glibc"
|
"glibc"
|
||||||
],
|
],
|
||||||
@@ -742,7 +725,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"libc": [
|
"libc": [
|
||||||
"glibc"
|
"glibc"
|
||||||
],
|
],
|
||||||
@@ -762,7 +744,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"libc": [
|
"libc": [
|
||||||
"musl"
|
"musl"
|
||||||
],
|
],
|
||||||
@@ -782,7 +763,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -799,7 +779,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"wasm32"
|
"wasm32"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -818,7 +797,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -835,7 +813,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -849,14 +826,281 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
|
||||||
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@tybys/wasm-util": {
|
||||||
"version": "0.10.2",
|
"version": "0.10.2",
|
||||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
|
||||||
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
|
"integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1119,7 +1363,6 @@
|
|||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -1132,6 +1375,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/escalade": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||||
@@ -1362,7 +1618,6 @@
|
|||||||
"version": "6.5.0",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
@@ -1431,7 +1686,6 @@
|
|||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -1478,6 +1732,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/hermes-estree": {
|
||||||
"version": "0.25.1",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
|
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
|
||||||
@@ -1545,6 +1805,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -1627,7 +1896,6 @@
|
|||||||
"version": "1.32.0",
|
"version": "1.32.0",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||||
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"detect-libc": "^2.0.3"
|
"detect-libc": "^2.0.3"
|
||||||
@@ -1660,7 +1928,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1681,7 +1948,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1702,7 +1968,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1723,7 +1988,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1744,7 +2008,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1765,7 +2028,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"libc": [
|
"libc": [
|
||||||
"glibc"
|
"glibc"
|
||||||
],
|
],
|
||||||
@@ -1789,7 +2051,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"libc": [
|
"libc": [
|
||||||
"musl"
|
"musl"
|
||||||
],
|
],
|
||||||
@@ -1813,7 +2074,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"libc": [
|
"libc": [
|
||||||
"glibc"
|
"glibc"
|
||||||
],
|
],
|
||||||
@@ -1837,7 +2097,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"libc": [
|
"libc": [
|
||||||
"musl"
|
"musl"
|
||||||
],
|
],
|
||||||
@@ -1861,7 +2120,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1882,7 +2140,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1922,6 +2179,15 @@
|
|||||||
"yallist": "^3.0.2"
|
"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": {
|
"node_modules/minimatch": {
|
||||||
"version": "10.2.5",
|
"version": "10.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||||
@@ -1949,7 +2215,6 @@
|
|||||||
"version": "3.3.12",
|
"version": "3.3.12",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -2055,14 +2320,12 @@
|
|||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@@ -2075,7 +2338,6 @@
|
|||||||
"version": "8.5.15",
|
"version": "8.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -2145,7 +2407,6 @@
|
|||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
|
||||||
"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
|
"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "=0.133.0",
|
"@oxc-project/types": "=0.133.0",
|
||||||
@@ -2218,17 +2479,34 @@
|
|||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.17",
|
"version": "0.2.17",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||||
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -2245,7 +2523,6 @@
|
|||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"dev": true,
|
|
||||||
"license": "0BSD",
|
"license": "0BSD",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
@@ -2307,7 +2584,6 @@
|
|||||||
"version": "8.0.16",
|
"version": "8.0.16",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
|
||||||
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
|
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.32.0",
|
||||||
|
|||||||
@@ -10,8 +10,10 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.3.1",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.6"
|
"react-dom": "^19.2.6",
|
||||||
|
"tailwindcss": "^4.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+246
-119
@@ -1,122 +1,249 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||||
import reactLogo from './assets/react.svg'
|
import Header from './components/Header'
|
||||||
import viteLogo from './assets/vite.svg'
|
import FailedBanner from './components/FailedBanner'
|
||||||
import heroImg from './assets/hero.png'
|
import Calendar from './components/Calendar'
|
||||||
import './App.css'
|
import RightPanel from './components/RightPanel'
|
||||||
|
import SettingsModal from './components/SettingsModal'
|
||||||
|
import { api, groupScheduleItems } from './api'
|
||||||
|
|
||||||
function App() {
|
// ---------------------------------------------------------------------------
|
||||||
const [count, setCount] = useState(0)
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
return (
|
function getMondayOfWeek(date) {
|
||||||
<>
|
const d = new Date(date)
|
||||||
<section id="center">
|
const day = d.getDay()
|
||||||
<div className="hero">
|
d.setDate(d.getDate() + (day === 0 ? -6 : 1 - day))
|
||||||
<img src={heroImg} className="base" width="170" height="179" alt="" />
|
d.setHours(0, 0, 0, 0)
|
||||||
<img src={reactLogo} className="framework" alt="React logo" />
|
return d
|
||||||
<img src={viteLogo} className="vite" alt="Vite logo" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1>Get started</h1>
|
|
||||||
<p>
|
|
||||||
Edit <code>src/App.jsx</code> and save to test <code>HMR</code>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="counter"
|
|
||||||
onClick={() => setCount((count) => count + 1)}
|
|
||||||
>
|
|
||||||
Count is {count}
|
|
||||||
</button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div className="ticks"></div>
|
|
||||||
|
|
||||||
<section id="next-steps">
|
|
||||||
<div id="docs">
|
|
||||||
<svg className="icon" role="presentation" aria-hidden="true">
|
|
||||||
<use href="/icons.svg#documentation-icon"></use>
|
|
||||||
</svg>
|
|
||||||
<h2>Documentation</h2>
|
|
||||||
<p>Your questions, answered</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href="https://vite.dev/" target="_blank">
|
|
||||||
<img className="logo" src={viteLogo} alt="" />
|
|
||||||
Explore Vite
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://react.dev/" target="_blank">
|
|
||||||
<img className="button-icon" src={reactLogo} alt="" />
|
|
||||||
Learn more
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div id="social">
|
|
||||||
<svg className="icon" role="presentation" aria-hidden="true">
|
|
||||||
<use href="/icons.svg#social-icon"></use>
|
|
||||||
</svg>
|
|
||||||
<h2>Connect with us</h2>
|
|
||||||
<p>Join the Vite community</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href="https://github.com/vitejs/vite" target="_blank">
|
|
||||||
<svg
|
|
||||||
className="button-icon"
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<use href="/icons.svg#github-icon"></use>
|
|
||||||
</svg>
|
|
||||||
GitHub
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://chat.vite.dev/" target="_blank">
|
|
||||||
<svg
|
|
||||||
className="button-icon"
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<use href="/icons.svg#discord-icon"></use>
|
|
||||||
</svg>
|
|
||||||
Discord
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://x.com/vite_js" target="_blank">
|
|
||||||
<svg
|
|
||||||
className="button-icon"
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<use href="/icons.svg#x-icon"></use>
|
|
||||||
</svg>
|
|
||||||
X.com
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://bsky.app/profile/vite.dev" target="_blank">
|
|
||||||
<svg
|
|
||||||
className="button-icon"
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<use href="/icons.svg#bluesky-icon"></use>
|
|
||||||
</svg>
|
|
||||||
Bluesky
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div className="ticks"></div>
|
|
||||||
<section id="spacer"></section>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (
|
||||||
|
<div className="flex flex-col min-h-screen bg-gray-950 text-gray-100">
|
||||||
|
<Header onOpenSettings={() => setSettingsOpen(true)} />
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="px-6 py-2 bg-orange-900/60 border-b border-orange-700 text-orange-200 text-sm flex items-center justify-between">
|
||||||
|
<span>{error}</span>
|
||||||
|
<button className="ml-3 text-orange-400 hover:text-white" onClick={() => setError(null)}>✕</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FailedBanner
|
||||||
|
failedTests={failedTests}
|
||||||
|
estimatedMinutes={0}
|
||||||
|
onDecision={handleRerunDecision}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<main className="flex flex-1 gap-4 p-4 overflow-hidden">
|
||||||
|
<div className="flex-1 min-w-0 flex flex-col">
|
||||||
|
<Calendar
|
||||||
|
scheduleData={scheduleData}
|
||||||
|
daytimeDateKey={daytimeEnabled ? toKey(new Date()) : null}
|
||||||
|
weekStart={weekStart}
|
||||||
|
onWeekChange={setWeekStart}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RightPanel
|
||||||
|
completionDate={completionDate}
|
||||||
|
daytimeEnabled={daytimeEnabled}
|
||||||
|
onDaytimeEnabledChange={setDaytimeEnabled}
|
||||||
|
topPriority={topPriority}
|
||||||
|
onTopPriorityChange={setTopPriority}
|
||||||
|
lowestPriority={lowestPriority}
|
||||||
|
onLowestPriorityChange={setLowestPriority}
|
||||||
|
onRemakeSchedule={handleRemakeSchedule}
|
||||||
|
loading={loading}
|
||||||
|
tonightConfigRows={tonightConfigRows}
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<SettingsModal
|
||||||
|
isOpen={settingsOpen}
|
||||||
|
onClose={() => setSettingsOpen(false)}
|
||||||
|
settings={settings}
|
||||||
|
onSave={handleSaveSettings}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div className="flex flex-col gap-3 h-full">
|
||||||
|
{/* Week navigation */}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => onWeekChange(addDays(weekStart, -7))}
|
||||||
|
className="p-1 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
|
||||||
|
title="Previous week"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<span className="text-sm font-medium text-gray-300 min-w-36 text-center">
|
||||||
|
{formatDateRange(weekStart)}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => onWeekChange(addDays(weekStart, 7))}
|
||||||
|
className="p-1 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
|
||||||
|
title="Next week"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => onWeekChange(getMondayOfWeek(new Date()))}
|
||||||
|
className="px-2 py-1 text-xs font-semibold text-gray-300 border border-gray-600 rounded hover:bg-gray-700 hover:text-white transition-colors"
|
||||||
|
title="Jump to this week"
|
||||||
|
>
|
||||||
|
Today
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 7-day grid */}
|
||||||
|
<div className="flex gap-1.5 flex-1 overflow-x-auto">
|
||||||
|
{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 (
|
||||||
|
<DayColumn
|
||||||
|
key={key}
|
||||||
|
date={date}
|
||||||
|
isToday={isToday}
|
||||||
|
activeShifts={activeShifts}
|
||||||
|
shifts={shifts}
|
||||||
|
showShift2={isWeekend || (daytimeDateKey !== null && key === daytimeDateKey)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Legend */}
|
||||||
|
<div className="flex gap-4 text-xs text-gray-400">
|
||||||
|
<span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-sm bg-gray-500 inline-block" />Pending</span>
|
||||||
|
<span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-sm bg-green-700 inline-block" />Completed</span>
|
||||||
|
<span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-sm bg-red-700 inline-block" />Failed</span>
|
||||||
|
<span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-sm bg-yellow-700 inline-block" />Invalid</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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 (
|
||||||
|
<div className="flex flex-col min-w-0 flex-1 rounded-lg border border-gray-700 bg-gray-800/40">
|
||||||
|
{/* Day header — subtle today ring, no full-column highlight */}
|
||||||
|
<div
|
||||||
|
className={`text-center py-1.5 rounded-t-lg ${
|
||||||
|
isToday ? 'bg-gray-600 text-white' : 'bg-gray-700/60 text-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<p className="text-[11px] font-semibold uppercase tracking-wide leading-none">
|
||||||
|
{dayName}
|
||||||
|
</p>
|
||||||
|
<p className={`text-base font-bold leading-tight ${isToday ? 'text-white' : 'text-gray-100'}`}>
|
||||||
|
{dayNum}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Shifts */}
|
||||||
|
<div className="flex flex-col flex-1 px-0.5 py-1">
|
||||||
|
<ShiftSlot label="12AM–9AM" tests={shifts.shift1} visible={true} active={activeShifts.has(1)} />
|
||||||
|
<ShiftSlot label="9AM–5PM" tests={shifts.shift2} visible={true} active={activeShifts.has(2)} />
|
||||||
|
<ShiftSlot label="5PM–12AM" tests={shifts.shift3} visible={true} active={activeShifts.has(3)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div className="flex items-start gap-4 px-6 py-3 bg-red-900/70 border-b border-red-700 text-red-100">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="w-5 h-5 mt-0.5 shrink-0 text-red-300"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M12 9v4m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
<span className="font-semibold">{failedTests.length} test{failedTests.length !== 1 ? 's' : ''}</span>
|
||||||
|
{' '}failed last night and require a rerun —{' '}
|
||||||
|
<span className="font-mono text-red-200">{failedTests.join(', ')}</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-red-300 mt-0.5">
|
||||||
|
Estimated rerun time: <span className="font-semibold text-red-100">{timeStr}</span>
|
||||||
|
{' '}— Rerun these tests during the day today?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 shrink-0">
|
||||||
|
<button
|
||||||
|
onClick={() => onDecision(true)}
|
||||||
|
className="px-3 py-1 text-sm font-medium bg-red-700 hover:bg-red-600 text-white rounded-md border border-red-500 transition-colors"
|
||||||
|
>
|
||||||
|
Yes
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => onDecision(false)}
|
||||||
|
className="px-3 py-1 text-sm font-medium bg-gray-700 hover:bg-gray-600 text-gray-100 rounded-md border border-gray-500 transition-colors"
|
||||||
|
>
|
||||||
|
No
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
export default function Header({ onOpenSettings }) {
|
||||||
|
return (
|
||||||
|
<header className="flex items-center justify-between px-6 py-3 bg-gray-900 border-b border-gray-700 shrink-0">
|
||||||
|
<h1 className="text-white font-semibold text-lg tracking-wide">
|
||||||
|
CGW453 Scheduler
|
||||||
|
</h1>
|
||||||
|
<button
|
||||||
|
onClick={onOpenSettings}
|
||||||
|
className="flex items-center gap-2 px-3 py-1.5 text-sm text-gray-300 bg-gray-800 border border-gray-600 rounded-md hover:bg-gray-700 hover:text-white transition-colors"
|
||||||
|
title="Settings"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="w-4 h-4"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={1.8}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Settings
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<aside className="flex flex-col gap-4 w-60 shrink-0 pt-8">
|
||||||
|
{/* Estimated Completion */}
|
||||||
|
<div>
|
||||||
|
<p className="text-[11px] font-semibold uppercase tracking-wider text-gray-500 mb-1">
|
||||||
|
Est. Completion
|
||||||
|
</p>
|
||||||
|
<p className="text-lg font-bold text-white">
|
||||||
|
{formattedCompletionDate ?? <span className="text-gray-500 text-sm font-normal">Not calculated</span>}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Daytime testing toggle */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-[11px] font-semibold uppercase tracking-wider text-gray-500">
|
||||||
|
Day Time Testing (Today)
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
role="switch"
|
||||||
|
aria-checked={daytimeEnabled}
|
||||||
|
onClick={() => onDaytimeEnabledChange(!daytimeEnabled)}
|
||||||
|
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors ${
|
||||||
|
daytimeEnabled ? 'bg-blue-600' : 'bg-gray-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`pointer-events-none inline-block h-4 w-4 rounded-full bg-white shadow transform transition-transform ${
|
||||||
|
daytimeEnabled ? 'translate-x-4' : 'translate-x-0'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Top priority */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-[11px] font-semibold uppercase tracking-wider text-gray-500 mb-1.5">
|
||||||
|
Top Priority
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
rows={3}
|
||||||
|
value={topPriority}
|
||||||
|
onChange={(e) => onTopPriorityChange(e.target.value)}
|
||||||
|
placeholder="P2PRXAX001, COERXBE002…"
|
||||||
|
className="w-full bg-gray-900 border border-gray-600 rounded-md px-2.5 py-1.5 text-xs text-gray-200 placeholder-gray-600 focus:outline-none focus:border-blue-500 resize-none font-mono"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Remake schedule */}
|
||||||
|
<button
|
||||||
|
onClick={onRemakeSchedule}
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full py-2 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-500 border border-blue-500 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{loading ? 'Working…' : 'Remake Schedule'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Config for tonight */}
|
||||||
|
<div>
|
||||||
|
<p className="text-[11px] font-semibold uppercase tracking-wider text-gray-500 mb-1.5">
|
||||||
|
Config for Today
|
||||||
|
</p>
|
||||||
|
{tonightConfigRows.length === 0 ? (
|
||||||
|
<p className="text-xs text-gray-500">No STA placement mapping available for tonight.</p>
|
||||||
|
) : (
|
||||||
|
<div className="max-h-52 overflow-y-auto rounded border border-gray-700">
|
||||||
|
<table className="w-full text-xs">
|
||||||
|
<thead className="bg-gray-900 sticky top-0">
|
||||||
|
<tr>
|
||||||
|
<th className="text-left text-gray-400 font-semibold px-2 py-1.5 border-b border-gray-700">STA</th>
|
||||||
|
<th className="text-left text-gray-400 font-semibold px-2 py-1.5 border-b border-gray-700">Testpoint</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{tonightConfigRows.map((row) => (
|
||||||
|
<tr key={row.sta} className="odd:bg-gray-950 even:bg-gray-900/70">
|
||||||
|
<td className="text-gray-200 font-semibold px-2 py-1.5 border-b border-gray-800">{row.sta}</td>
|
||||||
|
<td className="text-gray-300 px-2 py-1.5 border-b border-gray-800">{row.testPoints.join(', ')}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
|
function Field({ label, hint, children }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-gray-300 mb-1">
|
||||||
|
{label}
|
||||||
|
{hint && <span className="ml-1.5 text-gray-500 font-normal">{hint}</span>}
|
||||||
|
</label>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
||||||
|
onClick={handleBackdropClick}
|
||||||
|
>
|
||||||
|
<div className="relative bg-gray-900 border border-gray-700 rounded-xl shadow-2xl w-full max-w-xl max-h-[90vh] flex flex-col">
|
||||||
|
{/* Modal header */}
|
||||||
|
<div className="flex items-center justify-between px-5 py-4 border-b border-gray-700">
|
||||||
|
<h2 className="text-base font-semibold text-white">Settings</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-500 hover:text-white transition-colors"
|
||||||
|
title="Close"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scrollable body */}
|
||||||
|
<div className="flex flex-col gap-5 overflow-y-auto px-5 py-5">
|
||||||
|
|
||||||
|
{/* Section: Paths */}
|
||||||
|
<div>
|
||||||
|
<p className="text-[11px] font-bold uppercase tracking-widest text-gray-500 mb-3">
|
||||||
|
File Paths
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Field label="P2P / COE CSV">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={INPUT_CLS}
|
||||||
|
placeholder="/path/to/p2p_coe_tests.csv"
|
||||||
|
value={form.p2pCoeCsvPath ?? ''}
|
||||||
|
onChange={(e) => set('p2pCoeCsvPath', e.target.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="P3P CSV">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={INPUT_CLS}
|
||||||
|
placeholder="/path/to/p3p_tests.csv"
|
||||||
|
value={form.p3pCsvPath ?? ''}
|
||||||
|
onChange={(e) => set('p3pCsvPath', e.target.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="REF Result Directory">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={INPUT_CLS}
|
||||||
|
placeholder="/path/to/ref/results"
|
||||||
|
value={form.refResultDir ?? ''}
|
||||||
|
onChange={(e) => set('refResultDir', e.target.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="DUT Result Directory">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={INPUT_CLS}
|
||||||
|
placeholder="/path/to/dut/results"
|
||||||
|
value={form.dutResultDir ?? ''}
|
||||||
|
onChange={(e) => set('dutResultDir', e.target.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className="border-gray-700" />
|
||||||
|
|
||||||
|
{/* Section: Test Exclusion */}
|
||||||
|
<div>
|
||||||
|
<p className="text-[11px] font-bold uppercase tracking-widest text-gray-500 mb-3">
|
||||||
|
Test Exclusion
|
||||||
|
</p>
|
||||||
|
<Field label="Excluded Test IDs" hint="(comma-separated)">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={INPUT_CLS}
|
||||||
|
placeholder="P2PRXAX001, COERXBE002…"
|
||||||
|
value={form.testExclusion ?? ''}
|
||||||
|
onChange={(e) => set('testExclusion', e.target.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className="border-gray-700" />
|
||||||
|
|
||||||
|
{/* Section: Holidays */}
|
||||||
|
<div>
|
||||||
|
<p className="text-[11px] font-bold uppercase tracking-widest text-gray-500 mb-3">
|
||||||
|
Holidays
|
||||||
|
</p>
|
||||||
|
<Field label="Holiday Dates" hint="(comma-separated, YYYY-MM-DD)">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={INPUT_CLS}
|
||||||
|
placeholder="2026-07-04, 2026-12-25…"
|
||||||
|
value={form.holidays ?? ''}
|
||||||
|
onChange={(e) => set('holidays', e.target.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className="border-gray-700" />
|
||||||
|
|
||||||
|
{/* Section: Schedule */}
|
||||||
|
<div>
|
||||||
|
<p className="text-[11px] font-bold uppercase tracking-widest text-gray-500 mb-3">
|
||||||
|
Schedule
|
||||||
|
</p>
|
||||||
|
<Field label="Start Date Override" hint="(YYYY-MM-DD, leave blank for today)">
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className={INPUT_CLS}
|
||||||
|
value={form.startDateOverride ?? ''}
|
||||||
|
onChange={(e) => set('startDateOverride', e.target.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="flex justify-end gap-2 px-5 py-4 border-t border-gray-700">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-1.5 text-sm text-gray-300 bg-gray-800 border border-gray-600 rounded-md hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
className="px-4 py-1.5 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-500 border border-blue-500 rounded-md transition-colors"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import TestCard from './TestCard'
|
||||||
|
|
||||||
|
export default function ShiftSlot({ label, tests = [], visible = true, active = false }) {
|
||||||
|
if (!visible) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`border-t pt-1 pb-1.5 ${
|
||||||
|
active ? 'border-blue-500/50 bg-blue-950/20 rounded' : 'border-gray-700/60'
|
||||||
|
}`}>
|
||||||
|
<p className={`text-[10px] font-semibold uppercase tracking-wider mb-1 px-1 ${
|
||||||
|
active ? 'text-blue-400' : 'text-gray-500'
|
||||||
|
}`}>
|
||||||
|
{label}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col gap-0.5 px-1 min-h-4">
|
||||||
|
{tests.length === 0 ? (
|
||||||
|
<span className="text-[10px] text-gray-600 italic">—</span>
|
||||||
|
) : (
|
||||||
|
tests.map((test) => <TestCard key={`${test.test_id}-${test.device}`} test={test} />)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div className="relative group">
|
||||||
|
<div
|
||||||
|
className={`px-1.5 py-0.5 rounded border text-xs font-mono truncate cursor-default select-none ${style}`}
|
||||||
|
style={{ maxWidth: '100%' }}
|
||||||
|
>
|
||||||
|
{test.test_id}
|
||||||
|
</div>
|
||||||
|
{/* Hover tooltip */}
|
||||||
|
<div className="absolute z-50 bottom-full left-0 mb-1 hidden group-hover:block min-w-max">
|
||||||
|
<div className="bg-gray-800 border border-gray-600 rounded-md px-2.5 py-1.5 text-xs text-gray-200 shadow-lg">
|
||||||
|
<p><span className="text-gray-400">Type:</span> {test.test_type ?? '—'}</p>
|
||||||
|
<p><span className="text-gray-400">Rotation:</span> {test.rotation ?? '—'}</p>
|
||||||
|
<p><span className="text-gray-400">Device:</span> {test.device ?? '—'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
-111
@@ -1,111 +1 @@
|
|||||||
:root {
|
@import "tailwindcss";
|
||||||
--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);
|
|
||||||
}
|
|
||||||
|
|||||||
+10
-1
@@ -1,7 +1,16 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react(), tailwindcss()],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8000',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user