p2p/coe backend implemented
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
/backend/.venv
|
||||||
|
/backend/__pycache__
|
||||||
|
/backend/_run_sched_test.py
|
||||||
|
IMPLEMENTATIONPLAN.md
|
||||||
|
scheduler.db
|
||||||
|
scheduler.db*
|
||||||
+141
-85
@@ -1,68 +1,71 @@
|
|||||||
# Scheduler
|
# Scheduler
|
||||||
A test scheduler for the NJTH which compiles a schedule according to the rules and priority of the tests.
|
A test scheduler for the NJTH which compiles a best schedule according to the rules and priority of the tests. Moniters the test results and if there is failed or invalid tests, it calculates estimated time for rerun and reschedules the tests according to user's requests.
|
||||||
|
|
||||||
## Inputs
|
## Inputs
|
||||||
- Target test directory, test results directories
|
- Target tests csv
|
||||||
|
- Keys: Priority, Index, Interferer, COE Pair , Rotation, TC ID, Victim Band, 6GHz Power Mode, 5G Test Point, 5G Channel, 5G Bandwidth, 5G RSSI, 5G Direction, 5G STA, 6G Test Point, 6G Channel, 6G Bandwidth, 6G RSSI ,6G Direction, 6G STA, 2G Test Point, 2G Channel, 2G Bandwidth, 2G RSSI, 2G Direction, 2G STA
|
||||||
|
|
||||||
|
- DUT test result directory path, REF test result directory path
|
||||||
- Estimated runtime for each type of test for calculations
|
- Estimated runtime for each type of test for calculations
|
||||||
|
- Rotation definition
|
||||||
|
- Test exclusions
|
||||||
- Top priority tests
|
- Top priority tests
|
||||||
- Allow user to manually override and enter tests that needs to be run tonight
|
- Allow user to manually override and enter tests that needs to be run tonight
|
||||||
- Day time testing (Y/N)
|
- Day time testing
|
||||||
- if yes, enter how many hours
|
|
||||||
|
|
||||||
| TestID | Type | ROT | COE_Pairs | RX_TX | Priority | Scheduled_on | Completed |
|
|
||||||
| ------ | ---- | --- | --------- | ----- |--------- | ------------ | --------- |
|
|
||||||
| P2PRXAX001 | P2P | ROT1 | | RX | 3 | | 0 |
|
|
||||||
| P2PRXAX004 | P2P | ROT1 | [COERXAX001, COERXAX002] | AX | 2 | | 0 |
|
|
||||||
| P2PTXAX001 | P2P | ROT1 | | TX | 3 | | 0 |
|
|
||||||
| COERXAX001 | COE | ROT1 | | RX | 3 | | 0 |
|
|
||||||
| COERXAX002 | COE | ROT1 | | RX | 3 | | 0 |
|
|
||||||
| P2PRXAX014 | P2P | ROT2 | | RX | 3 | | 0 |
|
|
||||||
| P2PTXAX014 | P2P | ROT2 | | TX | 3 | | 0 |
|
|
||||||
|
|
||||||
## Output
|
## Output
|
||||||
### Backend
|
### Backend
|
||||||
- Compile a schedule that would require the lowest amount of time in total according to the following rules and priority:
|
- Compile a schedule that would require the lowest amount of time in total according to the following rules and priority:
|
||||||
- **Rules**
|
- **Rules**
|
||||||
- Two tests' overlapping stations are located at the same test points, they can be run in the same night
|
- Can be run in the same night
|
||||||
- i.e. when two tests have the same rotation
|
- Same rotation and same test point P2P/COE tests
|
||||||
- P2PRXAX001, P2PRXAX004, P2PTXAX001, COERXAX001, COERXAX002 can be run on the same night;
|
- COERXBE002: T1F -> STA63, T2A -> STA56, T3E -> STA4, R3
|
||||||
- P2PRXAX014, P2PTXAX014 can be run on the same night
|
- COERXBE003: T1F -> STA63, T2A -> STA56, T3E -> STA4, R3
|
||||||
- Up link down link p2p should run b2b (RX and TX)
|
|
||||||
- Ex. P2PRXAX001 and P2PTXAX001 should run b2b
|
- Different rotations but all stations are at same testpoints
|
||||||
- COE P2P bundle should run b2b
|
- R2 P2PRXAC003 5G T1C -> STA4
|
||||||
- Ex. P2PRXAX001 and COERXAX001 and COERXAX002 should run b2b
|
- R1 P2PRXAC012 2G T1C -> STA4
|
||||||
|
|
||||||
|
- Same rotation and same test point P3P tests
|
||||||
|
- Needs to be run back to back
|
||||||
|
- P2P up link (RX) and down link (TX)
|
||||||
|
- Ex. P2PRXAX001 and P2PTXAX001
|
||||||
|
- COE P2P bundle
|
||||||
|
- DUT and REF tests
|
||||||
- **Priority**
|
- **Priority**
|
||||||
- 1: highest priority (run tonight)
|
- Default priority: COE P2P pair -> P2P only -> P3P
|
||||||
- 5: lowest priority
|
- 1: highest priority (must run tonight)
|
||||||
- General priority: COE P2P pair -> P2P only -> P3P
|
- 2: COE/P2P pairs
|
||||||
- Set priority of each test according to the type when first read in the tests P2P with COE pairing -> 2, P2P only -> 3, and P3P -> 4
|
- 3: P2P only
|
||||||
- Change priority to 1 for manually entered top priority tests, meaning it will be run tonight
|
- 4: P3P
|
||||||
|
- 5: lowest priority
|
||||||
|
- Priority 1 and 5 needs to be entered manually
|
||||||
|
- Same test points and same rotation will be run first
|
||||||
|
- Same test points but different will run if there is still remaining testing time for the day
|
||||||
- **Calculation**
|
- **Calculation**
|
||||||
- Use the estimate run time for each type of test to fit as many tests as possible in a shift of testing. One shift at night on weekdays is 16hrs, on weekends 24 hours with 1hr margin
|
- Use the estimate run time for each type of test to fit as many tests as possible in a shift of testing.
|
||||||
|
- Weekdays are 16hrs, weekends are 24 hours with 1hr margin
|
||||||
|
- If day time testing for today is on, today will be counted as 24 hours
|
||||||
|
- Holidays will be counted as 24 hours
|
||||||
- Estimate run time
|
- Estimate run time
|
||||||
- P2P: 85 mins
|
- P2P: 85 mins
|
||||||
- COE: 115 mins
|
- COE: 115 mins
|
||||||
- P3P: 105 mins
|
- P3P: 105 mins
|
||||||
- If day time testing for today is yes,
|
|
||||||
- **Failed test management**
|
- **Failed test management**
|
||||||
- After there's a modification to the result directories (results are in), and there are tests that are scheduled for last night but were not added to the result directories, meaning the test failed/invalid
|
- After there's a modification to the result directories (results are in), and there are tests that are scheduled for last night but were not added to the result directories, meaning the test failed/invalid
|
||||||
- Estimate how much time for rerun:
|
- Estimate how much time for rerun:
|
||||||
- If user choose to rerun for next day, move the whole schedule down by a day
|
- If user choose to rerun for next day, move the whole schedule down by a day
|
||||||
|
- If user choose to rerun during the day, schedule unchanged
|
||||||
|
|
||||||
### Frontend
|
### Frontend Components
|
||||||
|
|
||||||
- **Calendar**
|
- **Calendar**
|
||||||
|
- Display the schedule that the tests should be conducted after compiling
|
||||||
| | Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|
|
||||||
| - | --- | --- | --- | --- | --- | --- | --- |
|
|
||||||
| S1 | TestId | TestId | TestId | TestId | TestId | TestId | TestId |
|
|
||||||
| S2 | TestId | TestId | TestId | TestId | TestId | TestId | TestId |
|
|
||||||
| S3 | TestId | TestId | TestId | TestId | TestId | TestId | TestId |
|
|
||||||
|
|
||||||
- List the order that the tests should be conducted after compiling
|
|
||||||
- Split one day into 3 eight-hour shift
|
- Split one day into 3 eight-hour shift
|
||||||
- Shift 1 is daytime, on weekdays don't include unless day time testing for today is on. On weekends it is included
|
- Shift 1: 1AM - 9AM
|
||||||
- Shift 2 and 3 are night time
|
- Shift 2: 9AM - 5PM
|
||||||
|
- On weekdays don't include unless day time testing is on. On weekends it is included
|
||||||
|
- Shifi 3: 5PM - 1AM
|
||||||
- Hover over each tests to see test details: type, rotation
|
- Hover over each tests to see test details: type, rotation
|
||||||
- Display one week at a time, arrows to go to prev/next week
|
- Display one week at a time, arrows to go to prev/next week
|
||||||
- Today will be hightlighted or boxed
|
- Today will be hightlighted or boxed
|
||||||
@@ -75,68 +78,121 @@ A test scheduler for the NJTH which compiles a schedule according to the rules a
|
|||||||
- Day time testing for today: Yes or No
|
- Day time testing for today: Yes or No
|
||||||
- If yes, include shift 1 in todays testing
|
- If yes, include shift 1 in todays testing
|
||||||
- Top priority tests
|
- Top priority tests
|
||||||
- Enter a list of comma separated TestIDs to make those test priority 1
|
- Enter a list of comma separated TestIDs to make those tests priority 1
|
||||||
- **Test Config for tonight**
|
- Lowest priority tests
|
||||||
- After the schedule is compiled, show where each station should be placed at what test point
|
- Enter a list of comma separated TestIDs to make those tests priority 5
|
||||||
- **Estimated Completion Date**
|
- **Estimated Completion Date**
|
||||||
- **Settings**
|
- **Settings**
|
||||||
- Estimated run time for each type of test
|
- 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
|
||||||
- Holidays
|
- Holidays
|
||||||
- User can enter holidays and that day will be counted as a weekend (24hrs)
|
- User can enter holidays and that day will be counted as a weekend (24hrs)
|
||||||
- **Failed Tests**
|
- **Failed Tests**
|
||||||
- Display a banner with:
|
- Display a red banner with:
|
||||||
- A message saying what tests failed last night and requires a rerun
|
- A message saying what tests failed last night and requires a rerun
|
||||||
- Estimate rerun time
|
- Estimate rerun time
|
||||||
- Yes/No button for rerun during the day
|
- Yes/No button for rerun during the day
|
||||||
- If yes, display today's shift 1 with the rerun tests
|
|
||||||
- If no, the rerun tests will be ran tonight, the whole schedule is shift down by one day, update the calendar
|
|
||||||
|
|
||||||
#### Layout
|
### Frontend Layout
|
||||||
```
|
```
|
||||||
┌──────────────────────────────────────────────────────────────┐
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
| CGW453 Scheduler
|
| CGW453 Scheduler [⚙ Settings] |
|
||||||
| ┌────────────────────────────────────┐ COMPLETION DATE
|
|──────────────────────────────────────────────────────────────|
|
||||||
| | |
|
│ ⚠ Tests[____] failed last night — est time required to |
|
||||||
| | CALENDAR |
|
│ rerun: [_____] Rerun these tests during the day today? │
|
||||||
| | |
|
| [Yes/No] |
|
||||||
| | |
|
├──────────────────────────────────────────────────────────────┤
|
||||||
|
|
| Jun 7th - June 13th < > |
|
||||||
|
|
| ┌────────────────────────────────────┐ |
|
||||||
|
|
| | SUN MON TUE WED THU FRI SAT | COMPLETION DATE |
|
||||||
|
|
| |────────────────────────────────────| Day time [Y/N] |
|
||||||
|
|
| | | Top priority:[] |
|
||||||
|
| |────────────────────────────────────| Lowest Priority:[] |
|
||||||
|
| | | |
|
||||||
|
| |────────────────────────────────────| |
|
||||||
|
| | | [Remake Schedule] |
|
||||||
|
| └────────────────────────────────────┘ |
|
||||||
└──────────────────────────────────────────────────────────────┘
|
└──────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
## Implementation
|
|
||||||
### Backend
|
|
||||||
- Python, FastAPI, SQLite for storage
|
|
||||||
- graph.py
|
|
||||||
- graph
|
|
||||||
- Node: test
|
|
||||||
- Edge: compatible
|
|
||||||
- Adjacency list
|
|
||||||
- **Graph will not be modified**
|
|
||||||
- active_tests (set)
|
|
||||||
- Keep track of tests that are not completed
|
|
||||||
- Remove test from active_tests when test completed or is excluded
|
|
||||||
- priorities (dict)
|
|
||||||
- Keep track of test's priority
|
|
||||||
- Methods
|
|
||||||
- build_graph()
|
|
||||||
|
|
||||||
- compatible(test1, test2) -> bool
|
Scheduler.py implementation
|
||||||
- all overlapping stations are at the same test point
|
Graph:
|
||||||
- Degree(t): ``len(graph[t] & active_tests)``
|
- Only built once, don't exclude any tests, implement using adjacency list
|
||||||
- Base node selection: priority -> degree
|
dut_active_priority, ref_active_priority:
|
||||||
|
- 2 dicts, one for active DUT tests one for active reference tests, dict {testId: priority, testId: priority, ...}
|
||||||
|
- Default priorities
|
||||||
|
- P2P with COE pairs: 2
|
||||||
|
- P2P only, COE tests: 3
|
||||||
|
- P3P tests: 4
|
||||||
|
- Remove tests when they are completed
|
||||||
|
- When result directories are modified
|
||||||
|
- Rebuild full dict when exclusion rules are modified
|
||||||
|
- When user updates exclusion rules in settings(frontend)
|
||||||
|
|
||||||
``
|
Methods:
|
||||||
base = min(
|
build_test_bundles():
|
||||||
active_tests, key=lambda t: (priority[t], -len(graph[t] & active_tests)))
|
- Create bundles of tests that needs to be run back to back
|
||||||
``
|
- Types of test bundles and their order
|
||||||
|
1. Failed tests rerun if exist
|
||||||
|
2. The base is P2P and has COE pairs: P2P on DUT -> COE pairings on DUT -> P2P on REF -> COE pairings on REF
|
||||||
|
3. P2P tests without COE pairs: P2PRX on DUT -> P2PTX on DUT -> P2PRX on REF -> P2PTX on REF
|
||||||
|
4. P3P tests (ALL ROT's)
|
||||||
|
```
|
||||||
|
test_bundles: {
|
||||||
|
base: base test
|
||||||
|
bundled_tests: tests that are bundled to the base test
|
||||||
|
total_time: total time of all tests base + bundled_tests
|
||||||
|
priority: priority of base test
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- sort test_bundles first with priority then with least total_time
|
||||||
|
|
||||||
### Frontend
|
create_schedule():
|
||||||
- React
|
- General rules:
|
||||||
|
- Monday to Thursday, starts on shift 3 of that day (5pm-1am), fill in test bundles untill shift 3 and shift 1 of next day(1am-9am) is filled.
|
||||||
|
- use an array of shifts: [3,1]
|
||||||
|
- total 16hr +1 hr margin
|
||||||
|
- tests needs to fit into 17hrs
|
||||||
|
- All tests bundles needs to be compatible
|
||||||
|
- Check compatibility between current test bundle base and the first test bundle base
|
||||||
|
- Friday, starts on shift 3 of Friday, fill in test bundles untill shift 1 of Monday
|
||||||
|
- use an array of shifts: [3,1,2,3,1,2,3,1]
|
||||||
|
- total 64hr + 1hr margin
|
||||||
|
- tests need to fit into 65 hours
|
||||||
|
- All tests bundles needs to be compatible
|
||||||
|
- Check compatibility between current test bundle base and the first test bundle base
|
||||||
|
- For frontend to easily fetch schedule and display it, create a Schedule structured like:
|
||||||
|
```
|
||||||
|
Schedule: {
|
||||||
|
date: {
|
||||||
|
Shift1: list[ScheduleTest] or []
|
||||||
|
Shift2: list[ScheduleTest] or []
|
||||||
|
Shift3: list[ScheduleTest] or []
|
||||||
|
},
|
||||||
|
date: {
|
||||||
|
Shift1: list[ScheduleTest] or []
|
||||||
|
Shift2: list[ScheduleTest] or []
|
||||||
|
Shift3: list[ScheduleTest] or []
|
||||||
|
}, ...
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
DUT="CGW453"
|
||||||
|
REF="CGW452"
|
||||||
+182
@@ -0,0 +1,182 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
from datetime import date, datetime
|
||||||
|
import os
|
||||||
|
|
||||||
|
from fastapi import FastAPI, HTTPException
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
import db
|
||||||
|
import graph
|
||||||
|
from parser import CsvValidationError, parse_target_csv
|
||||||
|
from scheduler import SchedulerTest, compile_schedule, remove_from_active, reset_scheduler_state
|
||||||
|
|
||||||
|
|
||||||
|
APP_ROOT = Path(__file__).resolve().parent
|
||||||
|
DB_PATH = APP_ROOT / "scheduler.db"
|
||||||
|
DUT = os.getenv("DUT", "CGW453").strip()
|
||||||
|
REF = os.getenv("REF", "CGW452").strip()
|
||||||
|
|
||||||
|
app = FastAPI(title="Scheduler API", version="0.1.0")
|
||||||
|
|
||||||
|
|
||||||
|
class LoadTestsRequest(BaseModel):
|
||||||
|
csv_path: str = Field(..., description="Absolute or backend-relative path to target CSV")
|
||||||
|
|
||||||
|
|
||||||
|
class SaveSettingsRequest(BaseModel):
|
||||||
|
settings: dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class CompileScheduleRequest(BaseModel):
|
||||||
|
start_date: str | None = Field(default=None, description="YYYY-MM-DD")
|
||||||
|
rule: str = ""
|
||||||
|
daytime_testing_today: bool = False
|
||||||
|
top_priority_tests: list[str] = Field(default_factory=list)
|
||||||
|
lowest_priority_tests: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class RemoveActiveTestsRequest(BaseModel):
|
||||||
|
test_ids: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
def on_startup() -> None:
|
||||||
|
db.init_db(DB_PATH)
|
||||||
|
graph.reset_graph_state()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/health")
|
||||||
|
def health() -> dict[str, str]:
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/settings")
|
||||||
|
def save_settings(request: SaveSettingsRequest) -> dict[str, str]:
|
||||||
|
db.save_settings(request.settings, DB_PATH)
|
||||||
|
return {"status": "saved"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/settings")
|
||||||
|
def get_settings() -> dict[str, Any]:
|
||||||
|
return db.read_settings(DB_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/tests/load")
|
||||||
|
def load_tests(request: LoadTestsRequest) -> dict[str, Any]:
|
||||||
|
csv_path = Path(request.csv_path)
|
||||||
|
if not csv_path.is_absolute():
|
||||||
|
csv_path = APP_ROOT / csv_path
|
||||||
|
|
||||||
|
try:
|
||||||
|
parsed = parse_target_csv(csv_path)
|
||||||
|
except FileNotFoundError as exc:
|
||||||
|
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||||
|
except CsvValidationError as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
count = db.upsert_tests(parsed.tests, DB_PATH)
|
||||||
|
reset_scheduler_state()
|
||||||
|
graph.reset_graph_state()
|
||||||
|
all_dut_tests = db.list_tests_for_device(DUT, DB_PATH)
|
||||||
|
graph.build_graph_once(all_dut_tests)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"loaded_tests": count,
|
||||||
|
"warnings": parsed.warnings,
|
||||||
|
"dut_graph_nodes": len(graph.get_graph()),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/schedule/active/remove")
|
||||||
|
def remove_active_tests(request: RemoveActiveTestsRequest) -> dict[str, Any]:
|
||||||
|
remove_from_active({item.strip() for item in request.test_ids if item.strip()})
|
||||||
|
return {"status": "ok", "removed": len(request.test_ids)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/schedule/compile")
|
||||||
|
def compile_schedule_endpoint(request: CompileScheduleRequest) -> dict[str, Any]:
|
||||||
|
if request.start_date:
|
||||||
|
try:
|
||||||
|
datetime.strptime(request.start_date, "%Y-%m-%d")
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=400, detail="start_date must be YYYY-MM-DD") from exc
|
||||||
|
|
||||||
|
stored_tests = db.list_schedulable_tests(DB_PATH, rule=request.rule)
|
||||||
|
if not stored_tests:
|
||||||
|
version = db.create_schedule_version([], DB_PATH)
|
||||||
|
return {
|
||||||
|
"schedule_version": version,
|
||||||
|
"scheduled_tests": 0,
|
||||||
|
"completion_date": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduler_tests = [
|
||||||
|
SchedulerTest(
|
||||||
|
test_id=t.test_id,
|
||||||
|
device=t.device,
|
||||||
|
test_type=t.test_type,
|
||||||
|
rotation=t.rotation,
|
||||||
|
rx_tx=t.rx_tx,
|
||||||
|
has_coe_pair=t.has_coe_pair,
|
||||||
|
coe_pairing=t.coe_pairing or [],
|
||||||
|
config=t.config,
|
||||||
|
estimated_minutes=t.estimated_minutes,
|
||||||
|
priority=t.priority,
|
||||||
|
raw_payload=t.raw_payload or {},
|
||||||
|
)
|
||||||
|
for t in stored_tests
|
||||||
|
]
|
||||||
|
|
||||||
|
holiday_dates = db.list_holidays(DB_PATH)
|
||||||
|
try:
|
||||||
|
entries, completion_date = compile_schedule(
|
||||||
|
tests=scheduler_tests,
|
||||||
|
start_date=request.start_date,
|
||||||
|
holiday_dates=holiday_dates,
|
||||||
|
top_priority_tests={item.strip() for item in request.top_priority_tests if item.strip()},
|
||||||
|
lowest_priority_tests={item.strip() for item in request.lowest_priority_tests if item.strip()},
|
||||||
|
daytime_testing_today=request.daytime_testing_today,
|
||||||
|
)
|
||||||
|
except RuntimeError as exc:
|
||||||
|
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
version = db.create_schedule_version(
|
||||||
|
[(e.test_id, e.device, e.scheduled_date, e.shift_index, e.sequence_in_shift) for e in entries],
|
||||||
|
DB_PATH,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"schedule_version": version,
|
||||||
|
"scheduled_tests": len(entries),
|
||||||
|
"completion_date": completion_date,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/schedule/week")
|
||||||
|
def get_schedule_week(start: str | None = None) -> dict[str, Any]:
|
||||||
|
week_start = start or date.today().isoformat()
|
||||||
|
try:
|
||||||
|
datetime.strptime(week_start, "%Y-%m-%d")
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=400, detail="start must be YYYY-MM-DD") from exc
|
||||||
|
|
||||||
|
rows = db.get_schedule_week(week_start, DB_PATH)
|
||||||
|
return {
|
||||||
|
"start_date": week_start,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"test_id": row.test_id,
|
||||||
|
"device": row.device,
|
||||||
|
"scheduled_date": row.scheduled_date,
|
||||||
|
"shift_index": row.shift_index,
|
||||||
|
"sequence_in_shift": row.sequence_in_shift,
|
||||||
|
"test_type": row.test_type,
|
||||||
|
"rotation": row.rotation,
|
||||||
|
"status": row.status,
|
||||||
|
"priority": row.priority,
|
||||||
|
"estimated_minutes": row.estimated_minutes,
|
||||||
|
}
|
||||||
|
for row in rows
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,251 @@
|
|||||||
|
Priority,Index,Interferer,COE Pair ,Rotation,TC ID,Victim Band,6GHz Power Mode,5G Test Point,5G Channel,5G Bandwidth,5G RSSI,5G Direction,5G STA,6G Test Point,6G Channel,6G Bandwidth,6G RSSI,6G Direction,6G STA,2G Test Point,2G Channel,2G Bandwidth,2G RSSI,2G Direction,2G STA
|
||||||
|
P2,3,Yes,Yes,R2,COERXAC001,5G,LPI,T1F,100,80,-76,UL,STA4,T2A,5,160,OFF,DL,STA63,T3E,6,20,-70,DL,STA56
|
||||||
|
P2,6,Yes,Yes,R2,COERXAC002,5G,LPI,T1F,161,80,-76,UL,STA4,T2A,133,160,OFF,DL,STA63,T3E,6,20,-70,DL,STA56
|
||||||
|
P2,15,Yes,Yes,R1,COERXAC003,2G,LPI,T1F,36,80,-76,DL,STA56,T2A,5,160,OFF,DL,STA63,T3E,1,20,-70,UL,STA4
|
||||||
|
P2,17,Yes,Yes,R1,COERXAC004,2G,LPI,T1F,36,80,-76,DL,STA56,T2A,133,160,OFF,DL,STA63,T3E,6,20,-70,UL,STA4
|
||||||
|
P2,19,Yes,Yes,R1,COERXAC005,2G,LPI,T1F,36,80,-76,DL,STA56,T2A,197,160,OFF,DL,STA63,T3E,11,20,-70,UL,STA4
|
||||||
|
P2,3,Yes,Yes,R1,COERXAX001,5G,LPI,T1F,100,80,-76,UL,STA56,T2A,5,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,4,Yes,Yes,R1,COERXAX002,5G,LPI,T1F,100,80,-76,UL,STA56,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,6,Yes,Yes,R1,COERXAX003,5G,LPI,T1F,161,80,-76,UL,STA56,T2A,5,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,7,Yes,Yes,R1,COERXAX004,5G,LPI,T1F,161,80,-76,UL,STA56,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,8,Yes,Yes,R1,COERXAX005,5G,SP,T1F,161,80,-76,UL,STA56,T2A,5,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,9,Yes,Yes,R1,COERXAX006,5G,LPI,T1F,161,80,-76,UL,STA56,T2A,133,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,11,Yes,Yes,R1,COERXAX007,5G,SP,T1F,100,160,-76,UL,STA56,T2A,5,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,12,Yes,Yes,R1,COERXAX008,5G,LPI,T1F,100,160,-76,UL,STA56,T2A,133,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,14,Yes,Yes,R2,COERXAX009,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,5,160,-79,UL,STA56,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,16,Yes,Yes,R2,COERXAX010,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,133,160,-79,UL,STA56,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,18,Yes,Yes,R2,COERXAX011,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,197,160,-79,UL,STA56,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,20,Yes,Yes,R2,COERXAX012,6G,SP,T1F,161,80,-76,DL,STA63,T2A,5,320,-79,UL,STA56,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,22,Yes,Yes,R2,COERXAX013,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,133,320,-79,UL,STA56,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,24,Yes,Yes,R3,COERXAX014,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,5,160,-79,DL,STA63,T3E,1,20,-70,UL,STA56
|
||||||
|
P2,26,Yes,Yes,R3,COERXAX015,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,UL,STA56
|
||||||
|
P2,28,Yes,Yes,R3,COERXAX016,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,197,160,-79,DL,STA63,T3E,11,20,-70,UL,STA56
|
||||||
|
P2,3,Yes,Yes,R3,COERXBE001,5G,LPI,T1F,100,80,-76,UL,STA63,T2A,5,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,4,Yes,Yes,R3,COERXBE002,5G,LPI,T1F,100,80,-76,UL,STA63,T2A,133,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,6,Yes,Yes,R3,COERXBE003,5G,LPI,T1F,161,80,-76,UL,STA63,T2A,5,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,7,Yes,Yes,R3,COERXBE004,5G,LPI,T1F,161,80,-76,UL,STA63,T2A,133,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,8,Yes,Yes,R3,COERXBE005,5G,SP,T1F,161,80,-76,UL,STA63,T2A,5,320,-79,DL,STA64,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,9,Yes,Yes,R3,COERXBE006,5G,LPI,T1F,161,80,-76,UL,STA63,T2A,133,320,-79,DL,STA64,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,11,Yes,Yes,R3,COERXBE007,5G,SP,T1F,100,160,-76,UL,STA63,T2A,5,320,-79,DL,STA64,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,12,Yes,Yes,R3,COERXBE008,5G,LPI,T1F,100,160,-76,UL,STA63,T2A,133,320,-79,DL,STA64,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,14,Yes,Yes,R1,COERXBE009,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,5,160,-79,UL,STA63,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,16,Yes,Yes,R1,COERXBE010,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,133,160,-79,UL,STA63,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,18,Yes,Yes,R1,COERXBE011,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,197,160,-79,UL,STA63,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,20,Yes,Yes,R1,COERXBE012,6G,SP,T1F,161,80,-76,DL,STA56,T2A,5,320,-79,UL,STA63,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,22,Yes,Yes,R1,COERXBE013,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,133,320,-79,UL,STA63,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,24,Yes,Yes,R2,COERXBE014,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,5,160,-79,DL,STA56,T3E,1,20,-70,UL,STA63
|
||||||
|
P2,26,Yes,Yes,R2,COERXBE015,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,133,160,-79,DL,STA56,T3E,6,20,-70,UL,STA63
|
||||||
|
P2,28,Yes,Yes,R2,COERXBE016,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,197,160,-79,DL,STA56,T3E,11,20,-70,UL,STA63
|
||||||
|
P2,3,Yes,Yes,R2,COETXAC001,5G,LPI,T1F,100,80,-76,DL,STA4,T1K2A,5,160,OFF,DL,STA63,T3E,6,20,-70,DL,STA56
|
||||||
|
P2,6,Yes,Yes,R2,COETXAC002,5G,LPI,T1F,161,80,-76,DL,STA4,T1K2A,133,160,OFF,DL,STA63,T3E,6,20,-70,DL,STA56
|
||||||
|
P2,15,Yes,Yes,R1,COETXAC003,2G,LPI,T1F,36,80,-76,DL,STA56,T1K2A,5,160,OFF,DL,STA63,T3E,1,20,-70,DL,STA4
|
||||||
|
P2,17,Yes,Yes,R1,COETXAC004,2G,LPI,T1F,36,80,-76,DL,STA56,T1K2A,133,160,OFF,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,19,Yes,Yes,R1,COETXAC005,2G,LPI,T1F,36,80,-76,DL,STA56,T1K2A,5,320,OFF,DL,STA63,T3E,11,20,-70,DL,STA4
|
||||||
|
P2,3,Yes,Yes,R1,COETXAX001,5G,LPI,T1F,100,80,-76,DL,STA56,T2A,5,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,4,Yes,Yes,R1,COETXAX002,5G,LPI,T1F,100,80,-76,DL,STA56,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,6,Yes,Yes,R1,COETXAX003,5G,LPI,T1F,161,80,-76,DL,STA56,T2A,5,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,7,Yes,Yes,R1,COETXAX004,5G,LPI,T1F,161,80,-76,DL,STA56,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,9,Yes,Yes,R1,COETXAX005,5G,SP,T1F,100,160,-76,DL,STA56,T2A,5,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,10,Yes,Yes,R1,COETXAX006,5G,LPI,T1F,100,160,-76,DL,STA56,T2A,133,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,12,Yes,Yes,R2,COETXAX007,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,5,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,14,Yes,Yes,R2,COETXAX008,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,133,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,16,Yes,Yes,R2,COETXAX009,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,197,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,18,Yes,Yes,R2,COETXAX010,6G,SP,T1F,161,80,-76,DL,STA63,T2A,5,320,-79,DL,STA56,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,20,Yes,Yes,R2,COETXAX011,6G,LPI,T1F,161,80,-76,DL,STA63,T2A,133,320,-79,DL,STA56,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,22,Yes,Yes,R3,COETXAX012,2G,LPI,T1F,36,80,-76,DL,STA56,T2A,5,160,-79,DL,STA63,T3E,1,20,-70,DL,STA56
|
||||||
|
P2,24,Yes,Yes,R3,COETXAX013,2G,LPI,T1F,36,80,-76,DL,STA56,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,DL,STA56
|
||||||
|
P2,26,Yes,Yes,R3,COETXAX014,2G,LPI,T1F,36,80,-76,DL,STA56,T2A,197,160,-79,DL,STA63,T3E,11,20,-70,DL,STA56
|
||||||
|
P2,3,Yes,Yes,R3,COETXBE001,5G,LPI,T1F,100,80,-76,DL,STA63,T2A,5,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,4,Yes,Yes,R3,COETXBE002,5G,LPI,T1F,100,80,-76,DL,STA63,T2A,133,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,6,Yes,Yes,R3,COETXBE003,5G,LPI,T1F,161,80,-76,DL,STA63,T2A,5,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,7,Yes,Yes,R3,COETXBE004,5G,LPI,T1F,161,80,-76,DL,STA63,T2A,133,160,-79,DL,STA56,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,9,Yes,Yes,R3,COETXBE005,5G,SP,T1F,100,160,-76,DL,STA63,T2A,5,320,-79,DL,STA64,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,10,Yes,Yes,R3,COETXBE006,5G,LPI,T1F,100,160,-76,DL,STA63,T2A,133,320,-79,DL,STA64,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,12,Yes,Yes,R1,COETXBE007,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,5,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,14,Yes,Yes,R1,COETXBE008,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,133,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,16,Yes,Yes,R1,COETXBE009,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,197,160,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,18,Yes,Yes,R1,COETXBE010,6G,SP,T1F,161,80,-76,DL,STA56,T2A,5,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,20,Yes,Yes,R1,COETXBE011,6G,LPI,T1F,161,80,-76,DL,STA56,T2A,133,320,-79,DL,STA63,T3E,6,20,-70,DL,STA4
|
||||||
|
P2,22,Yes,Yes,R2,COETXBE012,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,5,160,-79,DL,STA56,T3E,1,20,-70,DL,STA63
|
||||||
|
P2,24,Yes,Yes,R2,COETXBE013,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,133,160,-79,DL,STA56,T3E,6,20,-70,DL,STA63
|
||||||
|
P2,26,Yes,Yes,R2,COETXBE014,2G,LPI,T1F,36,80,-76,DL,STA4,T2A,197,160,-79,DL,STA56,T3E,11,20,-70,DL,STA63
|
||||||
|
P1,1,No,No,R2,P2PRXAC001,5G,LPI,T1F,36,80,-76,UL,STA4,,,,,,,,,,,,
|
||||||
|
P1,1,No,No,R2,P2PRXAC002,5G,LPI,T2B,36,80,-67,UL,STA4,,,,,,,,,,,,
|
||||||
|
P1,1,No,No,R2,P2PRXAC003,5G,LPI,T1C,36,80,-45,UL,STA4,,,,,,,,,,,,
|
||||||
|
P1,2,No,Yes,R2,P2PRXAC004,5G,LPI,T1F,100,80,-76,UL,STA4,,,,,,,,,,,,
|
||||||
|
P1,2,No,No,R2,P2PRXAC005,5G,LPI,T2B,100,80,-67,UL,STA4,,,,,,,,,,,,
|
||||||
|
P1,2,No,No,R2,P2PRXAC006,5G,LPI,T1C,100,80,-45,UL,STA4,,,,,,,,,,,,
|
||||||
|
P1,5,No,Yes,R2,P2PRXAC007,5G,LPI,T1F,161,80,-76,UL,STA4,,,,,,,,,,,,
|
||||||
|
P1,5,No,No,R2,P2PRXAC008,5G,LPI,T2B,161,80,-67,UL,STA4,,,,,,,,,,,,
|
||||||
|
P1,5,No,No,R2,P2PRXAC009,5G,LPI,T1C,161,80,-45,UL,STA4,,,,,,,,,,,,
|
||||||
|
P1,14,No,Yes,R1,P2PRXAC010,2G,LPI,,,,,,,,,,,,,T3E,1,20,-70,UL,STA4
|
||||||
|
P1,14,No,No,R1,P2PRXAC011,2G,LPI,,,,,,,,,,,,,T1D,1,20,-60,UL,STA4
|
||||||
|
P1,14,No,No,R1,P2PRXAC012,2G,LPI,,,,,,,,,,,,,T1C,1,20,-45,UL,STA4
|
||||||
|
P1,16,No,Yes,R1,P2PRXAC013,2G,LPI,,,,,,,,,,,,,T3E,6,20,-70,UL,STA4
|
||||||
|
P1,16,No,No,R1,P2PRXAC014,2G,LPI,,,,,,,,,,,,,T1D,6,20,-60,UL,STA4
|
||||||
|
P1,16,No,No,R1,P2PRXAC015,2G,LPI,,,,,,,,,,,,,T1C,6,20,-45,UL,STA4
|
||||||
|
P1,18,No,Yes,R1,P2PRXAC016,2G,LPI,,,,,,,,,,,,,T3E,11,20,-70,UL,STA4
|
||||||
|
P1,18,No,No,R1,P2PRXAC017,2G,LPI,,,,,,,,,,,,,T1D,11,20,-60,UL,STA4
|
||||||
|
P1,18,No,No,R1,P2PRXAC018,2G,LPI,,,,,,,,,,,,,T1C,11,20,-45,UL,STA4
|
||||||
|
P1,1,No,No,R1,P2PRXAX001,5G,LPI,T1F,36,80,-76,UL,STA56,,,,,,,,,,,,
|
||||||
|
P1,1,No,No,R1,P2PRXAX002,5G,LPI,T2B,36,80,-67,UL,STA56,,,,,,,,,,,,
|
||||||
|
P1,1,No,No,R1,P2PRXAX003,5G,LPI,T1C,36,80,-45,UL,STA56,,,,,,,,,,,,
|
||||||
|
P1,2,No,Yes,R1,P2PRXAX004,5G,LPI,T1F,100,80,-76,UL,STA56,,,,,,,,,,,,
|
||||||
|
P1,2,No,No,R1,P2PRXAX005,5G,LPI,T2B,100,80,-67,UL,STA56,,,,,,,,,,,,
|
||||||
|
P1,2,No,No,R1,P2PRXAX006,5G,LPI,T1C,100,80,-45,UL,STA56,,,,,,,,,,,,
|
||||||
|
P1,5,No,Yes,R1,P2PRXAX007,5G,LPI,T1F,161,80,-76,UL,STA56,,,,,,,,,,,,
|
||||||
|
P1,5,No,No,R1,P2PRXAX008,5G,LPI,T2B,161,80,-67,UL,STA56,,,,,,,,,,,,
|
||||||
|
P1,5,No,No,R1,P2PRXAX009,5G,LPI,T1C,161,80,-45,UL,STA56,,,,,,,,,,,,
|
||||||
|
P1,10,No,Yes,R1,P2PRXAX010,5G,LPI,T1F,100,160,-76,UL,STA56,,,,,,,,,,,,
|
||||||
|
P1,10,No,No,R1,P2PRXAX011,5G,LPI,T2B,100,160,-67,UL,STA56,,,,,,,,,,,,
|
||||||
|
P1,10,No,No,R1,P2PRXAX012,5G,LPI,T1C,100,160,-45,UL,STA56,,,,,,,,,,,,
|
||||||
|
P1,13,No,Yes,R2,P2PRXAX013,6G,LPI,,,,,,,T2A,5,160,-79,UL,STA56,,,,,,
|
||||||
|
P1,13,No,No,R2,P2PRXAX014,6G,LPI,,,,,,,T2B,5,160,-70,UL,STA56,,,,,,
|
||||||
|
P1,13,No,No,R2,P2PRXAX015,6G,LPI,,,,,,,T1C,5,160,-45,UL,STA56,,,,,,
|
||||||
|
P1,15,No,Yes,R2,P2PRXAX016,6G,LPI,,,,,,,T2A,133,160,-79,UL,STA56,,,,,,
|
||||||
|
P1,15,No,No,R2,P2PRXAX017,6G,LPI,,,,,,,T2B,133,160,-70,UL,STA56,,,,,,
|
||||||
|
P1,15,No,No,R2,P2PRXAX018,6G,LPI,,,,,,,T1C,133,160,-45,UL,STA63,,,,,,
|
||||||
|
P1,17,No,Yes,R2,P2PRXAX019,6G,LPI,,,,,,,T2A,197,160,-79,UL,STA56,,,,,,
|
||||||
|
P1,17,No,No,R2,P2PRXAX020,6G,LPI,,,,,,,T2B,197,160,-70,UL,STA56,,,,,,
|
||||||
|
P1,17,No,No,R2,P2PRXAX021,6G,LPI,,,,,,,T1C,197,160,-45,UL,STA56,,,,,,
|
||||||
|
P1,19,No,Yes,R2,P2PRXAX022,6G,SP,,,,,,,T2A,5,320,-79,UL,STA56,,,,,,
|
||||||
|
P1,19,No,No,R2,P2PRXAX023,6G,SP,,,,,,,T2B,5,320,-70,UL,STA56,,,,,,
|
||||||
|
P1,19,No,No,R2,P2PRXAX024,6G,SP,,,,,,,T1C,5,320,-45,UL,STA56,,,,,,
|
||||||
|
P1,21,No,Yes,R2,P2PRXAX025,6G,LPI,,,,,,,T2A,133,320,-79,UL,STA56,,,,,,
|
||||||
|
P1,21,No,No,R2,P2PRXAX026,6G,LPI,,,,,,,T2B,133,320,-70,UL,STA56,,,,,,
|
||||||
|
P1,21,No,No,R2,P2PRXAX027,6G,LPI,,,,,,,T1C,133,320,-45,UL,STA56,,,,,,
|
||||||
|
P1,23,No,Yes,R3,P2PRXAX028,2G,LPI,,,,,,,,,,,,,T3E,1,20,-70,UL,STA56
|
||||||
|
P1,23,No,No,R3,P2PRXAX029,2G,LPI,,,,,,,,,,,,,T1D,1,20,-60,UL,STA56
|
||||||
|
P1,23,No,No,R3,P2PRXAX030,2G,LPI,,,,,,,,,,,,,T1C,1,20,-45,UL,STA56
|
||||||
|
P1,25,No,Yes,R3,P2PRXAX031,2G,LPI,,,,,,,,,,,,,T3E,6,20,-70,UL,STA56
|
||||||
|
P1,25,No,No,R3,P2PRXAX032,2G,LPI,,,,,,,,,,,,,T1D,6,20,-60,UL,STA56
|
||||||
|
P1,25,No,No,R3,P2PRXAX033,2G,LPI,,,,,,,,,,,,,T1C,6,20,-45,UL,STA56
|
||||||
|
P1,27,No,Yes,R3,P2PRXAX034,2G,LPI,,,,,,,,,,,,,T3E,11,20,-70,UL,STA56
|
||||||
|
P1,27,No,No,R3,P2PRXAX035,2G,LPI,,,,,,,,,,,,,T1D,11,20,-60,UL,STA56
|
||||||
|
P1,27,No,No,R3,P2PRXAX036,2G,LPI,,,,,,,,,,,,,T1C,11,20,-45,UL,STA56
|
||||||
|
P1,1,No,No,R3,P2PRXBE001,5G,LPI,T1F,36,80,-76,UL,STA63,,,,,,,,,,,,
|
||||||
|
P1,1,No,No,R3,P2PRXBE002,5G,LPI,T2B,36,80,-67,UL,STA63,,,,,,,,,,,,
|
||||||
|
P1,1,No,No,R3,P2PRXBE003,5G,LPI,T1C,36,80,-45,UL,STA63,,,,,,,,,,,,
|
||||||
|
P1,2,No,Yes,R3,P2PRXBE004,5G,LPI,T1F,100,80,-76,UL,STA63,,,,,,,,,,,,
|
||||||
|
P1,2,No,No,R3,P2PRXBE005,5G,LPI,T2B,100,80,-67,UL,STA63,,,,,,,,,,,,
|
||||||
|
P1,2,No,No,R3,P2PRXBE006,5G,LPI,T1C,100,80,-45,UL,STA63,,,,,,,,,,,,
|
||||||
|
P1,5,No,Yes,R3,P2PRXBE007,5G,LPI,T1F,161,80,-76,UL,STA63,,,,,,,,,,,,
|
||||||
|
P1,5,No,No,R3,P2PRXBE008,5G,LPI,T2B,161,80,-67,UL,STA63,,,,,,,,,,,,
|
||||||
|
P1,5,No,No,R3,P2PRXBE009,5G,LPI,T1C,161,80,-45,UL,STA63,,,,,,,,,,,,
|
||||||
|
P1,10,No,Yes,R3,P2PRXBE010,5G,LPI,T1F,100,160,-76,UL,STA63,,,,,,,,,,,,
|
||||||
|
P1,10,No,No,R3,P2PRXBE011,5G,LPI,T2B,100,160,-67,UL,STA63,,,,,,,,,,,,
|
||||||
|
P1,10,No,No,R3,P2PRXBE012,5G,LPI,T1C,100,160,-45,UL,STA63,,,,,,,,,,,,
|
||||||
|
P1,13,No,Yes,R1,P2PRXBE013,6G,LPI,,,,,,,T2A,5,160,-79,UL,STA63,,,,,,
|
||||||
|
P1,13,No,No,R1,P2PRXBE014,6G,LPI,,,,,,,T2B,5,160,-70,UL,STA63,,,,,,
|
||||||
|
P1,13,No,No,R1,P2PRXBE015,6G,LPI,,,,,,,T1C,5,160,-45,UL,STA63,,,,,,
|
||||||
|
P1,15,No,Yes,R1,P2PRXBE016,6G,LPI,,,,,,,T2A,133,160,-79,UL,STA63,,,,,,
|
||||||
|
P1,15,No,No,R1,P2PRXBE017,6G,LPI,,,,,,,T2B,133,160,-70,UL,STA63,,,,,,
|
||||||
|
P1,15,No,No,R1,P2PRXBE018,6G,LPI,,,,,,,T1C,133,160,-45,UL,STA63,,,,,,
|
||||||
|
P1,17,No,Yes,R1,P2PRXBE019,6G,LPI,,,,,,,T2A,197,160,-79,UL,STA63,,,,,,
|
||||||
|
P1,17,No,No,R1,P2PRXBE020,6G,LPI,,,,,,,T2B,197,160,-70,UL,STA63,,,,,,
|
||||||
|
P1,17,No,No,R1,P2PRXBE021,6G,LPI,,,,,,,T1C,197,160,-45,UL,STA63,,,,,,
|
||||||
|
P1,19,No,Yes,R1,P2PRXBE022,6G,SP,,,,,,,T2A,5,320,-79,UL,STA63,,,,,,
|
||||||
|
P1,19,No,No,R1,P2PRXBE023,6G,SP,,,,,,,T2B,5,320,-70,UL,STA63,,,,,,
|
||||||
|
P1,19,No,No,R1,P2PRXBE024,6G,SP,,,,,,,T1C,5,320,-45,UL,STA63,,,,,,
|
||||||
|
P1,21,No,Yes,R1,P2PRXBE025,6G,LPI,,,,,,,T2A,133,320,-79,UL,STA63,,,,,,
|
||||||
|
P1,21,No,No,R1,P2PRXBE026,6G,LPI,,,,,,,T2B,133,320,-70,UL,STA63,,,,,,
|
||||||
|
P1,21,No,No,R1,P2PRXBE027,6G,LPI,,,,,,,T1C,133,320,-45,UL,STA63,,,,,,
|
||||||
|
P1,23,No,Yes,R2,P2PRXBE028,2G,LPI,,,,,,,,,,,,,T3E,1,20,-70,UL,STA63
|
||||||
|
P1,23,No,No,R2,P2PRXBE029,2G,LPI,,,,,,,,,,,,,T1D,1,20,-60,UL,STA63
|
||||||
|
P1,23,No,No,R2,P2PRXBE030,2G,LPI,,,,,,,,,,,,,T1C,1,20,-45,UL,STA63
|
||||||
|
P1,25,No,Yes,R2,P2PRXBE031,2G,LPI,,,,,,,,,,,,,T3E,6,20,-70,UL,STA63
|
||||||
|
P1,25,No,No,R2,P2PRXBE032,2G,LPI,,,,,,,,,,,,,T1D,6,20,-60,UL,STA63
|
||||||
|
P1,25,No,No,R2,P2PRXBE033,2G,LPI,,,,,,,,,,,,,T1C,6,20,-45,UL,STA63
|
||||||
|
P1,27,No,Yes,R2,P2PRXBE034,2G,LPI,,,,,,,,,,,,,T3E,11,20,-70,UL,STA63
|
||||||
|
P1,27,No,No,R2,P2PRXBE035,2G,LPI,,,,,,,,,,,,,T1D,11,20,-60,UL,STA63
|
||||||
|
P1,27,No,No,R2,P2PRXBE036,2G,LPI,,,,,,,,,,,,,T1C,11,20,-45,UL,STA63
|
||||||
|
P1,1,No,No,R2,P2PTXAC001,5G,LPI,T1F,36,80,-76,DL,STA4,,,,,,,,,,,,
|
||||||
|
P1,1,No,No,R2,P2PTXAC002,5G,LPI,T2B,36,80,-67,DL,STA4,,,,,,,,,,,,
|
||||||
|
P1,1,No,No,R2,P2PTXAC003,5G,LPI,T1C,36,80,-45,DL,STA4,,,,,,,,,,,,
|
||||||
|
P1,2,No,Yes,R2,P2PTXAC004,5G,LPI,T1F,100,80,-76,DL,STA4,,,,,,,,,,,,
|
||||||
|
P1,2,No,No,R2,P2PTXAC005,5G,LPI,T2B,100,80,-67,DL,STA4,,,,,,,,,,,,
|
||||||
|
P1,2,No,No,R2,P2PTXAC006,5G,LPI,T1C,100,80,-45,DL,STA4,,,,,,,,,,,,
|
||||||
|
P1,5,No,Yes,R2,P2PTXAC007,5G,LPI,T1F,161,80,-76,DL,STA4,,,,,,,,,,,,
|
||||||
|
P1,5,No,No,R2,P2PTXAC008,5G,LPI,T2B,161,80,-67,DL,STA4,,,,,,,,,,,,
|
||||||
|
P1,5,No,No,R2,P2PTXAC009,5G,LPI,T1C,161,80,-45,DL,STA4,,,,,,,,,,,,
|
||||||
|
P1,14,No,Yes,R1,P2PTXAC010,2G,LPI,,,,,,,,,,,,,T3E,1,20,-70,DL,STA4
|
||||||
|
P1,14,No,No,R1,P2PTXAC011,2G,LPI,,,,,,,,,,,,,T1D,1,20,-60,DL,STA4
|
||||||
|
P1,14,No,No,R1,P2PTXAC012,2G,LPI,,,,,,,,,,,,,T1C,1,20,-45,DL,STA4
|
||||||
|
P1,16,No,Yes,R1,P2PTXAC013,2G,LPI,,,,,,,,,,,,,T3E,6,20,-70,DL,STA4
|
||||||
|
P1,16,No,No,R1,P2PTXAC014,2G,LPI,,,,,,,,,,,,,T1D,6,20,-60,DL,STA4
|
||||||
|
P1,16,No,No,R1,P2PTXAC015,2G,LPI,,,,,,,,,,,,,T1C,6,20,-45,DL,STA4
|
||||||
|
P1,18,No,Yes,R1,P2PTXAC016,2G,LPI,,,,,,,,,,,,,T3E,11,20,-70,DL,STA4
|
||||||
|
P1,18,No,No,R1,P2PTXAC017,2G,LPI,,,,,,,,,,,,,T1D,11,20,-60,DL,STA4
|
||||||
|
P1,18,No,No,R1,P2PTXAC018,2G,LPI,,,,,,,,,,,,,T1C,11,20,-45,DL,STA4
|
||||||
|
P1,1,No,No,R1,P2PTXAX001,5G,LPI,T1F,36,80,-76,DL,STA56,,,,,,,,,,,,
|
||||||
|
P1,1,No,No,R1,P2PTXAX002,5G,LPI,T2B,36,80,-67,DL,STA56,,,,,,,,,,,,
|
||||||
|
P1,1,No,No,R1,P2PTXAX003,5G,LPI,T1C,36,80,-45,DL,STA56,,,,,,,,,,,,
|
||||||
|
P1,2,No,Yes,R1,P2PTXAX004,5G,LPI,T1F,100,80,-76,DL,STA56,,,,,,,,,,,,
|
||||||
|
P1,2,No,No,R1,P2PTXAX005,5G,LPI,T2B,100,80,-67,DL,STA56,,,,,,,,,,,,
|
||||||
|
P1,2,No,No,R1,P2PTXAX006,5G,LPI,T1C,100,80,-45,DL,STA56,,,,,,,,,,,,
|
||||||
|
P1,5,No,Yes,R1,P2PTXAX007,5G,LPI,T1F,161,80,-76,DL,STA56,,,,,,,,,,,,
|
||||||
|
P1,5,No,No,R1,P2PTXAX008,5G,LPI,T2B,161,80,-67,DL,STA56,,,,,,,,,,,,
|
||||||
|
P1,5,No,No,R1,P2PTXAX009,5G,LPI,T1C,161,80,-45,DL,STA56,,,,,,,,,,,,
|
||||||
|
P1,8,No,Yes,R1,P2PTXAX010,5G,LPI,T1F,100,160,-76,DL,STA56,,,,,,,,,,,,
|
||||||
|
P1,8,No,No,R1,P2PTXAX011,5G,LPI,T2B,100,160,-67,DL,STA56,,,,,,,,,,,,
|
||||||
|
P1,8,No,No,R1,P2PTXAX012,5G,LPI,T1C,100,160,-45,DL,STA56,,,,,,,,,,,,
|
||||||
|
P1,11,No,Yes,R2,P2PTXAX013,6G,LPI,,,,,,,T2A,5,160,-79,DL,STA56,,,,,,
|
||||||
|
P1,11,No,No,R2,P2PTXAX014,6G,LPI,,,,,,,T2B,5,160,-70,DL,STA56,,,,,,
|
||||||
|
P1,11,No,No,R2,P2PTXAX015,6G,LPI,,,,,,,T1C,5,160,-45,DL,STA56,,,,,,
|
||||||
|
P1,13,No,Yes,R2,P2PTXAX016,6G,LPI,,,,,,,T2A,133,160,-79,DL,STA56,,,,,,
|
||||||
|
P1,13,No,No,R2,P2PTXAX017,6G,LPI,,,,,,,T2B,133,160,-70,DL,STA56,,,,,,
|
||||||
|
P1,13,No,No,R2,P2PTXAX018,6G,LPI,,,,,,,T1C,133,160,-45,DL,STA56,,,,,,
|
||||||
|
P1,15,No,Yes,R2,P2PTXAX019,6G,LPI,,,,,,,T2A,197,160,-79,DL,STA56,,,,,,
|
||||||
|
P1,15,No,No,R2,P2PTXAX020,6G,LPI,,,,,,,T2B,197,160,-70,DL,STA56,,,,,,
|
||||||
|
P1,15,No,No,R2,P2PTXAX021,6G,LPI,,,,,,,T1C,197,160,-45,DL,STA56,,,,,,
|
||||||
|
P1,17,No,Yes,R2,P2PTXAX022,6G,SP,,,,,,,T2A,5,320,-79,DL,STA56,,,,,,
|
||||||
|
P1,17,No,No,R2,P2PTXAX023,6G,SP,,,,,,,T2B,5,320,-70,DL,STA56,,,,,,
|
||||||
|
P1,17,No,No,R2,P2PTXAX024,6G,SP,,,,,,,T1C,5,320,-45,DL,STA56,,,,,,
|
||||||
|
P1,19,No,Yes,R2,P2PTXAX025,6G,LPI,,,,,,,T2A,133,320,-79,DL,STA56,,,,,,
|
||||||
|
P1,19,No,No,R2,P2PTXAX026,6G,LPI,,,,,,,T2B,133,320,-70,DL,STA56,,,,,,
|
||||||
|
P1,19,No,No,R2,P2PTXAX027,6G,LPI,,,,,,,T1C,133,320,-45,DL,STA56,,,,,,
|
||||||
|
P1,21,No,Yes,R3,P2PTXAX028,2G,LPI,,,,,,,,,,,,,T3E,1,20,-70,UL,STA56
|
||||||
|
P1,21,No,No,R3,P2PTXAX029,2G,LPI,,,,,,,,,,,,,T1D,1,20,-60,UL,STA56
|
||||||
|
P1,21,No,No,R3,P2PTXAX030,2G,LPI,,,,,,,,,,,,,T1C,1,20,-45,UL,STA56
|
||||||
|
P1,23,No,Yes,R3,P2PTXAX031,2G,LPI,,,,,,,,,,,,,T3E,6,20,-70,UL,STA56
|
||||||
|
P1,23,No,No,R3,P2PTXAX032,2G,LPI,,,,,,,,,,,,,T1D,6,20,-60,UL,STA56
|
||||||
|
P1,23,No,No,R3,P2PTXAX033,2G,LPI,,,,,,,,,,,,,T1C,6,20,-45,UL,STA56
|
||||||
|
P1,25,No,Yes,R3,P2PTXAX034,2G,LPI,,,,,,,,,,,,,T3E,11,20,-70,UL,STA56
|
||||||
|
P1,25,No,No,R3,P2PTXAX035,2G,LPI,,,,,,,,,,,,,T1D,11,20,-60,UL,STA56
|
||||||
|
P1,25,No,No,R3,P2PTXAX036,2G,LPI,,,,,,,,,,,,,T1C,11,20,-45,UL,STA56
|
||||||
|
P1,1,No,No,R3,P2PTXBE001,5G,LPI,T1F,36,80,-76,DL,STA63,,,,,,,,,,,,
|
||||||
|
P1,1,No,No,R3,P2PTXBE002,5G,LPI,T2B,36,80,-67,DL,STA63,,,,,,,,,,,,
|
||||||
|
P1,1,No,No,R3,P2PTXBE003,5G,LPI,T1C,36,80,-45,DL,STA63,,,,,,,,,,,,
|
||||||
|
P1,2,No,Yes,R3,P2PTXBE004,5G,LPI,T1F,100,80,-76,DL,STA63,,,,,,,,,,,,
|
||||||
|
P1,2,No,No,R3,P2PTXBE005,5G,LPI,T2B,100,80,-67,DL,STA63,,,,,,,,,,,,
|
||||||
|
P1,2,No,No,R3,P2PTXBE006,5G,LPI,T1C,100,80,-45,DL,STA63,,,,,,,,,,,,
|
||||||
|
P1,5,No,Yes,R3,P2PTXBE007,5G,LPI,T1F,161,80,-76,DL,STA63,,,,,,,,,,,,
|
||||||
|
P1,5,No,No,R3,P2PTXBE008,5G,LPI,T2B,161,80,-67,DL,STA63,,,,,,,,,,,,
|
||||||
|
P1,5,No,No,R3,P2PTXBE009,5G,LPI,T1C,161,80,-45,DL,STA63,,,,,,,,,,,,
|
||||||
|
P1,8,No,Yes,R3,P2PTXBE010,5G,LPI,T1F,100,160,-76,DL,STA63,,,,,,,,,,,,
|
||||||
|
P1,8,No,No,R3,P2PTXBE011,5G,LPI,T2B,100,160,-67,DL,STA63,,,,,,,,,,,,
|
||||||
|
P1,8,No,No,R3,P2PTXBE012,5G,LPI,T1C,100,160,-45,DL,STA63,,,,,,,,,,,,
|
||||||
|
P1,11,No,Yes,R3,P2PTXBE013,6G,LPI,T1F,5,160,-79,DL,STA63,,,,,,,,,,,,
|
||||||
|
P1,11,No,No,R1,P2PTXBE014,6G,LPI,T2B,5,160,-70,DL,STA63,,,,,,,,,,,,
|
||||||
|
P1,11,No,No,R1,P2PTXBE015,6G,LPI,T1C,5,160,-45,DL,STA63,,,,,,,,,,,,
|
||||||
|
P1,13,No,Yes,R1,P2PTXBE016,6G,LPI,T1F,133,160,-79,DL,STA63,,,,,,,,,,,,
|
||||||
|
P1,13,No,No,R1,P2PTXBE017,6G,LPI,T2B,133,160,-70,DL,STA63,,,,,,,,,,,,
|
||||||
|
P1,13,No,No,R1,P2PTXBE018,6G,LPI,T1C,133,160,-45,DL,STA63,,,,,,,,,,,,
|
||||||
|
P1,15,No,Yes,R1,P2PTXBE019,6G,LPI,T1F,197,160,-79,DL,STA63,,,,,,,,,,,,
|
||||||
|
P1,15,No,No,R1,P2PTXBE020,6G,LPI,T2B,197,160,-70,DL,STA63,,,,,,,,,,,,
|
||||||
|
P1,15,No,No,R1,P2PTXBE021,6G,LPI,T1C,197,160,-45,DL,STA63,,,,,,,,,,,,
|
||||||
|
P1,17,No,Yes,R1,P2PTXBE022,6G,SP,T1F,5,320,-79,DL,STA63,,,,,,,,,,,,
|
||||||
|
P1,17,No,No,R1,P2PTXBE023,6G,SP,T2B,5,320,-70,DL,STA63,,,,,,,,,,,,
|
||||||
|
P1,17,No,No,R1,P2PTXBE024,6G,SP,T1C,5,320,-45,DL,STA63,,,,,,,,,,,,
|
||||||
|
P1,19,No,Yes,R1,P2PTXBE025,6G,LPI,T1F,133,320,-79,DL,STA63,,,,,,,,,,,,
|
||||||
|
P1,19,No,No,R1,P2PTXBE026,6G,LPI,T2B,133,320,-70,DL,STA63,,,,,,,,,,,,
|
||||||
|
P1,19,No,No,R1,P2PTXBE027,6G,LPI,T1C,133,320,-45,DL,STA63,,,,,,,,,,,,
|
||||||
|
P1,21,No,Yes,R2,P2PTXBE028,2G,LPI,T3E,1,20,-70,UL,STA63,,,,,,,,,,,,
|
||||||
|
P1,21,No,No,R2,P2PTXBE029,2G,LPI,T1D,1,20,-60,UL,STA63,,,,,,,,,,,,
|
||||||
|
P1,21,No,No,R2,P2PTXBE030,2G,LPI,T1C,1,20,-45,UL,STA63,,,,,,,,,,,,
|
||||||
|
P1,23,No,Yes,R2,P2PTXBE031,2G,LPI,T3E,6,20,-70,UL,STA63,,,,,,,,,,,,
|
||||||
|
P1,23,No,No,R2,P2PTXBE032,2G,LPI,T1D,6,20,-60,UL,STA63,,,,,,,,,,,,
|
||||||
|
P1,23,No,No,R2,P2PTXBE033,2G,LPI,T1C,6,20,-45,UL,STA63,,,,,,,,,,,,
|
||||||
|
P1,25,No,Yes,R2,P2PTXBE034,2G,LPI,T3E,11,20,-70,UL,STA63,,,,,,,,,,,,
|
||||||
|
P1,25,No,No,R2,P2PTXBE035,2G,LPI,T1D,11,20,-60,UL,STA63,,,,,,,,,,,,
|
||||||
|
P1,25,No,No,R2,P2PTXBE036,2G,LPI,T1C,11,20,-45,UL,STA63,,,,,,,,,,,,
|
||||||
|
@@ -0,0 +1,71 @@
|
|||||||
|
Priority,Index,Throttled,Rotation,TC ID,Band,6GHz Power Mode,Station 1 Test Point,Station 1 Channel,Station 1 Bandwidth,Station 1 RSSI,Station 1 Direction,Station 1 Rate,Station 1 STA,Station 2 Test Point,Station 2 Channel,Station 2 Bandwidth,Station 2 RSSI,Station 2 Direction,Station 2 Rate,Station 2 STA,Station 3 Test Point,Station 3 Channel,Station 3 Bandwidth,Station 3 RSSI,Station 3 Direction,Station 3 Rate,Station 3 STA
|
||||||
|
P2,1,Yes,R3,P3PRXBETH001,5G,LPI,T1F,36,80,-76,UL,22Mbps,STA63,T1I,36,80,-76,UL,22Mbps,STA64,T2J,36,80,-76,UL,11Mbps,STA65
|
||||||
|
P2,2,Yes,R3,P3PRXBETH002,5G,LPI,T1F,100,80,-76,UL,22Mbps,STA63,T1I,100,80,-76,UL,22Mbps,STA64,T2J,100,80,-76,UL,11Mbps,STA65
|
||||||
|
P2,3,Yes,R3,P3PRXBETH003,5G,LPI,T1F,161,80,-76,UL,22Mbps,STA63,T1I,161,80,-76,UL,22Mbps,STA64,T2J,161,80,-76,UL,11Mbps,STA65
|
||||||
|
P2,4,Yes,R1,P3PRXBETH004,6G,LPI,T2A,5,160,-79,UL,22Mbps,STA63,T1L,5,160,-79,UL,22Mbps,STA64,T2O,5,160,-79,UL,11Mbps,STA65
|
||||||
|
P2,5,Yes,R1,P3PRXBETH005,6G,LPI,T2A,133,160,-79,UL,22Mbps,STA63,T1L,133,160,-79,UL,22Mbps,STA64,T2O,133,160,-79,UL,11Mbps,STA65
|
||||||
|
P2,6,Yes,R1,P3PRXBETH006,6G,LPI,T2A,197,160,-79,UL,22Mbps,STA63,T1L,197,160,-79,UL,22Mbps,STA64,T2O,197,160,-79,UL,11Mbps,STA65
|
||||||
|
P2,7,Yes,R2,P3PRXBETH007,2G,LPI,T3E,6,20,-70,UL,22Mbps,STA63,T1P,6,20,-70,UL,22Mbps,STA64,T2Q,6,20,-70,UL,11Mbps,STA65
|
||||||
|
P2,8,Yes,R1,P3PRXBETH008,6G,SP,T2A,5,320,-79,UL,22Mbps,STA63,T1L,5,320,-79,UL,22Mbps,STA64,T2O,5,320,-79,UL,11Mbps,STA65
|
||||||
|
P2,9,Yes,R1,P3PRXBETH009,6G,LPI,T2A,133,320,-79,UL,22Mbps,STA63,T1L,133,320,-79,UL,22Mbps,STA64,T2O,133,320,-79,UL,11Mbps,STA65
|
||||||
|
P2,1,No,R3,P3PRXBEUT001,5G,LPI,T1F,36,80,-76,UL,Unlimited,STA63,T1I,36,80,-76,UL,Unlimited,STA64,T2J,36,80,-76,UL,Unlimited,STA65
|
||||||
|
P2,2,No,R3,P3PRXBEUT002,5G,LPI,T1F,100,80,-76,UL,Unlimited,STA63,T1I,100,80,-76,UL,Unlimited,STA64,T2J,100,80,-76,UL,Unlimited,STA65
|
||||||
|
P2,3,No,R3,P3PRXBEUT003,5G,LPI,T1F,161,80,-76,UL,Unlimited,STA63,T1I,161,80,-76,UL,Unlimited,STA64,T2J,161,80,-76,UL,Unlimited,STA65
|
||||||
|
P2,4,No,R1,P3PRXBEUT004,6G,LPI,T2A,5,160,-79,UL,Unlimited,STA63,T1L,5,160,-79,UL,Unlimited,STA64,T2O,5,160,-79,UL,Unlimited,STA65
|
||||||
|
P2,5,No,R1,P3PRXBEUT005,6G,LPI,T2A,133,160,-79,UL,Unlimited,STA63,T1L,133,160,-79,UL,Unlimited,STA64,T2O,133,160,-79,UL,Unlimited,STA65
|
||||||
|
P2,6,No,R1,P3PRXBEUT006,6G,LPI,T2A,197,160,-79,UL,Unlimited,STA63,T1L,197,160,-79,UL,Unlimited,STA64,T2O,197,160,-79,UL,Unlimited,STA65
|
||||||
|
P2,7,No,R2,P3PRXBEUT007,2G,LPI,T3E,6,20,-70,UL,Unlimited,STA63,T1P,6,20,-70,UL,Unlimited,STA64,T2Q,6,20,-70,UL,Unlimited,STA65
|
||||||
|
P2,8,No,R1,P3PRXBEUT008,6G,SP,T2A,5,320,-79,UL,Unlimited,STA63,T1L,5,320,-79,UL,Unlimited,STA64,T2O,5,320,-79,UL,Unlimited,STA65
|
||||||
|
P2,9,No,R1,P3PRXBEUT009,6G,LPI,T2A,133,320,-79,UL,Unlimited,STA63,T1L,133,320,-79,UL,Unlimited,STA64,T2O,133,320,-79,UL,Unlimited,STA65
|
||||||
|
P2,1,Yes,R1,P3PRXAXTH001,5G,LPI,T1F,36,80,-76,UL,22Mbps,STA56,T1I,36,80,-76,UL,22Mbps,STA58,T2J,36,80,-76,UL,11Mbps,STA59
|
||||||
|
P2,2,Yes,R1,P3PRXAXTH002,5G,LPI,T1F,100,80,-76,UL,22Mbps,STA56,T1I,100,80,-76,UL,22Mbps,STA58,T2J,100,80,-76,UL,11Mbps,STA59
|
||||||
|
P2,3,Yes,R1,P3PRXAXTH003,5G,LPI,T1F,161,80,-76,UL,22Mbps,STA56,T1I,161,80,-76,UL,22Mbps,STA58,T2J,161,80,-76,UL,11Mbps,STA59
|
||||||
|
P2,4,Yes,R2,P3PRXAXTH004,6G,LPI,T2A,5,160,-79,UL,22Mbps,STA56,T1L,5,160,-79,UL,22Mbps,STA58,T2O,5,160,-79,UL,11Mbps,STA59
|
||||||
|
P2,5,Yes,R2,P3PRXAXTH005,6G,LPI,T2A,133,160,-79,UL,22Mbps,STA56,T1L,133,160,-79,UL,22Mbps,STA58,T2O,133,160,-79,UL,11Mbps,STA59
|
||||||
|
P2,6,Yes,R2,P3PRXAXTH006,6G,LPI,T2A,197,160,-79,UL,22Mbps,STA56,T1L,197,160,-79,UL,22Mbps,STA58,T2O,197,160,-79,UL,11Mbps,STA59
|
||||||
|
P2,7,Yes,R3,P3PRXAXTH007,2G,LPI,T3E,6,20,-70,UL,22Mbps,STA56,T1P,6,20,-70,UL,22Mbps,STA58,T2Q,6,20,-70,UL,11Mbps,STA59
|
||||||
|
P2,8,Yes,R2,P3PRXAXTH008,6G,SP,T2A,5,320,-79,UL,22Mbps,STA56,T1L,5,320,-79,UL,22Mbps,STA58,T2O,5,320,-79,UL,11Mbps,STA59
|
||||||
|
P2,9,Yes,R2,P3PRXAXTH009,6G,LPI,T2A,133,320,-79,UL,22Mbps,STA56,T1L,133,320,-79,UL,22Mbps,STA58,T2O,133,320,-79,UL,11Mbps,STA59
|
||||||
|
P2,1,No,R1,P3PRXAXUT001,5G,LPI,T1F,36,80,-76,UL,Unlimited,STA56,T1I,36,80,-76,UL,Unlimited,STA58,T2J,36,80,-76,UL,Unlimited,STA59
|
||||||
|
P2,2,No,R1,P3PRXAXUT002,5G,LPI,T1F,100,80,-76,UL,Unlimited,STA56,T1I,100,80,-76,UL,Unlimited,STA58,T2J,100,80,-76,UL,Unlimited,STA59
|
||||||
|
P2,3,No,R1,P3PRXAXUT003,5G,LPI,T1F,161,80,-76,UL,Unlimited,STA56,T1I,161,80,-76,UL,Unlimited,STA58,T2J,161,80,-76,UL,Unlimited,STA59
|
||||||
|
P2,4,No,R2,P3PRXAXUT004,6G,LPI,T2A,5,160,-79,UL,Unlimited,STA56,T1L,5,160,-79,UL,Unlimited,STA58,T2O,5,160,-79,UL,Unlimited,STA59
|
||||||
|
P2,5,No,R2,P3PRXAXUT005,6G,LPI,T2A,133,160,-79,UL,Unlimited,STA56,T1L,133,160,-79,UL,Unlimited,STA58,T2O,133,160,-79,UL,Unlimited,STA59
|
||||||
|
P2,6,No,R2,P3PRXAXUT006,6G,LPI,T2A,197,160,-79,UL,Unlimited,STA56,T1L,197,160,-79,UL,Unlimited,STA58,T2O,197,160,-79,UL,Unlimited,STA59
|
||||||
|
P2,7,No,R3,P3PRXAXUT007,2G,LPI,T3E,6,20,-70,UL,Unlimited,STA56,T1P,6,20,-70,UL,Unlimited,STA58,T2Q,6,20,-70,UL,Unlimited,STA59
|
||||||
|
P2,8,No,R2,P3PRXAXUT008,6G,SP,T2A,5,320,-79,UL,Unlimited,STA56,T1L,5,320,-79,UL,Unlimited,STA58,T2O,5,320,-79,UL,Unlimited,STA59
|
||||||
|
P2,9,No,R2,P3PRXAXUT009,6G,LPI,T2A,133,320,-79,UL,Unlimited,STA56,T1L,133,320,-79,UL,Unlimited,STA58,T2O,133,320,-79,UL,Unlimited,STA59
|
||||||
|
P2,1,Yes,R1,P3PTXAXTH001,5G,LPI,T1F,36,80,-76,DL,22Mbps,STA56,T1I,36,80,-76,DL,22Mbps,STA58,T2J,36,80,-76,DL,11Mbps,STA59
|
||||||
|
P2,2,Yes,R1,P3PTXAXTH002,5G,LPI,T1F,100,80,-76,DL,22Mbps,STA56,T1I,100,80,-76,DL,22Mbps,STA58,T2J,100,80,-76,DL,11Mbps,STA59
|
||||||
|
P2,3,Yes,R1,P3PTXAXTH003,5G,LPI,T1F,161,80,-76,DL,22Mbps,STA56,T1I,161,80,-76,DL,22Mbps,STA58,T2J,161,80,-76,DL,11Mbps,STA59
|
||||||
|
P2,4,Yes,R2,P3PTXAXTH004,6G,LPI,T2A,5,160,-79,DL,22Mbps,STA56,T1L,5,160,-79,DL,22Mbps,STA58,T2O,5,160,-79,DL,11Mbps,STA59
|
||||||
|
P2,5,Yes,R2,P3PTXAXTH005,6G,LPI,T2A,133,160,-79,DL,22Mbps,STA56,T1L,133,160,-79,DL,22Mbps,STA58,T2O,133,160,-79,DL,11Mbps,STA59
|
||||||
|
P2,6,Yes,R2,P3PTXAXTH006,6G,LPI,T2A,197,160,-79,DL,22Mbps,STA56,T1L,197,160,-79,DL,22Mbps,STA58,T2O,197,160,-79,DL,11Mbps,STA59
|
||||||
|
P2,7,Yes,R3,P3PTXAXTH007,2G,LPI,T3E,6,20,-70,DL,22Mbps,STA56,T1P,6,20,-70,DL,22Mbps,STA58,T2Q,6,20,-70,DL,11Mbps,STA59
|
||||||
|
P2,8,Yes,R2,P3PTXAXTH008,6G,SP,T2A,5,320,-79,DL,22Mbps,STA56,T1L,5,320,-79,DL,22Mbps,STA58,T2O,5,320,-79,DL,11Mbps,STA59
|
||||||
|
P2,9,Yes,R2,P3PTXAXTH009,6G,LPI,T2A,133,320,-79,DL,22Mbps,STA56,T1L,133,320,-79,DL,22Mbps,STA58,T2O,133,320,-79,DL,11Mbps,STA59
|
||||||
|
P2,1,No,R1,P3PTXAXUT001,5G,LPI,T1F,36,80,-76,DL,Unlimited,STA56,T1I,36,80,-76,DL,Unlimited,STA58,T2J,36,80,-76,DL,Unlimited,STA59
|
||||||
|
P2,2,No,R1,P3PTXAXUT002,5G,LPI,T1F,100,80,-76,DL,Unlimited,STA56,T1I,100,80,-76,DL,Unlimited,STA58,T2J,100,80,-76,DL,Unlimited,STA59
|
||||||
|
P2,3,No,R1,P3PTXAXUT003,5G,LPI,T1F,161,80,-76,DL,Unlimited,STA56,T1I,161,80,-76,DL,Unlimited,STA58,T2J,161,80,-76,DL,Unlimited,STA59
|
||||||
|
P2,4,No,R2,P3PTXAXUT004,6G,LPI,T2A,5,160,-79,DL,Unlimited,STA56,T1L,5,160,-79,DL,Unlimited,STA58,T2O,5,160,-79,DL,Unlimited,STA59
|
||||||
|
P2,5,No,R2,P3PTXAXUT005,6G,LPI,T2A,133,160,-79,DL,Unlimited,STA56,T1L,133,160,-79,DL,Unlimited,STA58,T2O,133,160,-79,DL,Unlimited,STA59
|
||||||
|
P2,6,No,R2,P3PTXAXUT006,6G,LPI,T2A,197,160,-79,DL,Unlimited,STA56,T1L,197,160,-79,DL,Unlimited,STA58,T2O,197,160,-79,DL,Unlimited,STA59
|
||||||
|
P2,7,No,R3,P3PTXAXUT007,2G,LPI,T3E,6,20,-70,DL,Unlimited,STA56,T1P,6,20,-70,DL,Unlimited,STA58,T2Q,6,20,-70,DL,Unlimited,STA59
|
||||||
|
P2,8,No,R2,P3PTXAXUT008,6G,SP,T2A,5,320,-79,DL,Unlimited,STA56,T1L,5,320,-79,DL,Unlimited,STA58,T2O,5,320,-79,DL,Unlimited,STA59
|
||||||
|
P2,9,No,R2,P3PTXAXUT009,6G,LPI,T2A,133,320,-79,DL,Unlimited,STA56,T1L,133,320,-79,DL,Unlimited,STA58,T2O,133,320,-79,DL,Unlimited,STA59
|
||||||
|
P2,1,Yes,R2,P3PRXACTH001,5G,LPI,T1F,36,80,-76,UL,22Mbps,STA4,T1I,36,80,-76,UL,22Mbps,STA5,T2J,36,80,-76,UL,11Mbps,STA6
|
||||||
|
P2,2,Yes,R2,P3PRXACTH002,5G,LPI,T1F,100,80,-76,UL,22Mbps,STA4,T1I,100,80,-76,UL,22Mbps,STA5,T2J,100,80,-76,UL,11Mbps,STA6
|
||||||
|
P2,3,Yes,R2,P3PRXACTH003,5G,LPI,T1F,161,80,-76,UL,22Mbps,STA4,T1I,161,80,-76,UL,22Mbps,STA5,T2J,161,80,-76,UL,11Mbps,STA6
|
||||||
|
P2,4,Yes,R1,P3PRXACTH004,2G,LPI,T3E,6,20,-70,UL,22Mbps,STA4,T1P,6,20,-70,UL,22Mbps,STA5,T2Q,6,20,-70,UL,11Mbps,STA6
|
||||||
|
P2,1,No,R2,P3PRXACUT001,5G,LPI,T1F,36,80,-76,UL,Unlimited,STA4,T1I,36,80,-76,UL,Unlimited,STA5,T2J,36,80,-76,UL,Unlimited,STA6
|
||||||
|
P2,2,No,R2,P3PRXACUT002,5G,LPI,T1F,100,80,-76,UL,Unlimited,STA4,T1I,100,80,-76,UL,Unlimited,STA5,T2J,100,80,-76,UL,Unlimited,STA6
|
||||||
|
P2,3,No,R2,P3PRXACUT003,5G,LPI,T1F,161,80,-76,UL,Unlimited,STA4,T1I,161,80,-76,UL,Unlimited,STA5,T2J,161,80,-76,UL,Unlimited,STA6
|
||||||
|
P2,4,No,R1,P3PRXACUT004,2G,LPI,T3E,6,20,-70,UL,Unlimited,STA4,T1P,6,20,-70,UL,Unlimited,STA5,T2Q,6,20,-70,UL,Unlimited,STA6
|
||||||
|
P2,1,Yes,R2,P3PTXACTH001,5G,LPI,T1F,36,80,-76,DL,22Mbps,STA4,T1I,36,80,-76,DL,22Mbps,STA5,T2J,36,80,-76,DL,11Mbps,STA6
|
||||||
|
P2,2,Yes,R2,P3PTXACTH002,5G,LPI,T1F,100,80,-76,DL,22Mbps,STA4,T1I,100,80,-76,DL,22Mbps,STA5,T2J,100,80,-76,DL,11Mbps,STA6
|
||||||
|
P2,3,Yes,R2,P3PTXACTH003,5G,LPI,T1F,161,80,-76,DL,22Mbps,STA4,T1I,161,80,-76,DL,22Mbps,STA5,T2J,161,80,-76,DL,11Mbps,STA6
|
||||||
|
P2,4,Yes,R1,P3PTXACTH004,2G,LPI,T3E,6,20,-70,DL,22Mbps,STA4,T1P,6,20,-70,DL,22Mbps,STA5,T2Q,6,20,-70,DL,11Mbps,STA6
|
||||||
|
P2,1,No,R2,P3PTXACUT001,5G,LPI,T1F,36,80,-76,DL,Unlimited,STA4,T1I,36,80,-76,DL,Unlimited,STA5,T2J,36,80,-76,DL,Unlimited,STA6
|
||||||
|
P2,2,No,R2,P3PTXACUT002,5G,LPI,T1F,100,80,-76,DL,Unlimited,STA4,T1I,100,80,-76,DL,Unlimited,STA5,T2J,100,80,-76,DL,Unlimited,STA6
|
||||||
|
P2,3,No,R2,P3PTXACUT003,5G,LPI,T1F,161,80,-76,DL,Unlimited,STA4,T1I,161,80,-76,DL,Unlimited,STA5,T2J,161,80,-76,DL,Unlimited,STA6
|
||||||
|
P2,4,No,R1,P3PTXACUT004,2G,LPI,T3E,6,20,-70,DL,Unlimited,STA4,T1P,6,20,-70,DL,Unlimited,STA5,T2Q,6,20,-70,DL,Unlimited,STA6
|
||||||
|
+511
@@ -0,0 +1,511 @@
|
|||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Iterator
|
||||||
|
|
||||||
|
|
||||||
|
APP_ROOT = Path(__file__).resolve().parent
|
||||||
|
DB_PATH = APP_ROOT / "scheduler.db"
|
||||||
|
|
||||||
|
# Hardware device names (from environment or defaults)
|
||||||
|
DUT = os.getenv("DUT", "CGW453").strip()
|
||||||
|
REF = os.getenv("REF", "CGW452").strip()
|
||||||
|
|
||||||
|
# Device names for the test database (use hardware device names)
|
||||||
|
DEVICE_DUT = DUT
|
||||||
|
DEVICE_REF = REF
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class TestRecord:
|
||||||
|
test_id: str
|
||||||
|
device: str
|
||||||
|
test_type: str
|
||||||
|
rotation: str | None
|
||||||
|
rx_tx: str | None
|
||||||
|
has_coe_pair: bool
|
||||||
|
coe_pairing: list[str]
|
||||||
|
priority: int
|
||||||
|
victim_band: str | None
|
||||||
|
config: dict[str, dict[str, str | None]]
|
||||||
|
estimated_minutes: int
|
||||||
|
status: str = "pending"
|
||||||
|
excluded: bool = False
|
||||||
|
raw_payload: dict[str, Any] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ScheduleRow:
|
||||||
|
test_id: str
|
||||||
|
device: str
|
||||||
|
scheduled_date: str
|
||||||
|
shift_index: int
|
||||||
|
sequence_in_shift: int
|
||||||
|
test_type: str
|
||||||
|
rotation: str | None
|
||||||
|
status: str
|
||||||
|
priority: int
|
||||||
|
estimated_minutes: int
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def get_connection(db_path: str | Path = DB_PATH) -> Iterator[sqlite3.Connection]:
|
||||||
|
conn = sqlite3.connect(str(db_path))
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
try:
|
||||||
|
yield conn
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def init_db(db_path: str | Path = DB_PATH) -> None:
|
||||||
|
# Validate device names are set
|
||||||
|
if not DUT or not REF:
|
||||||
|
raise ValueError(f"Invalid device names: DUT={DUT!r}, REF={REF!r}")
|
||||||
|
|
||||||
|
with get_connection(db_path) as conn:
|
||||||
|
conn.executescript(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS tests (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
test_id TEXT NOT NULL,
|
||||||
|
device TEXT NOT NULL,
|
||||||
|
test_type TEXT NOT NULL CHECK (test_type IN ('P2P', 'COE', 'P3P')),
|
||||||
|
rotation TEXT,
|
||||||
|
rx_tx TEXT CHECK (rx_tx IN ('RX', 'TX') OR rx_tx IS NULL),
|
||||||
|
has_coe_pair INTEGER NOT NULL DEFAULT 0,
|
||||||
|
coe_pairing_json TEXT,
|
||||||
|
priority INTEGER NOT NULL CHECK (priority BETWEEN 1 AND 5),
|
||||||
|
victim_band TEXT,
|
||||||
|
config_json TEXT,
|
||||||
|
estimated_minutes INTEGER NOT NULL,
|
||||||
|
status TEXT NOT NULL CHECK (status IN ('pending', 'completed', 'failed', 'invalid')),
|
||||||
|
excluded INTEGER NOT NULL DEFAULT 0,
|
||||||
|
raw_payload TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(test_id, device)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS schedules (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
schedule_version INTEGER NOT NULL,
|
||||||
|
test_id TEXT NOT NULL,
|
||||||
|
device TEXT NOT NULL,
|
||||||
|
scheduled_date TEXT NOT NULL,
|
||||||
|
shift_index INTEGER NOT NULL CHECK (shift_index IN (1, 2, 3)),
|
||||||
|
sequence_in_shift INTEGER NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY(test_id, device) REFERENCES tests(test_id, device)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value_json TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS holidays (
|
||||||
|
date TEXT PRIMARY KEY,
|
||||||
|
note TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS runtime_defaults (
|
||||||
|
test_type TEXT PRIMARY KEY,
|
||||||
|
minutes INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS rerun_events (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
detected_date TEXT NOT NULL,
|
||||||
|
failed_test_ids_json TEXT NOT NULL,
|
||||||
|
estimated_rerun_minutes INTEGER NOT NULL,
|
||||||
|
rerun_during_day INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tests_status ON tests(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tests_priority ON tests(priority);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_schedules_date_shift ON schedules(scheduled_date, shift_index);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
_ensure_column(conn, "tests", "has_coe_pair", "INTEGER NOT NULL DEFAULT 0")
|
||||||
|
_ensure_column(conn, "tests", "coe_pairing_json", "TEXT")
|
||||||
|
_ensure_column(conn, "tests", "config_json", "TEXT")
|
||||||
|
_ensure_column(conn, "tests", "victim_band", "TEXT")
|
||||||
|
_ensure_column(conn, "tests", "excluded", "INTEGER NOT NULL DEFAULT 0")
|
||||||
|
|
||||||
|
# Seed runtime defaults for schedule estimation.
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO runtime_defaults(test_type, minutes) VALUES
|
||||||
|
('P2P', 80),
|
||||||
|
('COE', 115),
|
||||||
|
('P3P', 105)
|
||||||
|
ON CONFLICT(test_type) DO NOTHING
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def upsert_tests(records: list[TestRecord], db_path: str | Path = DB_PATH) -> int:
|
||||||
|
if not records:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
with get_connection(db_path) as conn:
|
||||||
|
conn.executemany(
|
||||||
|
"""
|
||||||
|
INSERT INTO tests(
|
||||||
|
test_id, device, test_type, rotation, rx_tx, has_coe_pair,
|
||||||
|
coe_pairing_json, config_json, priority, victim_band, estimated_minutes, status, excluded, raw_payload
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(test_id, device) DO UPDATE SET
|
||||||
|
test_type = excluded.test_type,
|
||||||
|
device = excluded.device,
|
||||||
|
rotation = excluded.rotation,
|
||||||
|
rx_tx = excluded.rx_tx,
|
||||||
|
has_coe_pair = excluded.has_coe_pair,
|
||||||
|
coe_pairing_json = excluded.coe_pairing_json,
|
||||||
|
config_json = excluded.config_json,
|
||||||
|
priority = excluded.priority,
|
||||||
|
victim_band = excluded.victim_band,
|
||||||
|
estimated_minutes = excluded.estimated_minutes,
|
||||||
|
status = excluded.status,
|
||||||
|
excluded = excluded.excluded,
|
||||||
|
raw_payload = excluded.raw_payload,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
""",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
r.test_id,
|
||||||
|
r.device,
|
||||||
|
r.test_type,
|
||||||
|
r.rotation,
|
||||||
|
r.rx_tx,
|
||||||
|
int(r.has_coe_pair),
|
||||||
|
json.dumps(r.coe_pairing or []),
|
||||||
|
json.dumps(r.config),
|
||||||
|
r.priority,
|
||||||
|
r.victim_band,
|
||||||
|
r.estimated_minutes,
|
||||||
|
r.status,
|
||||||
|
int(r.excluded),
|
||||||
|
json.dumps(r.raw_payload or {}),
|
||||||
|
)
|
||||||
|
for r in records
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return len(records)
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_column(conn: sqlite3.Connection, table: str, column: str, column_type: str) -> None:
|
||||||
|
existing = conn.execute(f"PRAGMA table_info({table})").fetchall()
|
||||||
|
column_names = {row[1] for row in existing}
|
||||||
|
if column not in column_names:
|
||||||
|
conn.execute(f"ALTER TABLE {table} ADD COLUMN {column} {column_type}")
|
||||||
|
|
||||||
|
|
||||||
|
def save_settings(payload: dict[str, Any], db_path: str | Path = DB_PATH) -> None:
|
||||||
|
with get_connection(db_path) as conn:
|
||||||
|
for key, value in payload.items():
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO settings(key, value_json, updated_at)
|
||||||
|
VALUES (?, ?, CURRENT_TIMESTAMP)
|
||||||
|
ON CONFLICT(key) DO UPDATE SET
|
||||||
|
value_json = excluded.value_json,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
""",
|
||||||
|
(key, json.dumps(value)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def read_settings(db_path: str | Path = DB_PATH) -> dict[str, Any]:
|
||||||
|
with get_connection(db_path) as conn:
|
||||||
|
rows = conn.execute("SELECT key, value_json FROM settings").fetchall()
|
||||||
|
return {row["key"]: json.loads(row["value_json"]) for row in rows}
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_rule_tokens(rule: str | None) -> list[str]:
|
||||||
|
if not rule:
|
||||||
|
return []
|
||||||
|
return [token.strip().upper() for token in rule.split(",") if token.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_text(value: Any) -> str:
|
||||||
|
if value is None:
|
||||||
|
return ""
|
||||||
|
return " ".join(str(value).strip().upper().split())
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_compact(value: Any) -> str:
|
||||||
|
return re.sub(r"[^A-Z0-9+\-]", "", _normalize_text(value))
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_signed_int(value: Any) -> str:
|
||||||
|
match = re.search(r"[-+]?\d+", _normalize_text(value))
|
||||||
|
return match.group(0) if match else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _band_entry(config: dict[str, dict[str, str | None]], band: str) -> dict[str, str | None]:
|
||||||
|
return config.get(band) or config.get(band.lower()) or {}
|
||||||
|
|
||||||
|
|
||||||
|
def _entry_value(entry: dict[str, str | None], key: str) -> str:
|
||||||
|
if not entry:
|
||||||
|
return ""
|
||||||
|
return _normalize_text(entry.get(key) or entry.get(key.capitalize()) or entry.get(key.upper()))
|
||||||
|
|
||||||
|
|
||||||
|
def _match_any_band_value(record: TestRecord, key: str, token_suffix: str) -> bool:
|
||||||
|
if not token_suffix:
|
||||||
|
return False
|
||||||
|
|
||||||
|
target_num = _extract_signed_int(token_suffix)
|
||||||
|
target_compact = _normalize_compact(token_suffix)
|
||||||
|
for band in ("5G", "6G", "2G"):
|
||||||
|
entry = _band_entry(record.config, band)
|
||||||
|
value = _entry_value(entry, key)
|
||||||
|
if not value:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if target_num:
|
||||||
|
if _extract_signed_int(value) == target_num:
|
||||||
|
return True
|
||||||
|
elif target_compact and _normalize_compact(value) == target_compact:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _matches_atomic_exclusion_token(record: TestRecord, token: str) -> bool:
|
||||||
|
if not token:
|
||||||
|
return False
|
||||||
|
|
||||||
|
device = _normalize_text(record.device)
|
||||||
|
test_type = _normalize_text(record.test_type)
|
||||||
|
rotation = _normalize_text(record.rotation)
|
||||||
|
victim_band = _normalize_text(record.victim_band)
|
||||||
|
power_mode = _entry_value(_band_entry(record.config, "6G"), "power_mode")
|
||||||
|
known_devices = {_normalize_text(DUT), _normalize_text(REF)}
|
||||||
|
|
||||||
|
if token in {"LPI", "SP"}:
|
||||||
|
return power_mode == token
|
||||||
|
|
||||||
|
if token in known_devices:
|
||||||
|
return device == token
|
||||||
|
|
||||||
|
if token in {"P2P", "P3P", "COE"}:
|
||||||
|
return test_type == token
|
||||||
|
|
||||||
|
if token in {"2G", "5G", "6G"}:
|
||||||
|
return victim_band == token
|
||||||
|
|
||||||
|
if token.startswith("BW"):
|
||||||
|
return _match_any_band_value(record, "bandwidth", token[2:])
|
||||||
|
|
||||||
|
if token.startswith("CH"):
|
||||||
|
return _match_any_band_value(record, "channel", token[2:])
|
||||||
|
|
||||||
|
if token.startswith("RSSI"):
|
||||||
|
return _match_any_band_value(record, "rssi", token[4:])
|
||||||
|
|
||||||
|
if token.startswith("R") and len(token) > 1:
|
||||||
|
return rotation == token
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _matches_exclusion_token(record: TestRecord, token: str) -> bool:
|
||||||
|
if not token:
|
||||||
|
return False
|
||||||
|
|
||||||
|
parts = [part for part in token.split("_") if part]
|
||||||
|
if len(parts) > 1:
|
||||||
|
# Treat underscore as logical AND across sub-tokens.
|
||||||
|
return all(_matches_atomic_exclusion_token(record, part) for part in parts)
|
||||||
|
|
||||||
|
return _matches_atomic_exclusion_token(record, token)
|
||||||
|
|
||||||
|
|
||||||
|
def _should_exclude_by_rule(record: TestRecord, tokens: list[str]) -> bool:
|
||||||
|
return any(_matches_exclusion_token(record, token) for token in tokens)
|
||||||
|
|
||||||
|
|
||||||
|
def list_schedulable_tests(db_path: str | Path = DB_PATH, rule: str = "") -> list[TestRecord]:
|
||||||
|
tokens = _parse_rule_tokens(rule)
|
||||||
|
with get_connection(db_path) as conn:
|
||||||
|
rows = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
test_id, device, test_type, rotation, rx_tx, has_coe_pair,
|
||||||
|
config_json, victim_band,
|
||||||
|
coe_pairing_json, priority, estimated_minutes,
|
||||||
|
excluded, status, raw_payload
|
||||||
|
FROM tests
|
||||||
|
WHERE excluded = 0
|
||||||
|
AND status != 'completed'
|
||||||
|
"""
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
results: list[TestRecord] = []
|
||||||
|
for row in rows:
|
||||||
|
results.append(
|
||||||
|
TestRecord(
|
||||||
|
test_id=row["test_id"],
|
||||||
|
device=row["device"],
|
||||||
|
test_type=row["test_type"],
|
||||||
|
rotation=row["rotation"],
|
||||||
|
rx_tx=row["rx_tx"],
|
||||||
|
has_coe_pair=bool(row["has_coe_pair"]),
|
||||||
|
coe_pairing=json.loads(row["coe_pairing_json"] or "[]"),
|
||||||
|
priority=int(row["priority"]),
|
||||||
|
victim_band=row["victim_band"],
|
||||||
|
config=json.loads(row["config_json"] or "{}"),
|
||||||
|
estimated_minutes=int(row["estimated_minutes"]),
|
||||||
|
status=row["status"],
|
||||||
|
excluded=bool(row["excluded"]),
|
||||||
|
raw_payload=json.loads(row["raw_payload"] or "{}"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not tokens:
|
||||||
|
return results
|
||||||
|
|
||||||
|
return [record for record in results if not _should_exclude_by_rule(record, tokens)]
|
||||||
|
# Get all tests for a specific device, regardless of exclusion or completion status. Used for schedule display and management.
|
||||||
|
def list_tests_for_device(device: str, db_path: str | Path = DB_PATH) -> list[TestRecord]:
|
||||||
|
with get_connection(db_path) as conn:
|
||||||
|
rows = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
test_id, device, test_type, rotation, rx_tx, has_coe_pair,
|
||||||
|
config_json, victim_band,
|
||||||
|
coe_pairing_json, priority, estimated_minutes,
|
||||||
|
excluded, status, raw_payload
|
||||||
|
FROM tests
|
||||||
|
WHERE device = ?
|
||||||
|
""",
|
||||||
|
(device,),
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
results: list[TestRecord] = []
|
||||||
|
for row in rows:
|
||||||
|
results.append(
|
||||||
|
TestRecord(
|
||||||
|
test_id=row["test_id"],
|
||||||
|
device=row["device"],
|
||||||
|
test_type=row["test_type"],
|
||||||
|
rotation=row["rotation"],
|
||||||
|
rx_tx=row["rx_tx"],
|
||||||
|
has_coe_pair=bool(row["has_coe_pair"]),
|
||||||
|
coe_pairing=json.loads(row["coe_pairing_json"] or "[]"),
|
||||||
|
priority=int(row["priority"]),
|
||||||
|
victim_band=row["victim_band"],
|
||||||
|
config=json.loads(row["config_json"] or "{}"),
|
||||||
|
estimated_minutes=int(row["estimated_minutes"]),
|
||||||
|
status=row["status"],
|
||||||
|
excluded=bool(row["excluded"]),
|
||||||
|
raw_payload=json.loads(row["raw_payload"] or "{}"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def list_holidays(db_path: str | Path = DB_PATH) -> set[str]:
|
||||||
|
with get_connection(db_path) as conn:
|
||||||
|
rows = conn.execute("SELECT date FROM holidays").fetchall()
|
||||||
|
return {row["date"] for row in rows}
|
||||||
|
|
||||||
|
|
||||||
|
def create_schedule_version(
|
||||||
|
entries: list[tuple[str, str, str, int, int]],
|
||||||
|
db_path: str | Path = DB_PATH,
|
||||||
|
) -> int:
|
||||||
|
with get_connection(db_path) as conn:
|
||||||
|
row = conn.execute("SELECT COALESCE(MAX(schedule_version), 0) AS current FROM schedules").fetchone()
|
||||||
|
next_version = int(row["current"]) + 1
|
||||||
|
|
||||||
|
if entries:
|
||||||
|
conn.executemany(
|
||||||
|
"""
|
||||||
|
INSERT INTO schedules(schedule_version, test_id, device, scheduled_date, shift_index, sequence_in_shift)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
[
|
||||||
|
(next_version, test_id, device, scheduled_date, shift_index, sequence)
|
||||||
|
for test_id, device, scheduled_date, shift_index, sequence in entries
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
return next_version
|
||||||
|
|
||||||
|
|
||||||
|
def get_schedule_week(start_date: str, db_path: str | Path = DB_PATH) -> list[ScheduleRow]:
|
||||||
|
with get_connection(db_path) as conn:
|
||||||
|
version_row = conn.execute("SELECT MAX(schedule_version) AS latest FROM schedules").fetchone()
|
||||||
|
latest = version_row["latest"]
|
||||||
|
if latest is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
rows = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
s.test_id,
|
||||||
|
s.device,
|
||||||
|
s.scheduled_date,
|
||||||
|
s.shift_index,
|
||||||
|
s.sequence_in_shift,
|
||||||
|
t.test_type,
|
||||||
|
t.rotation,
|
||||||
|
t.status,
|
||||||
|
t.priority,
|
||||||
|
t.estimated_minutes
|
||||||
|
FROM schedules s
|
||||||
|
JOIN tests t ON t.test_id = s.test_id
|
||||||
|
WHERE s.schedule_version = ?
|
||||||
|
AND s.scheduled_date >= ?
|
||||||
|
AND s.scheduled_date < date(?, '+7 day')
|
||||||
|
ORDER BY s.scheduled_date, s.shift_index, s.sequence_in_shift
|
||||||
|
""",
|
||||||
|
(latest, start_date, start_date),
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
result: list[ScheduleRow] = []
|
||||||
|
for row in rows:
|
||||||
|
result.append(
|
||||||
|
ScheduleRow(
|
||||||
|
test_id=row["test_id"],
|
||||||
|
device=row["device"],
|
||||||
|
scheduled_date=row["scheduled_date"],
|
||||||
|
shift_index=int(row["shift_index"]),
|
||||||
|
sequence_in_shift=int(row["sequence_in_shift"]),
|
||||||
|
test_type=row["test_type"],
|
||||||
|
rotation=row["rotation"],
|
||||||
|
status=row["status"],
|
||||||
|
priority=int(row["priority"]),
|
||||||
|
estimated_minutes=int(row["estimated_minutes"]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def mark_tests_completed(test_ids_with_device: list[tuple[str, str]], db_path: str | Path = DB_PATH) -> None:
|
||||||
|
if not test_ids_with_device:
|
||||||
|
return
|
||||||
|
|
||||||
|
with get_connection(db_path) as conn:
|
||||||
|
conn.executemany(
|
||||||
|
"""
|
||||||
|
UPDATE tests
|
||||||
|
SET status = 'completed', updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE test_id = ?
|
||||||
|
AND device = ?
|
||||||
|
AND status != 'completed'
|
||||||
|
""",
|
||||||
|
test_ids_with_device,
|
||||||
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
# Immutable-in-practice graph state for the loaded DUT test set.
|
||||||
|
_TESTS_BY_ID: dict[str, Any] = {}
|
||||||
|
_GRAPH: dict[str, set[str]] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def reset_graph_state() -> None:
|
||||||
|
global _TESTS_BY_ID, _GRAPH
|
||||||
|
_TESTS_BY_ID = {}
|
||||||
|
_GRAPH = {}
|
||||||
|
|
||||||
|
|
||||||
|
def is_graph_built() -> bool:
|
||||||
|
return bool(_GRAPH)
|
||||||
|
|
||||||
|
|
||||||
|
def build_graph_once(tests: list[Any]) -> dict[str, set[str]]:
|
||||||
|
"""Build DUT-only compatibility adjacency graph once per load lifecycle."""
|
||||||
|
global _TESTS_BY_ID, _GRAPH
|
||||||
|
if _GRAPH:
|
||||||
|
return _GRAPH
|
||||||
|
|
||||||
|
# Keep only one representative per DUT test_id.
|
||||||
|
_TESTS_BY_ID = {str(t.test_id): t for t in tests if getattr(t, "test_id", None)}
|
||||||
|
_GRAPH = _build_graph(_TESTS_BY_ID)
|
||||||
|
return _GRAPH
|
||||||
|
|
||||||
|
|
||||||
|
def get_graph() -> dict[str, set[str]]:
|
||||||
|
return _GRAPH
|
||||||
|
|
||||||
|
|
||||||
|
def _build_graph(tests: dict[str, Any]) -> dict[str, set[str]]:
|
||||||
|
test_ids = list(tests.keys())
|
||||||
|
graph = {test_id: set() for test_id in test_ids}
|
||||||
|
for i in range(len(test_ids)):
|
||||||
|
for j in range(i + 1, len(test_ids)):
|
||||||
|
a_id = test_ids[i]
|
||||||
|
b_id = test_ids[j]
|
||||||
|
a = tests[a_id]
|
||||||
|
b = tests[b_id]
|
||||||
|
if _compatible(a, b):
|
||||||
|
graph[a_id].add(b_id)
|
||||||
|
graph[b_id].add(a_id)
|
||||||
|
return graph
|
||||||
|
|
||||||
|
|
||||||
|
def _compatible(a: Any, b: Any) -> bool:
|
||||||
|
# Compatible when same rotation, or both non-P3P and overlap has identical testpoints.
|
||||||
|
if a.rotation == b.rotation:
|
||||||
|
return True
|
||||||
|
if a.test_type != "P3P" and b.test_type != "P3P" and _same_test_points_for_overlap(a.config, b.config):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _same_test_points_for_overlap(
|
||||||
|
config_a: dict[str, dict[str, str | None]],
|
||||||
|
config_b: dict[str, dict[str, str | None]],
|
||||||
|
) -> bool:
|
||||||
|
station_map_a = _build_station_testpoint_map(config_a)
|
||||||
|
station_map_b = _build_station_testpoint_map(config_b)
|
||||||
|
|
||||||
|
overlap = set(station_map_a.keys()) & set(station_map_b.keys())
|
||||||
|
for station in overlap:
|
||||||
|
if station_map_a[station] != station_map_b[station]:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _build_station_testpoint_map(config: dict[str, dict[str, str | None]]) -> dict[str, str]:
|
||||||
|
station_to_testpoint: dict[str, str] = {}
|
||||||
|
for band in ("5G", "6G", "2G"):
|
||||||
|
entry = config.get(band) or {}
|
||||||
|
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:
|
||||||
|
continue
|
||||||
|
for sta in sta_raw.split(","):
|
||||||
|
sta_clean = _norm(sta)
|
||||||
|
if sta_clean:
|
||||||
|
station_to_testpoint[sta_clean] = testpoint
|
||||||
|
return station_to_testpoint
|
||||||
|
|
||||||
|
|
||||||
|
def _norm(value: Any) -> str:
|
||||||
|
if value is None:
|
||||||
|
return ""
|
||||||
|
return " ".join(str(value).strip().upper().split())
|
||||||
+2515
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,301 @@
|
|||||||
|
import csv
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from db import DEVICE_DUT, DEVICE_REF, TestRecord
|
||||||
|
|
||||||
|
REQUIRED_COLUMNS = [
|
||||||
|
"Priority",
|
||||||
|
"Index",
|
||||||
|
"Interferer",
|
||||||
|
"COE Pair",
|
||||||
|
"Rotation",
|
||||||
|
"TC ID",
|
||||||
|
"Victim Band",
|
||||||
|
"6GHz Power Mode",
|
||||||
|
"5G Test Point",
|
||||||
|
"5G Channel",
|
||||||
|
"5G Bandwidth",
|
||||||
|
"5G RSSI",
|
||||||
|
"5G Direction",
|
||||||
|
"5G STA",
|
||||||
|
"6G Test Point",
|
||||||
|
"6G Channel",
|
||||||
|
"6G Bandwidth",
|
||||||
|
"6G RSSI",
|
||||||
|
"6G Direction",
|
||||||
|
"6G STA",
|
||||||
|
"2G Test Point",
|
||||||
|
"2G Channel",
|
||||||
|
"2G Bandwidth",
|
||||||
|
"2G RSSI",
|
||||||
|
"2G Direction",
|
||||||
|
"2G STA",
|
||||||
|
]
|
||||||
|
|
||||||
|
RUNTIME_DEFAULTS = {
|
||||||
|
"P2P": 80,
|
||||||
|
"COE": 115,
|
||||||
|
"P3P": 105,
|
||||||
|
}
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ParseResult:
|
||||||
|
tests: list[TestRecord]
|
||||||
|
warnings: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
class CsvValidationError(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def parse_target_csv(csv_path: str | Path) -> ParseResult:
|
||||||
|
path = Path(csv_path)
|
||||||
|
if not path.exists():
|
||||||
|
raise FileNotFoundError(f"CSV file not found: {path}")
|
||||||
|
|
||||||
|
with path.open("r", encoding="utf-8-sig", newline="") as handle:
|
||||||
|
reader = csv.DictReader(handle)
|
||||||
|
if not reader.fieldnames:
|
||||||
|
raise CsvValidationError("CSV is missing a header row.")
|
||||||
|
|
||||||
|
normalized_fieldnames = {name.strip().upper() for name in reader.fieldnames if name}
|
||||||
|
|
||||||
|
# Check for required columns
|
||||||
|
missing_columns = [
|
||||||
|
c
|
||||||
|
for c in REQUIRED_COLUMNS
|
||||||
|
if c.strip().upper() not in normalized_fieldnames
|
||||||
|
]
|
||||||
|
if missing_columns:
|
||||||
|
raise CsvValidationError(
|
||||||
|
f"CSV is missing required columns: {', '.join(missing_columns)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
tests: list[TestRecord] = []
|
||||||
|
warnings: list[str] = []
|
||||||
|
seen_test_ids: set[str] = set()
|
||||||
|
|
||||||
|
records_with_signature: list[tuple[TestRecord, tuple[str, ...] | None]] = []
|
||||||
|
|
||||||
|
for row_num, row in enumerate(reader, start=2):
|
||||||
|
test_id = (row.get("TC ID") or "").strip()
|
||||||
|
if not test_id:
|
||||||
|
warnings.append(f"Row {row_num}: missing TC ID, row skipped.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if test_id in seen_test_ids:
|
||||||
|
warnings.append(f"Row {row_num}: duplicate TC ID '{test_id}', row skipped.")
|
||||||
|
continue
|
||||||
|
seen_test_ids.add(test_id)
|
||||||
|
|
||||||
|
test_type = _infer_test_type(test_id)
|
||||||
|
rx_tx = _infer_rx_tx(test_id)
|
||||||
|
rotation = _empty_to_none(_row_get(row, "Rotation"))
|
||||||
|
has_coe_pair = _normalize_yes_no(_row_get(row, "COE Pair"))
|
||||||
|
config = _build_config(row)
|
||||||
|
signature = _victim_band_signature(row)
|
||||||
|
estimated_minutes = RUNTIME_DEFAULTS.get(test_type)
|
||||||
|
victim_band = _normalize_victim_band(_row_get(row, "Victim Band"))
|
||||||
|
|
||||||
|
# Default priority based on test type and COE pairing
|
||||||
|
if test_type == "P2P":
|
||||||
|
priority = 2 if has_coe_pair else 3
|
||||||
|
elif test_type == "COE":
|
||||||
|
priority = 3
|
||||||
|
else:
|
||||||
|
priority = 4
|
||||||
|
|
||||||
|
record = TestRecord(
|
||||||
|
test_id=test_id,
|
||||||
|
device=DEVICE_DUT,
|
||||||
|
test_type=test_type,
|
||||||
|
rotation=rotation,
|
||||||
|
rx_tx=rx_tx,
|
||||||
|
has_coe_pair=has_coe_pair,
|
||||||
|
coe_pairing=[],
|
||||||
|
priority=priority,
|
||||||
|
victim_band=victim_band,
|
||||||
|
config=config,
|
||||||
|
estimated_minutes=estimated_minutes,
|
||||||
|
status="pending",
|
||||||
|
excluded=False,
|
||||||
|
raw_payload=row,
|
||||||
|
)
|
||||||
|
records_with_signature.append((record, signature))
|
||||||
|
|
||||||
|
# Build COE pairing based on victim band signature and RX/TX+band suffix.
|
||||||
|
# Example key suffixes: RXAX, TXAX, RXBE, TXBE.
|
||||||
|
coe_by_signature_and_suffix: dict[tuple, list[str]] = {}
|
||||||
|
for record, signature in records_with_signature:
|
||||||
|
if record.test_type == "COE" and signature is not None:
|
||||||
|
suffix = _extract_pairing_suffix(record.test_id)
|
||||||
|
if suffix:
|
||||||
|
key = (signature, suffix)
|
||||||
|
coe_by_signature_and_suffix.setdefault(key, []).append(record.test_id)
|
||||||
|
|
||||||
|
for record, signature in records_with_signature:
|
||||||
|
pairs = record.coe_pairing
|
||||||
|
if record.test_type == "P2P" and signature is not None:
|
||||||
|
suffix = _extract_pairing_suffix(record.test_id)
|
||||||
|
key = (signature, suffix) if suffix else None
|
||||||
|
pairs = sorted(coe_by_signature_and_suffix.get(key, [])) if key else []
|
||||||
|
|
||||||
|
for device in (DEVICE_DUT, DEVICE_REF):
|
||||||
|
tests.append(
|
||||||
|
TestRecord(
|
||||||
|
test_id=record.test_id,
|
||||||
|
device=device,
|
||||||
|
test_type=record.test_type,
|
||||||
|
rotation=record.rotation,
|
||||||
|
rx_tx=record.rx_tx,
|
||||||
|
has_coe_pair=bool(pairs),
|
||||||
|
coe_pairing=pairs,
|
||||||
|
priority=record.priority,
|
||||||
|
victim_band=record.victim_band,
|
||||||
|
config=record.config,
|
||||||
|
estimated_minutes=record.estimated_minutes,
|
||||||
|
status=record.status,
|
||||||
|
excluded=record.excluded,
|
||||||
|
raw_payload=record.raw_payload,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return ParseResult(tests=tests, warnings=warnings)
|
||||||
|
|
||||||
|
|
||||||
|
def _infer_test_type(test_id: str) -> str:
|
||||||
|
token = test_id.upper()
|
||||||
|
if token.startswith("COE"):
|
||||||
|
return "COE"
|
||||||
|
if token.startswith("P3P"):
|
||||||
|
return "P3P"
|
||||||
|
return "P2P"
|
||||||
|
|
||||||
|
|
||||||
|
def _infer_rx_tx(test_id: str) -> str | None:
|
||||||
|
token = test_id.upper()
|
||||||
|
if "RX" in token:
|
||||||
|
return "RX"
|
||||||
|
if "TX" in token:
|
||||||
|
return "TX"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_pairing_suffix(test_id: str) -> str | None:
|
||||||
|
"""Extract the RX/TX+band suffix used for pairing.
|
||||||
|
|
||||||
|
Format: [COE|P2P][RX|TX][Band][Number]
|
||||||
|
Example: P2PRXAC004 -> 'RXAC', COETXAX012 -> 'TXAX'
|
||||||
|
"""
|
||||||
|
token = test_id.upper()
|
||||||
|
# Remove COE/P2P prefix
|
||||||
|
if token.startswith("COE"):
|
||||||
|
token = token[3:]
|
||||||
|
elif token.startswith("P2P") or token.startswith("P3P"):
|
||||||
|
token = token[3:]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Extract RX/TX + band (for example RXAX, TXBE).
|
||||||
|
if len(token) >= 4 and (token.startswith("RX") or token.startswith("TX")):
|
||||||
|
return token[:4]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _empty_to_none(value: str | None) -> str | None:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
cleaned = value.strip()
|
||||||
|
return cleaned if cleaned else None
|
||||||
|
|
||||||
|
|
||||||
|
def _victim_band_signature(row: dict[str, str]) -> tuple[str, ...] | None:
|
||||||
|
band = _normalize_victim_band(_row_get(row, "Victim Band"))
|
||||||
|
if band is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
prefix = f"{band} "
|
||||||
|
test_point = _normalize_value(_row_get(row, f"{prefix}Test Point"))
|
||||||
|
channel = _normalize_value(_row_get(row, f"{prefix}Channel"))
|
||||||
|
rssi = _normalize_value(_row_get(row, f"{prefix}RSSI"))
|
||||||
|
bandwidth = _normalize_value(_row_get(row, f"{prefix}Bandwidth"))
|
||||||
|
|
||||||
|
# Direction is intentionally ignored for COE pairing matching.
|
||||||
|
if not all([test_point, channel, rssi, bandwidth]):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return (band, test_point, channel, rssi, bandwidth)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_victim_band(value: str | None) -> str | None:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
normalized = value.strip().upper().replace("GHZ", "G")
|
||||||
|
if normalized.startswith("5"):
|
||||||
|
return "5G"
|
||||||
|
if normalized.startswith("6"):
|
||||||
|
return "6G"
|
||||||
|
if normalized.startswith("2"):
|
||||||
|
return "2G"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_value(value: str | None) -> str:
|
||||||
|
if value is None:
|
||||||
|
return ""
|
||||||
|
return " ".join(value.strip().upper().split())
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_yes_no(value: str | None) -> bool:
|
||||||
|
return _normalize_value(value) == "YES"
|
||||||
|
|
||||||
|
|
||||||
|
def _build_config(row: dict[str, str]) -> dict[str, dict[str, str | None]]:
|
||||||
|
return {
|
||||||
|
"5G": {
|
||||||
|
"test_point": _empty_to_none(_row_get(row, "5G Test Point")),
|
||||||
|
"channel": _empty_to_none(_row_get(row, "5G Channel")),
|
||||||
|
"bandwidth": _empty_to_none(_row_get(row, "5G Bandwidth")),
|
||||||
|
"rssi": _empty_to_none(_row_get(row, "5G RSSI")),
|
||||||
|
"direction": _empty_to_none(_row_get(row, "5G Direction")),
|
||||||
|
"sta": _empty_to_none(_row_get(row, "5G STA")),
|
||||||
|
},
|
||||||
|
"6G": {
|
||||||
|
"power_mode": _empty_to_none(_row_get(row, "6GHz Power Mode")),
|
||||||
|
"test_point": _empty_to_none(_row_get(row, "6G Test Point")),
|
||||||
|
"channel": _empty_to_none(_row_get(row, "6G Channel")),
|
||||||
|
"bandwidth": _empty_to_none(_row_get(row, "6G Bandwidth")),
|
||||||
|
"rssi": _empty_to_none(_row_get(row, "6G RSSI")),
|
||||||
|
"direction": _empty_to_none(_row_get(row, "6G Direction")),
|
||||||
|
"sta": _empty_to_none(_row_get(row, "6G STA")),
|
||||||
|
},
|
||||||
|
"2G": {
|
||||||
|
"test_point": _empty_to_none(_row_get(row, "2G Test Point")),
|
||||||
|
"channel": _empty_to_none(_row_get(row, "2G Channel")),
|
||||||
|
"bandwidth": _empty_to_none(_row_get(row, "2G Bandwidth")),
|
||||||
|
"rssi": _empty_to_none(_row_get(row, "2G RSSI")),
|
||||||
|
"direction": _empty_to_none(_row_get(row, "2G Direction")),
|
||||||
|
"sta": _empty_to_none(_row_get(row, "2G STA")),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _row_get(row: dict[str, str], key: str) -> str | None:
|
||||||
|
if key in row:
|
||||||
|
return row.get(key)
|
||||||
|
|
||||||
|
normalized_key = key.strip().upper()
|
||||||
|
for existing_key, value in row.items():
|
||||||
|
if existing_key and existing_key.strip().upper() == normalized_key:
|
||||||
|
return value
|
||||||
|
|
||||||
|
if normalized_key == "COE PAIR":
|
||||||
|
for alias in ("COE PAIRING", "COE_PAIRING"):
|
||||||
|
for existing_key, value in row.items():
|
||||||
|
if existing_key and existing_key.strip().upper() == alias:
|
||||||
|
return value
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
fastapi==0.116.0
|
||||||
|
uvicorn==0.35.0
|
||||||
|
pydantic==2.11.7
|
||||||
|
watchdog==6.0.0
|
||||||
|
httpx==0.28.1
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import threading
|
||||||
|
|
||||||
|
try:
|
||||||
|
import smbclient
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
smbclient = None
|
||||||
|
|
||||||
|
from db import DEVICE_DUT, DEVICE_REF, mark_tests_completed
|
||||||
|
|
||||||
|
_SMB_SESSIONS = set()
|
||||||
|
_SCAN_STATE_LOCK = threading.Lock()
|
||||||
|
_ACTIVE_SCAN_COUNT = 0
|
||||||
|
_RESULT_TEST_ID_PATTERN = re.compile(r"(?:COE|P2P|P3P)(?:RX|TX)?[A-Z]{2}\d{3}", re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_test_id_from_result_dir_name(dir_name):
|
||||||
|
if not dir_name:
|
||||||
|
return None
|
||||||
|
|
||||||
|
match = _RESULT_TEST_ID_PATTERN.search(str(dir_name).upper())
|
||||||
|
if match:
|
||||||
|
return match.group(0)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _build_match_tokens(parsed):
|
||||||
|
tokens = set()
|
||||||
|
|
||||||
|
def _add(value):
|
||||||
|
if value in (None, ""):
|
||||||
|
return
|
||||||
|
tokens.add(str(value).upper())
|
||||||
|
|
||||||
|
_add(parsed.get("interference"))
|
||||||
|
_add(parsed.get("device"))
|
||||||
|
_add(parsed.get("rotation"))
|
||||||
|
_add(parsed.get("test_point"))
|
||||||
|
_add(parsed.get("rssi"))
|
||||||
|
_add(parsed.get("station"))
|
||||||
|
_add(parsed.get("band"))
|
||||||
|
_add(parsed.get("channel"))
|
||||||
|
_add(parsed.get("bandwidth"))
|
||||||
|
_add(parsed.get("direction"))
|
||||||
|
_add(parsed.get("throttled"))
|
||||||
|
_add(parsed.get("test_id"))
|
||||||
|
|
||||||
|
for bw in parsed.get("extra_bandwidths") or []:
|
||||||
|
_add(bw)
|
||||||
|
|
||||||
|
# Add numeric alias for devices, e.g. CGW453 -> 453.
|
||||||
|
device = (parsed.get("device") or "").upper()
|
||||||
|
device_digits = re.sub(r"\D", "", device)
|
||||||
|
if device_digits:
|
||||||
|
tokens.add(device_digits)
|
||||||
|
|
||||||
|
# SP can appear as flag or specific token variant (e.g., SP40).
|
||||||
|
if parsed.get("sp"):
|
||||||
|
tokens.add("SP")
|
||||||
|
tokens.add(str(parsed.get("sp")).upper())
|
||||||
|
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
|
||||||
|
def _rule_matches_target(rule, parsed_tokens):
|
||||||
|
# Rule parts are AND-ed: CGW453_P3P_ROT2 => device AND interference AND rotation.
|
||||||
|
# Support _, -, or spaces as condition separators.
|
||||||
|
parts = [p for p in re.split(r"[_\-\s]+", rule) if p]
|
||||||
|
if not parts:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _part_matches(part):
|
||||||
|
if part in parsed_tokens:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Support tag variants (e.g., BW80 should match BW80M/BW80+80 in source names).
|
||||||
|
# Keep this conservative for very short parts to avoid overmatching.
|
||||||
|
if len(part) >= 3:
|
||||||
|
for token in parsed_tokens:
|
||||||
|
if token.startswith(part) or part in token:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
return all(_part_matches(part) for part in parts)
|
||||||
|
|
||||||
|
|
||||||
|
def _should_exclude_target(parsed, exclusions):
|
||||||
|
parsed_tokens = _build_match_tokens(parsed)
|
||||||
|
# Any matching rule excludes the testcase.
|
||||||
|
return any(_rule_matches_target(rule, parsed_tokens) for rule in exclusions)
|
||||||
|
|
||||||
|
|
||||||
|
def _scan_started():
|
||||||
|
global _ACTIVE_SCAN_COUNT
|
||||||
|
with _SCAN_STATE_LOCK:
|
||||||
|
_ACTIVE_SCAN_COUNT += 1
|
||||||
|
|
||||||
|
|
||||||
|
def _scan_finished():
|
||||||
|
global _ACTIVE_SCAN_COUNT
|
||||||
|
with _SCAN_STATE_LOCK:
|
||||||
|
_ACTIVE_SCAN_COUNT = max(0, _ACTIVE_SCAN_COUNT - 1)
|
||||||
|
|
||||||
|
|
||||||
|
def is_scan_in_progress():
|
||||||
|
with _SCAN_STATE_LOCK:
|
||||||
|
return _ACTIVE_SCAN_COUNT > 0
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_runtime_path(path_value):
|
||||||
|
if not path_value:
|
||||||
|
return path_value
|
||||||
|
|
||||||
|
raw_path = str(path_value).strip()
|
||||||
|
if not raw_path:
|
||||||
|
return raw_path
|
||||||
|
|
||||||
|
# If the path is already valid in the current runtime, keep it.
|
||||||
|
if os.path.exists(raw_path):
|
||||||
|
return raw_path
|
||||||
|
|
||||||
|
# UNC/network paths are handled separately via smbclient.
|
||||||
|
if raw_path.startswith("\\\\") or raw_path.startswith("//"):
|
||||||
|
return raw_path
|
||||||
|
|
||||||
|
# Map host paths (Windows or Linux) to the container mount point when running in a container.
|
||||||
|
if os.name != "nt":
|
||||||
|
mount_root = os.getenv("HOST_MOUNT_ROOT", "/host").strip() or "/host"
|
||||||
|
host_root = os.getenv("HOST_BROWSE_ROOT", "").strip()
|
||||||
|
|
||||||
|
raw_norm = raw_path.replace("\\", "/")
|
||||||
|
if host_root:
|
||||||
|
host_norm = host_root.replace("\\", "/").rstrip("/")
|
||||||
|
if raw_norm.lower() == host_norm.lower() or raw_norm.lower().startswith(host_norm.lower() + "/"):
|
||||||
|
relative = raw_norm[len(host_norm):].lstrip("/")
|
||||||
|
if relative:
|
||||||
|
return os.path.join(mount_root, *relative.split("/"))
|
||||||
|
return mount_root
|
||||||
|
|
||||||
|
return raw_path
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_input_path(path_value):
|
||||||
|
if not path_value:
|
||||||
|
return path_value
|
||||||
|
|
||||||
|
path = resolve_runtime_path(path_value)
|
||||||
|
path = str(path).strip()
|
||||||
|
|
||||||
|
# Accept //server/share style and normalize to UNC for smbclient.
|
||||||
|
if path.startswith("//"):
|
||||||
|
return "\\\\" + path.lstrip("/").replace("/", "\\")
|
||||||
|
|
||||||
|
# Accept /<ipv4>/<share>/... and normalize to UNC for Linux-hosted inputs.
|
||||||
|
if re.match(r"^/\d{1,3}(?:\.\d{1,3}){3}/[^/]+", path):
|
||||||
|
return "\\\\" + path.lstrip("/").replace("/", "\\")
|
||||||
|
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def _is_unc_path(path):
|
||||||
|
return isinstance(path, str) and path.startswith("\\\\")
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_unc_server(path):
|
||||||
|
if not _is_unc_path(path):
|
||||||
|
return None
|
||||||
|
rest = path[2:]
|
||||||
|
return rest.split("\\", 1)[0] if rest else None
|
||||||
|
|
||||||
|
|
||||||
|
def _register_smb_session_if_needed(path):
|
||||||
|
if not _is_unc_path(path):
|
||||||
|
return
|
||||||
|
|
||||||
|
if smbclient is None:
|
||||||
|
raise ModuleNotFoundError("smbclient is required to scan UNC result paths")
|
||||||
|
|
||||||
|
server = _extract_unc_server(path)
|
||||||
|
if not server or server in _SMB_SESSIONS:
|
||||||
|
return
|
||||||
|
|
||||||
|
username = os.getenv("SMB_USERNAME", "").strip()
|
||||||
|
password = os.getenv("SMB_PASSWORD", "")
|
||||||
|
domain = os.getenv("SMB_DOMAIN", "").strip()
|
||||||
|
|
||||||
|
if username and domain and "\\" not in username and "@" not in username:
|
||||||
|
username = f"{domain}\\{username}"
|
||||||
|
|
||||||
|
if username:
|
||||||
|
smbclient.register_session(server, username=username, password=password)
|
||||||
|
else:
|
||||||
|
smbclient.register_session(server)
|
||||||
|
|
||||||
|
_SMB_SESSIONS.add(server)
|
||||||
|
|
||||||
|
|
||||||
|
def _iter_dir_entries(path):
|
||||||
|
path = _normalize_input_path(path)
|
||||||
|
if _is_unc_path(path):
|
||||||
|
if smbclient is None:
|
||||||
|
raise ModuleNotFoundError("smbclient is required to scan UNC result paths")
|
||||||
|
_register_smb_session_if_needed(path)
|
||||||
|
return list(smbclient.scandir(path))
|
||||||
|
return list(os.scandir(path))
|
||||||
|
|
||||||
|
|
||||||
|
def scan_results(results_dir_dut, results_dir_ref):
|
||||||
|
results_dir_dut = _normalize_input_path(results_dir_dut)
|
||||||
|
results_dir_ref = _normalize_input_path(results_dir_ref)
|
||||||
|
|
||||||
|
if not results_dir_dut or not results_dir_ref:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
dut_entries = [
|
||||||
|
entry.name
|
||||||
|
for entry in _iter_dir_entries(results_dir_dut)
|
||||||
|
if entry.is_dir()
|
||||||
|
]
|
||||||
|
except OSError as exc:
|
||||||
|
print(f"[scanner] Cannot read results dir: {exc}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"[scanner] results: {len(dut_entries)} result dir(s) found in DUT results dir")
|
||||||
|
|
||||||
|
try:
|
||||||
|
ref_entries = [
|
||||||
|
entry.name
|
||||||
|
for entry in _iter_dir_entries(results_dir_ref)
|
||||||
|
if entry.is_dir()
|
||||||
|
]
|
||||||
|
except OSError as exc:
|
||||||
|
print(f"[scanner] Cannot read results dir: {exc}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"[scanner] results: {len(ref_entries)} result dir(s) found in reference results dir")
|
||||||
|
|
||||||
|
completed_batch = []
|
||||||
|
unmatched_entries = []
|
||||||
|
|
||||||
|
for entry_name in dut_entries:
|
||||||
|
test_id = _extract_test_id_from_result_dir_name(entry_name)
|
||||||
|
if test_id:
|
||||||
|
completed_batch.append((test_id, DEVICE_DUT))
|
||||||
|
else:
|
||||||
|
unmatched_entries.append(entry_name)
|
||||||
|
|
||||||
|
for entry_name in ref_entries:
|
||||||
|
test_id = _extract_test_id_from_result_dir_name(entry_name)
|
||||||
|
if test_id:
|
||||||
|
completed_batch.append((test_id, DEVICE_REF))
|
||||||
|
else:
|
||||||
|
unmatched_entries.append(entry_name)
|
||||||
|
|
||||||
|
if unmatched_entries:
|
||||||
|
print(f"[scanner] skipped {len(unmatched_entries)} result dir(s) with no recognizable test id")
|
||||||
|
|
||||||
|
mark_tests_completed(completed_batch)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,548 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
import os
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import graph
|
||||||
|
|
||||||
|
# Bundle priority tiers for scheduling order (lower number = higher priority)
|
||||||
|
BUNDLE_PRIORITY_FAILED = 0 # Failed tests requiring rerun
|
||||||
|
BUNDLE_PRIORITY_P2P_WITH_COE = 1 # P2P tests with COE pairs
|
||||||
|
BUNDLE_PRIORITY_P2P_ONLY = 2 # P2P tests without COE pairs (RX/TX bundled)
|
||||||
|
BUNDLE_PRIORITY_COE_ONLY = 3 # COE tests without P2P pairing (should be rare/unschedulable)
|
||||||
|
BUNDLE_PRIORITY_P3P = 4 # P3P tests
|
||||||
|
|
||||||
|
DUT = os.getenv("DUT", "CGW453").strip()
|
||||||
|
REF = os.getenv("REF", "CGW452").strip()
|
||||||
|
TestKey = tuple[str, str]
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class SchedulerTest:
|
||||||
|
test_id: str
|
||||||
|
device: str
|
||||||
|
test_type: str
|
||||||
|
rotation: str | None
|
||||||
|
rx_tx: str | None
|
||||||
|
has_coe_pair: bool
|
||||||
|
coe_pairing: list[str]
|
||||||
|
config: dict[str, dict[str, str | None]]
|
||||||
|
estimated_minutes: int
|
||||||
|
priority: int
|
||||||
|
raw_payload: dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ScheduleEntry:
|
||||||
|
test_id: str
|
||||||
|
device: str
|
||||||
|
scheduled_date: str
|
||||||
|
shift_index: int
|
||||||
|
sequence_in_shift: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class TestBundle:
|
||||||
|
"""A bundle of tests that must be run together in sequence."""
|
||||||
|
test_ids: list[TestKey]
|
||||||
|
priority_tier: int
|
||||||
|
total_minutes: int
|
||||||
|
|
||||||
|
|
||||||
|
# Global scheduler state. Graph is built once per loaded test set.
|
||||||
|
_TESTS_BY_ID: dict[TestKey, SchedulerTest] = {}
|
||||||
|
_ACTIVE_DUT: dict[TestKey, int] = {}
|
||||||
|
_ACTIVE_REF: dict[TestKey, int] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def reset_scheduler_state() -> None:
|
||||||
|
global _TESTS_BY_ID, _ACTIVE_DUT, _ACTIVE_REF
|
||||||
|
_TESTS_BY_ID = {}
|
||||||
|
_ACTIVE_DUT = {}
|
||||||
|
_ACTIVE_REF = {}
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_scheduler_state(tests: list[SchedulerTest]) -> None:
|
||||||
|
"""Initialize active scheduler state once for the current loaded schedulable dataset."""
|
||||||
|
global _TESTS_BY_ID, _ACTIVE_DUT, _ACTIVE_REF
|
||||||
|
if _TESTS_BY_ID:
|
||||||
|
return
|
||||||
|
|
||||||
|
_TESTS_BY_ID = {(t.test_id, t.device): t for t in tests}
|
||||||
|
_ACTIVE_DUT = {
|
||||||
|
test_id: _derive_priority(test, set(), set(), tests)
|
||||||
|
for test_id, test in _TESTS_BY_ID.items() if test.device == DUT
|
||||||
|
}
|
||||||
|
_ACTIVE_REF = {
|
||||||
|
test_id: _derive_priority(test, set(), set(), tests)
|
||||||
|
for test_id, test in _TESTS_BY_ID.items() if test.device == REF
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def set_user_priorities(top_priority_tests: set[str], lowest_priority_tests: set[str]) -> None:
|
||||||
|
"""Update active priority map in-place using user overrides."""
|
||||||
|
if not _TESTS_BY_ID:
|
||||||
|
return
|
||||||
|
|
||||||
|
all_tests = list(_TESTS_BY_ID.values())
|
||||||
|
for key in list(_ACTIVE_DUT.keys()):
|
||||||
|
test = _TESTS_BY_ID.get(key)
|
||||||
|
if test is None:
|
||||||
|
continue
|
||||||
|
_ACTIVE_DUT[key] = _derive_priority(test, top_priority_tests, lowest_priority_tests, all_tests)
|
||||||
|
|
||||||
|
for key in list(_ACTIVE_REF.keys()):
|
||||||
|
test = _TESTS_BY_ID.get(key)
|
||||||
|
if test is None:
|
||||||
|
continue
|
||||||
|
_ACTIVE_REF[key] = _derive_priority(test, top_priority_tests, lowest_priority_tests, all_tests)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_from_active(completed_test_ids: set[str]) -> None:
|
||||||
|
"""Remove completed/invalid tests from active list without rebuilding graph."""
|
||||||
|
for test_id in completed_test_ids:
|
||||||
|
_ACTIVE_DUT.pop((test_id, DUT), None)
|
||||||
|
_ACTIVE_REF.pop((test_id, REF), None)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def compile_schedule(
|
||||||
|
tests: list[SchedulerTest],
|
||||||
|
start_date: str | None,
|
||||||
|
holiday_dates: set[str],
|
||||||
|
top_priority_tests: set[str],
|
||||||
|
lowest_priority_tests: set[str],
|
||||||
|
daytime_testing_today: bool,
|
||||||
|
) -> tuple[list[ScheduleEntry], str | None]:
|
||||||
|
"""Compile an optimized schedule for the given tests.
|
||||||
|
|
||||||
|
Uses the greedy algorithm respecting shift sequences per design:
|
||||||
|
- Mon-Thu: shift 3 (5pm-1am) then shift 1 next day (1am-10am)
|
||||||
|
- Friday: shift 3 through Monday shift 1 (4-day window)
|
||||||
|
Returns a list of ScheduleEntry objects and the completion date.
|
||||||
|
"""
|
||||||
|
if not tests:
|
||||||
|
return [], None
|
||||||
|
if not graph.is_graph_built():
|
||||||
|
raise RuntimeError("Graph not initialized. Load tests first to build DUT compatibility graph.")
|
||||||
|
|
||||||
|
initialize_scheduler_state(tests)
|
||||||
|
set_user_priorities(top_priority_tests, lowest_priority_tests)
|
||||||
|
|
||||||
|
# Use a local working copy for this compile run. Global active remains until result processing removes IDs.
|
||||||
|
dut_active_priority: dict[TestKey, int] = dict(_ACTIVE_DUT)
|
||||||
|
ref_active_priority: dict[TestKey, int] = dict(_ACTIVE_REF)
|
||||||
|
entries: list[ScheduleEntry] = []
|
||||||
|
|
||||||
|
current_date = _parse_date(start_date)
|
||||||
|
last_date: str | None = None
|
||||||
|
|
||||||
|
while dut_active_priority or ref_active_priority:
|
||||||
|
# Get the shift sequence for current date (respects Mon/Fri/weekend rules)
|
||||||
|
shift_sequence = _get_shift_sequence(current_date, holiday_dates)
|
||||||
|
|
||||||
|
if not shift_sequence:
|
||||||
|
break # No valid shift sequence
|
||||||
|
|
||||||
|
dut_active_test_ids: set[TestKey] = set(dut_active_priority.keys())
|
||||||
|
ref_active_test_ids: set[TestKey] = set(ref_active_priority.keys())
|
||||||
|
bundles = _create_bundles(dut_active_test_ids, ref_active_test_ids, _TESTS_BY_ID)
|
||||||
|
shift_capacities = {
|
||||||
|
(date_obj, shift_idx): _shift_capacity_for_date(
|
||||||
|
current_date=date_obj,
|
||||||
|
holiday_dates=holiday_dates,
|
||||||
|
start_date_override=start_date,
|
||||||
|
daytime_testing_today=daytime_testing_today,
|
||||||
|
).get(shift_idx, 0)
|
||||||
|
for date_obj, shift_idx in shift_sequence
|
||||||
|
}
|
||||||
|
|
||||||
|
placed_entries, placed_test_ids, placed_last_date = _fit_bundles_to_shifts(
|
||||||
|
bundles=bundles,
|
||||||
|
shift_sequence=shift_sequence,
|
||||||
|
tests=_TESTS_BY_ID,
|
||||||
|
graph_by_test_id=graph.get_graph(),
|
||||||
|
shift_capacities=shift_capacities,
|
||||||
|
)
|
||||||
|
entries.extend(placed_entries)
|
||||||
|
for key in placed_test_ids:
|
||||||
|
dut_active_priority.pop(key, None)
|
||||||
|
ref_active_priority.pop(key, None)
|
||||||
|
if placed_last_date is not None:
|
||||||
|
last_date = placed_last_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 3 (5pm), shift 1 is next day (1am)
|
||||||
|
if shift_sequence:
|
||||||
|
last_sequence_date = shift_sequence[-1][0]
|
||||||
|
last_shift_index = shift_sequence[-1][1]
|
||||||
|
|
||||||
|
if last_shift_index == 1:
|
||||||
|
# Last shift was 1 (1am-9am); next shift 3 is same day (5pm)
|
||||||
|
current_date = last_sequence_date
|
||||||
|
elif last_shift_index == 2:
|
||||||
|
# Last shift was 2 (9am-5pm); next shift 3 is same day (5pm)
|
||||||
|
current_date = last_sequence_date
|
||||||
|
else: # last_shift_index == 3
|
||||||
|
# Last shift was 3 (5pm-1am); next shift 1 is next day (1am)
|
||||||
|
current_date = last_sequence_date + timedelta(days=1)
|
||||||
|
else:
|
||||||
|
current_date = current_date + timedelta(days=1)
|
||||||
|
|
||||||
|
return entries, last_date
|
||||||
|
|
||||||
|
|
||||||
|
def format_schedule_for_frontend(
|
||||||
|
entries: list[ScheduleEntry],
|
||||||
|
) -> dict[str, dict[int, list[SchedulerTest]]]:
|
||||||
|
"""Convert flat ScheduleEntry list to nested format for frontend.
|
||||||
|
|
||||||
|
Returns: {date_string: {shift_index: [SchedulerTest, ...]}, ...}
|
||||||
|
"""
|
||||||
|
schedule: dict[str, dict[int, list[SchedulerTest]]] = {}
|
||||||
|
|
||||||
|
for entry in entries:
|
||||||
|
if entry.scheduled_date not in schedule:
|
||||||
|
schedule[entry.scheduled_date] = {1: [], 2: [], 3: []}
|
||||||
|
|
||||||
|
test = _TESTS_BY_ID.get((entry.test_id, entry.device))
|
||||||
|
if test:
|
||||||
|
schedule[entry.scheduled_date][entry.shift_index].append(test)
|
||||||
|
|
||||||
|
return schedule
|
||||||
|
|
||||||
|
|
||||||
|
def _derive_priority(
|
||||||
|
test: SchedulerTest,
|
||||||
|
top_priority_tests: set[str],
|
||||||
|
lowest_priority_tests: set[str],
|
||||||
|
all_tests: list[SchedulerTest],
|
||||||
|
) -> int:
|
||||||
|
if test.test_id in top_priority_tests:
|
||||||
|
return 1
|
||||||
|
if test.test_id in lowest_priority_tests:
|
||||||
|
return 5
|
||||||
|
return test.priority
|
||||||
|
|
||||||
|
|
||||||
|
def _fit_bundles_to_shifts(
|
||||||
|
bundles: list[TestBundle],
|
||||||
|
shift_sequence: list[tuple[date, int]],
|
||||||
|
tests: dict[TestKey, SchedulerTest],
|
||||||
|
graph_by_test_id: dict[str, set[str]],
|
||||||
|
shift_capacities: dict[tuple[date, int], int],
|
||||||
|
) -> tuple[list[ScheduleEntry], set[TestKey], str | None]:
|
||||||
|
"""Fit bundles into a shift sequence window (e.g., one day or one weekend).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- List of ScheduleEntry for placed tests
|
||||||
|
- Set of test_ids that were placed
|
||||||
|
- Last scheduled date
|
||||||
|
|
||||||
|
Bundles are placed in sequence order and may span multiple shifts inside the same window.
|
||||||
|
"""
|
||||||
|
entries: list[ScheduleEntry] = []
|
||||||
|
placed_test_ids: set[TestKey] = set()
|
||||||
|
last_date: str | None = None
|
||||||
|
|
||||||
|
# Track state per shift
|
||||||
|
shift_state: dict[tuple[date, int], dict] = {}
|
||||||
|
for date_shift in shift_sequence:
|
||||||
|
shift_state[date_shift] = {
|
||||||
|
'remaining_minutes': shift_capacities.get(date_shift, 0),
|
||||||
|
'sequence_counter': 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
shift_positions = {date_shift: idx for idx, date_shift in enumerate(shift_sequence)}
|
||||||
|
current_shift_pos = 0
|
||||||
|
window_placed_test_ids: set[str] = set()
|
||||||
|
|
||||||
|
for bundle in bundles:
|
||||||
|
# Skip bundles that are already fully placed.
|
||||||
|
if any(test_id in placed_test_ids for test_id in bundle.test_ids):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not bundle.test_ids:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Keep all tests in the same window pairwise-compatible.
|
||||||
|
if window_placed_test_ids:
|
||||||
|
is_compatible_with_window = True
|
||||||
|
bundle_test_ids_only = {test_key[0] for test_key in bundle.test_ids}
|
||||||
|
for placed_test_id in window_placed_test_ids:
|
||||||
|
placed_neighbors = graph_by_test_id.get(placed_test_id, set())
|
||||||
|
if not all(current_test_id in placed_neighbors for current_test_id in bundle_test_ids_only):
|
||||||
|
is_compatible_with_window = False
|
||||||
|
break
|
||||||
|
if not is_compatible_with_window:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Ensure the whole bundle can still fit somewhere in the remaining window.
|
||||||
|
remaining_window = sum(
|
||||||
|
shift_state[date_shift]['remaining_minutes'] for date_shift in shift_sequence[current_shift_pos:]
|
||||||
|
)
|
||||||
|
if remaining_window < bundle.total_minutes:
|
||||||
|
continue
|
||||||
|
|
||||||
|
bundle_start_shift_pos = current_shift_pos
|
||||||
|
placed_bundle_tests: list[tuple[TestKey, tuple[date, int]]] = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
for key in bundle.test_ids:
|
||||||
|
test = tests[key]
|
||||||
|
prev_remaining = 0
|
||||||
|
while current_shift_pos < len(shift_sequence):
|
||||||
|
date_shift = shift_sequence[current_shift_pos]
|
||||||
|
state = shift_state[date_shift]
|
||||||
|
state["remaining_minutes"] += prev_remaining # Add back any leftover from previous shift
|
||||||
|
if state['remaining_minutes'] >= test.estimated_minutes:
|
||||||
|
print(f"remaining minutes for {date_shift}: {state['remaining_minutes']} - placing {key} ({test.estimated_minutes}m)")
|
||||||
|
entries.append(
|
||||||
|
ScheduleEntry(
|
||||||
|
test_id=test.test_id,
|
||||||
|
device=test.device,
|
||||||
|
scheduled_date=date_shift[0].isoformat(),
|
||||||
|
shift_index=date_shift[1],
|
||||||
|
sequence_in_shift=state['sequence_counter'],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
state['sequence_counter'] += 1
|
||||||
|
state['remaining_minutes'] -= test.estimated_minutes
|
||||||
|
placed_test_ids.add(key)
|
||||||
|
placed_bundle_tests.append((key, date_shift))
|
||||||
|
last_date = date_shift[0].isoformat()
|
||||||
|
break
|
||||||
|
|
||||||
|
# Move to the next shift in the sequence and keep the bundle contiguous.
|
||||||
|
current_shift_pos += 1
|
||||||
|
prev_remaining = state['remaining_minutes']
|
||||||
|
if current_shift_pos >= len(shift_sequence):
|
||||||
|
failed = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if failed:
|
||||||
|
break
|
||||||
|
|
||||||
|
if failed:
|
||||||
|
# Roll back any partially placed tests from this bundle.
|
||||||
|
for key, date_shift in reversed(placed_bundle_tests):
|
||||||
|
state = shift_state[date_shift]
|
||||||
|
state['remaining_minutes'] += tests[key].estimated_minutes
|
||||||
|
state['sequence_counter'] -= 1
|
||||||
|
entries.pop()
|
||||||
|
placed_test_ids.discard(key)
|
||||||
|
last_date = None
|
||||||
|
current_shift_pos = bundle_start_shift_pos
|
||||||
|
continue
|
||||||
|
|
||||||
|
if placed_bundle_tests:
|
||||||
|
window_placed_test_ids.update(test_key[0] for test_key in bundle.test_ids)
|
||||||
|
|
||||||
|
return entries, placed_test_ids, last_date
|
||||||
|
|
||||||
|
def _create_bundles(
|
||||||
|
dut_active_test_ids: set[TestKey],
|
||||||
|
ref_active_test_ids: set[TestKey],
|
||||||
|
tests: dict[TestKey, SchedulerTest],
|
||||||
|
) -> list[TestBundle]:
|
||||||
|
"""Create bundles of tests that must run together, sorted by priority tier and efficiency.
|
||||||
|
|
||||||
|
Bundling rules:
|
||||||
|
- P2P with COE pairs: [P2P, COE1, COE2, ...]
|
||||||
|
- P2P without COE: [P2P_RX, P2P_TX] if both active
|
||||||
|
- COE without a linked active P2P: [COE] (standalone bundle)
|
||||||
|
- P3P: individual test (not bundled)
|
||||||
|
|
||||||
|
Returned list is sorted by (priority_tier, total_minutes) to place small/high-priority bundles first.
|
||||||
|
"""
|
||||||
|
bundles: list[TestBundle] = []
|
||||||
|
processed: set[TestKey] = set()
|
||||||
|
|
||||||
|
dut_by_id: dict[str, TestKey] = {test_id: key for test_id, _device in dut_active_test_ids for key in [(test_id, DUT)] if key in dut_active_test_ids}
|
||||||
|
ref_by_id: dict[str, TestKey] = {test_id: key for test_id, _device in ref_active_test_ids for key in [(test_id, REF)] if key in ref_active_test_ids}
|
||||||
|
all_ids = sorted(set(dut_by_id.keys()) | set(ref_by_id.keys()))
|
||||||
|
|
||||||
|
def _active_key(test_id: str, device: str) -> TestKey | None:
|
||||||
|
key = (test_id, device)
|
||||||
|
if key in dut_active_test_ids or key in ref_active_test_ids:
|
||||||
|
return key
|
||||||
|
return None
|
||||||
|
|
||||||
|
for test_id in all_ids:
|
||||||
|
seed_keys = [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 seed_keys:
|
||||||
|
continue
|
||||||
|
|
||||||
|
representative_key = seed_keys[0]
|
||||||
|
test = tests[representative_key]
|
||||||
|
if test.test_type == "P2P":
|
||||||
|
if test.has_coe_pair:
|
||||||
|
bundle_test_ids: list[TestKey] = []
|
||||||
|
for device in (DUT, REF):
|
||||||
|
base_key = _active_key(test_id, device)
|
||||||
|
if base_key is not None and base_key not in processed:
|
||||||
|
bundle_test_ids.append(base_key)
|
||||||
|
for coe_id in sorted(test.coe_pairing):
|
||||||
|
coe_key = _active_key(coe_id, device)
|
||||||
|
if coe_key is not None and coe_key not in processed:
|
||||||
|
bundle_test_ids.append(coe_key)
|
||||||
|
|
||||||
|
if not bundle_test_ids:
|
||||||
|
continue
|
||||||
|
processed.update(bundle_test_ids)
|
||||||
|
priority_tier = BUNDLE_PRIORITY_P2P_WITH_COE
|
||||||
|
bundles.append(TestBundle(
|
||||||
|
test_ids=bundle_test_ids,
|
||||||
|
priority_tier=priority_tier,
|
||||||
|
total_minutes=sum(tests[key].estimated_minutes for key in bundle_test_ids)
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
bundle_test_ids: list[TestKey] = []
|
||||||
|
for device in (DUT, REF):
|
||||||
|
base_key = _active_key(test_id, device)
|
||||||
|
if base_key is None or base_key in processed:
|
||||||
|
continue
|
||||||
|
bundle_test_ids.append(base_key)
|
||||||
|
pair_id = _rx_tx_pair_id(test_id, set(k[0] for k in (dut_active_test_ids if device == DUT else ref_active_test_ids)))
|
||||||
|
if pair_id:
|
||||||
|
pair_key = _active_key(pair_id, device)
|
||||||
|
if pair_key is not None and pair_key not in processed and pair_key not in bundle_test_ids:
|
||||||
|
bundle_test_ids.append(pair_key)
|
||||||
|
|
||||||
|
if not bundle_test_ids:
|
||||||
|
continue
|
||||||
|
processed.update(bundle_test_ids)
|
||||||
|
priority_tier = BUNDLE_PRIORITY_P2P_ONLY
|
||||||
|
bundles.append(TestBundle(
|
||||||
|
test_ids=bundle_test_ids,
|
||||||
|
priority_tier=priority_tier,
|
||||||
|
total_minutes=sum(tests[key].estimated_minutes for key in bundle_test_ids)
|
||||||
|
))
|
||||||
|
elif test.test_type == "P3P":
|
||||||
|
bundle_test_ids = [key for key in (_active_key(test_id, DUT), _active_key(test_id, REF)) if key is not None and key not in processed]
|
||||||
|
if not bundle_test_ids:
|
||||||
|
continue
|
||||||
|
priority_tier = BUNDLE_PRIORITY_P3P
|
||||||
|
bundles.append(TestBundle(
|
||||||
|
test_ids=bundle_test_ids,
|
||||||
|
priority_tier=priority_tier,
|
||||||
|
total_minutes=sum(tests[key].estimated_minutes for key in bundle_test_ids)
|
||||||
|
))
|
||||||
|
processed.update(bundle_test_ids)
|
||||||
|
|
||||||
|
# Second pass to catch any active tests left uncovered by first-pass grouping.
|
||||||
|
all_active_keys = sorted(dut_active_test_ids | ref_active_test_ids)
|
||||||
|
for key in all_active_keys:
|
||||||
|
if key in processed:
|
||||||
|
continue
|
||||||
|
test = tests[key]
|
||||||
|
priority_tier = BUNDLE_PRIORITY_COE_ONLY
|
||||||
|
bundle_test_ids = [key]
|
||||||
|
bundles.append(TestBundle(
|
||||||
|
test_ids=bundle_test_ids,
|
||||||
|
priority_tier=priority_tier,
|
||||||
|
total_minutes=sum(tests[item].estimated_minutes for item in bundle_test_ids)
|
||||||
|
))
|
||||||
|
processed.update(bundle_test_ids)
|
||||||
|
|
||||||
|
# Sort by priority tier (lower first), then by total minutes (smaller first for efficiency)
|
||||||
|
bundles.sort(key=lambda b: (b.priority_tier, b.total_minutes))
|
||||||
|
return bundles
|
||||||
|
|
||||||
|
|
||||||
|
def _rx_tx_pair_id(test_id: str, active: set[str]) -> str | None:
|
||||||
|
if "RX" in test_id:
|
||||||
|
candidate = test_id.replace("RX", "TX", 1)
|
||||||
|
return candidate if candidate in active else None
|
||||||
|
if "TX" in test_id:
|
||||||
|
candidate = test_id.replace("TX", "RX", 1)
|
||||||
|
return candidate if candidate in active else None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _shift_capacity_for_date(
|
||||||
|
current_date: date,
|
||||||
|
holiday_dates: set[str],
|
||||||
|
start_date_override: str | None,
|
||||||
|
daytime_testing_today: bool,
|
||||||
|
) -> dict[int, int]:
|
||||||
|
iso = current_date.isoformat()
|
||||||
|
if iso in holiday_dates:
|
||||||
|
return {1: 480, 2: 480, 3: 480}
|
||||||
|
|
||||||
|
is_weekend = current_date.weekday() >= 5
|
||||||
|
if is_weekend:
|
||||||
|
return {1: 480, 2: 480, 3: 480}
|
||||||
|
|
||||||
|
if start_date_override and iso == _parse_date(start_date_override).isoformat() and daytime_testing_today:
|
||||||
|
return {1: 480, 2: 480, 3: 480}
|
||||||
|
|
||||||
|
return {1: 600, 2: 0, 3: 420}
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_date(value: str | None) -> date:
|
||||||
|
if not value:
|
||||||
|
return date.today()
|
||||||
|
return datetime.strptime(value, "%Y-%m-%d").date()
|
||||||
|
|
||||||
|
|
||||||
|
def _get_shift_sequence(start_date: date, holiday_dates: set[str]) -> list[tuple[date, int]]:
|
||||||
|
"""Generate the sequence of (date, shift_index) tuples for a scheduling window.
|
||||||
|
|
||||||
|
Rules per design:
|
||||||
|
- Monday-Thursday: [3, 1] (shift 3 today, shift 1 next day) = 16 hours
|
||||||
|
- Friday: [3, 1, 2, 3, 1, 2, 3, 1] (Fri-Mon) = 24 hours continuous
|
||||||
|
- Saturday/Sunday/Holiday weekday: all 3 shifts
|
||||||
|
|
||||||
|
Always starts on shift 3 for weekdays.
|
||||||
|
Returns ordered list of (date, shift_index) pairs.
|
||||||
|
"""
|
||||||
|
iso = start_date.isoformat()
|
||||||
|
is_holiday = iso in holiday_dates
|
||||||
|
weekday = start_date.weekday() # 0=Mon, 4=Fri, 5=Sat, 6=Sun
|
||||||
|
|
||||||
|
shifts: list[tuple[date, int]] = []
|
||||||
|
|
||||||
|
# If holiday on a weekday, treat as weekend (all 3 shifts)
|
||||||
|
if is_holiday and weekday < 5:
|
||||||
|
shifts.append((start_date, 1))
|
||||||
|
shifts.append((start_date, 2))
|
||||||
|
shifts.append((start_date, 3))
|
||||||
|
return shifts
|
||||||
|
|
||||||
|
# Standard weekend day (Sat/Sun)
|
||||||
|
if weekday >= 5:
|
||||||
|
shifts.append((start_date, 1))
|
||||||
|
shifts.append((start_date, 2))
|
||||||
|
shifts.append((start_date, 3))
|
||||||
|
return shifts
|
||||||
|
|
||||||
|
# Friday: 4-day window (Fri-Mon)
|
||||||
|
if weekday == 4:
|
||||||
|
shifts.append((start_date, 3)) # Fri shift 3
|
||||||
|
|
||||||
|
sat = start_date + timedelta(days=1)
|
||||||
|
shifts.append((sat, 1)) # Sat shift 1
|
||||||
|
shifts.append((sat, 2)) # Sat shift 2
|
||||||
|
shifts.append((sat, 3)) # Sat shift 3
|
||||||
|
|
||||||
|
sun = sat + timedelta(days=1)
|
||||||
|
shifts.append((sun, 1)) # Sun shift 1
|
||||||
|
shifts.append((sun, 2)) # Sun shift 2
|
||||||
|
shifts.append((sun, 3)) # Sun shift 3
|
||||||
|
|
||||||
|
mon = sun + timedelta(days=1)
|
||||||
|
shifts.append((mon, 1)) # Mon shift 1
|
||||||
|
|
||||||
|
return shifts
|
||||||
|
|
||||||
|
# Monday-Thursday: 2-shift window
|
||||||
|
shifts.append((start_date, 3)) # Today shift 3
|
||||||
|
next_day = start_date + timedelta(days=1)
|
||||||
|
shifts.append((next_day, 1)) # Tomorrow shift 1
|
||||||
|
|
||||||
|
return shifts
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# React + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
||||||
|
|
||||||
|
## React Compiler
|
||||||
|
|
||||||
|
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{js,jsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
reactHooks.configs.flat.recommended,
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
globals: globals.browser,
|
||||||
|
parserOptions: { ecmaFeatures: { jsx: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>my-react-app</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+2454
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "my-react-app",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^19.2.6",
|
||||||
|
"react-dom": "^19.2.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^10.0.1",
|
||||||
|
"@types/react": "^19.2.14",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
|
"eslint": "^10.3.0",
|
||||||
|
"eslint-plugin-react-hooks": "^7.1.1",
|
||||||
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
|
"globals": "^17.6.0",
|
||||||
|
"vite": "^8.0.12"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.3 KiB |
@@ -0,0 +1,24 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
||||||
|
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
||||||
|
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="discord-icon" viewBox="0 0 20 19">
|
||||||
|
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="github-icon" viewBox="0 0 19 19">
|
||||||
|
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="social-icon" viewBox="0 0 20 20">
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="x-icon" viewBox="0 0 19 19">
|
||||||
|
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
||||||
|
</symbol>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.9 KiB |
@@ -0,0 +1,184 @@
|
|||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import reactLogo from './assets/react.svg'
|
||||||
|
import viteLogo from './assets/vite.svg'
|
||||||
|
import heroImg from './assets/hero.png'
|
||||||
|
import './App.css'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [count, setCount] = useState(0)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section id="center">
|
||||||
|
<div className="hero">
|
||||||
|
<img src={heroImg} className="base" width="170" height="179" alt="" />
|
||||||
|
<img src={reactLogo} className="framework" alt="React logo" />
|
||||||
|
<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
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.5 KiB |
@@ -0,0 +1,111 @@
|
|||||||
|
:root {
|
||||||
|
--text: #6b6375;
|
||||||
|
--text-h: #08060d;
|
||||||
|
--bg: #fff;
|
||||||
|
--border: #e5e4e7;
|
||||||
|
--code-bg: #f4f3ec;
|
||||||
|
--accent: #aa3bff;
|
||||||
|
--accent-bg: rgba(170, 59, 255, 0.1);
|
||||||
|
--accent-border: rgba(170, 59, 255, 0.5);
|
||||||
|
--social-bg: rgba(244, 243, 236, 0.5);
|
||||||
|
--shadow:
|
||||||
|
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
|
||||||
|
|
||||||
|
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
--mono: ui-monospace, Consolas, monospace;
|
||||||
|
|
||||||
|
font: 18px/145% var(--sans);
|
||||||
|
letter-spacing: 0.18px;
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: var(--text);
|
||||||
|
background: var(--bg);
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--text: #9ca3af;
|
||||||
|
--text-h: #f3f4f6;
|
||||||
|
--bg: #16171d;
|
||||||
|
--border: #2e303a;
|
||||||
|
--code-bg: #1f2028;
|
||||||
|
--accent: #c084fc;
|
||||||
|
--accent-bg: rgba(192, 132, 252, 0.15);
|
||||||
|
--accent-border: rgba(192, 132, 252, 0.5);
|
||||||
|
--social-bg: rgba(47, 48, 58, 0.5);
|
||||||
|
--shadow:
|
||||||
|
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#social .button-icon {
|
||||||
|
filter: invert(1) brightness(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
width: 1126px;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: center;
|
||||||
|
border-inline: 1px solid var(--border);
|
||||||
|
min-height: 100svh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2 {
|
||||||
|
font-family: var(--heading);
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-h);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 56px;
|
||||||
|
letter-spacing: -1.68px;
|
||||||
|
margin: 32px 0;
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
font-size: 36px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 118%;
|
||||||
|
letter-spacing: -0.24px;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
code,
|
||||||
|
.counter {
|
||||||
|
font-family: var(--mono);
|
||||||
|
display: inline-flex;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--text-h);
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 135%;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: var(--code-bg);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import './index.css'
|
||||||
|
import App from './App.jsx'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user