2 Commits

Author SHA1 Message Date
root
f2ce611330 fix: align CV rules and refresh final documentation
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-02 22:51:41 +02:00
root
d701339930 release: finalize clean submission repository
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-02 22:39:46 +02:00
11 changed files with 592 additions and 350 deletions

286
README.md
View File

@@ -1,110 +1,98 @@
# OZON Sorter Digital Twin
# OWL PRIME — Ozon Tech Track 3
Complete Track 3 Ozon Tech solution in one repository: a web digital twin of the sorting line, plus a real RealSense/OpenCV CV prototype.
## О проекте
OWL PRIME — инженерный контур предварительной сортировки товаров: веб-цифровой двойник конвейерной линии, классификация B/C/D, физическая маршрутизация в симуляции, рабочий CV-прототип на RealSense D415 и экспериментальный физический стенд.
Это не сертифицированный промышленный ПАК, а воспроизводимое решение хакатона: цифровой twin на проде, CV-прототип в репозитории, CAD/ассеты и документация для проверки судьями.
## Состав решения
1. **Web digital twin** — React/Three.js симуляция конвейера, измерения, classifier, diverters.
2. **CV-прототип**`cv/`: RealSense D415 + OpenCV → габариты / круг → B/C/D.
3. **CAD и runtime 3D** — авторский FreeCAD и GLB/STL для деплоя.
4. **Физический стенд** — лента, рама, камера над полотном, электроника/SCADA.
5. **Документация**`/documentation`, `docs/ENGINEERING.md`.
6. **Презентация**`presentation/Owl_Prime_Ozon_Tech_Track_3_FINAL.pdf`.
## Демо
**Production:** https://arhipovdan.ru
## 1. Overview
This repository delivers a continuous web simulation of an Ozon conveyor sorter: CAD conveyor, product models, camera/measurement stage, B/C/D classification, diverters, and Rapier physics. The public demo runs on https://arhipovdan.ru with routes `/` (simulation) and `/documentation` (engineering status).
Separately, `cv/` contains a **working hardware prototype** that reads Intel RealSense D415 depth, measures parcels with OpenCV, classifies B/C/D, and can publish results over MQTT. It is **not** wired into the live website.
Both paths share the same Track 3 classification domain (exclusive 10×10×10 … 450×320×320 mm, roundness K > 0.8). The web twin uses a digital sensor simulation; the CV folder uses real depth frames.
`main` is the canonical complete solution. Developers do not need other branches to run the web app or inspect/run the CV prototype.
Large submission artifacts (presentation, video, optional CAD/model mirrors) belong in team cloud storage; runtime assets required by deploy stay in Git.
## 2. Submission components
| Component | Location | Notes |
|---|---|---|
| Web digital twin | `src/`, `public/` | Production-integrated |
| Real CV prototype | `cv/` | WORKING_PROTOTYPE, not live-integrated |
| Author CAD | `3d_models/conveer.FCStd` | FreeCAD source |
| Official materials | `input_info/`, `official_sources/` | PDFs/ZIPs cited by docs/code |
| Production domain | https://arhipovdan.ru | Docker + nginx |
| Presentation / video | cloud (owner) | Links TBD — see §17 |
## 3. Production demo
- **URL:** https://arhipovdan.ru
- **`/`** — continuous digital-twin simulation
- **`/documentation`** — canonical engineering status
- Unknown routes redirect to `/`
Device behavior on current baseline:
- **Desktop:** interactive WebGL 3D
- **Mobile:** lightweight **2D** fallback (not full WebGL 3D)
Build identity: `/version.json`.
## 4. Web capabilities
- Three-module CAD conveyor (clean → camera → sorter)
- Camera / measurement simulation and Track 3 classifier
- B / C / D routing with CAD diverters (45° / +45°)
- Rapier product physics (contact sorting **not** fully validated)
- Continuous playback HUD + documentation page
## 5. Real CV prototype
| Field | Value |
| Route | Назначение |
|---|---|
| Path | `cv/` |
| Origin | `drho1y-mvp_1` / `vision_classifier/` |
| Technology | RealSense D415 + OpenCV (depth segmentation + metrics) |
| Status | WORKING_PROTOTYPE |
| Integration | **Not** connected to production web runtime |
| `/` | Непрерывная симуляция |
| `/documentation` | Инженерный статус |
| `*` | Редирект на `/` |
See **[cv/README.md](cv/README.md)** for install, demo, live camera, and MQTT.
- **Desktop:** интерактивный WebGL 3D.
- **Mobile:** облегчённый 2D fallback (как в текущем baseline).
## 6. Architecture
Идентичность сборки: `/version.json`.
## Основные возможности
- непрерывный CAD-конвейер и STL-товары;
- цифровой этап камеры / измерения;
- rule-based classifier B/C/D;
- CAD-дивертеры LEFT/RIGHT (45° / +45°);
- физика Rapier (лента 1 м/с);
- CV-прототип depth→B/C/D (не в live web);
- инженерная документация в приложении.
## Правила классификации
Официальные границы (`official_sources/doc-1783095831.pdf`), реализованы в web (`src/domain/classifier.ts`) и CV (`cv/classify.py`):
1. Габариты строго **> 10×10×10 мм** и **< 450×320×320 мм**, иначе → **C**.
2. Если габариты OK и **K > 0.8** (круг) → **D**.
3. Иначе → **B**.
4. **C-priority:** негабарит + круг → только **C**.
5. Граница: **K = 0.8 не круг** (→ B, не D). Одинаково в web и CV.
## Архитектура
**Web**
```
Real device path:
RealSense D415 → depth preprocess → segmentation → measurement
→ B/C/D → optional MQTT / hardware
Web path:
Digital product → simulated sensor → classifier
→ physical digital twin → B/C/D receiver visualization
Product → measurement (digital) → classifier → route → twin → B/C/D receiver
```
Shared: B/C/D semantics and official dimension/roundness rules.
Not shared today: live camera frames into the website.
## 7. Repository structure
**CV**
```
.github/ CI (build, unit, e2e)
3d_models/ Author CAD (conveer.FCStd)
cv/ Real CV prototype (Python)
docs/ Engineering notes
e2e/ Playwright smoke/routes
input_info/ Official Ozon input packs
official_sources/ Classifier bounds PDF
public/ Runtime static assets (GLB/STL/draco)
src/ React/Three web twin
Dockerfile Web production image
docker-compose.server.yml
nginx.conf
package.json / lock
vite / vitest / playwright / tsconfig
README.md
RealSense D415 → depth → segmentation → L×W×H + K → B/C/D → optional MQTT
```
No other top-level product directories are required to run or understand the solution.
CV **не подключён** к https://arhipovdan.ru. Общее — домен правил B/C/D, не live-канал кадров.
## 8. Requirements
## Технологии
**Web:** Node.js 20+, npm (`package-lock.json`).
**CV:** Python 3.10+, ffmpeg, V4L2; RealSense D415 for live mode (`cv/requirements.txt`).
**Hardware (CV live / MQTT):** D415 USB3; optional MQTT broker + servo/motor controllers on site network.
| Слой | Стек |
|---|---|
| Web | React, TypeScript, Three.js / R3F, Rapier, Vite, Vitest, Playwright |
| CV | Python, OpenCV, RealSense D415 (V4L2), optional MQTT (`paho-mqtt`) |
## 9. Web quick start
## Структура репозитория
```
.github/ CI
3d_models/ Авторский CAD (conveer.FCStd)
cv/ CV-прототип RealSense + OpenCV
docs/ Engineering notes
e2e/ Playwright smoke/routes
input_info/ Официальные входные пакеты
official_sources/ PDF с границами classifier
presentation/ Финальная презентация (один PDF)
public/ Runtime GLB/STL/draco
src/ Web twin
```
Конфиги деплоя: `Dockerfile`, `docker-compose.server.yml`, `nginx.conf`, `package.json`.
## Запуск web
Node.js 20+.
```bash
npm ci
@@ -114,100 +102,78 @@ npm run build
npm run preview # http://127.0.0.1:3100
```
## 10. CV quick start
## Запуск CV
Python 3.10+. Live-режим требует Intel RealSense D415 и ffmpeg.
```bash
cd cv
./demo.sh # venv + deps; HUD on :8080 (needs D415 for live view)
# without camera:
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
.venv/bin/python test_classify.py
.venv/bin/python test_geometry.py
# live pipeline (hardware):
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp config.example.yaml config.yaml # MQTT выключен по умолчанию
# без камеры:
python test_classify.py
python test_geometry.py
# с камерой:
./demo.sh # HUD http://127.0.0.1:8080/
./run.sh --preview --no-mqtt --no-motor
```
`npm install` does **not** install CV dependencies.
Подробности: [`cv/README.md`](cv/README.md). `npm` CV-зависимости не ставит.
## 11. Configuration
## Конфигурация
**Web (build-time, optional):** `VITE_BUILD_COMMIT`, `VITE_BUILD_BRANCH`, `VITE_BUILD_RELEASE``/version.json`. No runtime secrets.
- `.env` **не** коммитится.
- Web: опционально `VITE_BUILD_COMMIT` / `VITE_BUILD_BRANCH` / `VITE_BUILD_RELEASE` для `/version.json`.
- CV: `cv/config.example.yaml` → локальный `cv/config.yaml` (gitignored). MQTT/motor/routing **disabled by default**. Секреты не хранить в Git.
**CV:** copy `cv/config.example.yaml``cv/config.yaml` (gitignored). MQTT/motor/routing **disabled by default**. Never commit real passwords or broker credentials.
## CAD и модели
## 12. Classification rules
Verified in web (`src/domain/classifier.ts`) and CV (`cv/classify.py`):
- dimensions strictly **> 10×10×10 mm** and **< 450×320×320 mm**
- circular when **K > 0.8** (web) / `circle_ratio ≥ 0.8` (CV)
- order: dimensions fail → **C**; else circular → **D**; else **B**
Official citation: `official_sources/doc-1783095831.pdf` (present; not re-parsed on every doc pass). Missing extracted brief PDF is not claimed.
## 13. Physics (accepted `main`)
From current source (not superseded experimental branches):
- belt speed **1.0 m/s** (`CONVEYOR_SPEED_MPS`)
- fixed timestep **1/60 s** (`PHYSICS_TIMESTEP_SEC`)
- CCD for light/thin product bodies
- diverters LEFT **45°**, RIGHT **+45°**
- full contact-only junction sorting through CAD: **not fully validated**
## 14. CAD and assets
| Asset | Path |
| Файл | Роль |
|---|---|
| Author CAD | `3d_models/conveer.FCStd` |
| Runtime GLB | `public/models/sorter/conveyor-clean.glb` |
| Products | `public/models/*.stl` |
| `3d_models/conveer.FCStd` | Авторский CAD |
| `public/models/sorter/conveyor-clean.glb` | Runtime конвейер |
| `public/models/*.stl` | Модели товаров |
Keep runtime assets in Git for deploy. Mirror large CAD/models/presentation/video to cloud for submission.
Крупные материалы для сдачи дополнительно зеркалируйте в облако; runtime-ассеты для деплоя остаются в Git.
SHA-256 (frozen):
## Физика (текущий main)
```
3d_models/conveer.FCStd
90c1844a4ca05e26def783d6130fc4b993430dde14307534ef8fbb21c9fac2e6
- скорость ленты **1.0 м/с**;
- timestep **1/60 с**;
- CCD для лёгких/тонких тел;
- diverters 45° / +45°;
- полный contact-only junction sorting **не fully validated**.
public/models/sorter/conveyor-clean.glb
1dc7a8d7891bfe756e277ad5368df74cb73410156b2fe0f92845afb8a56f285a
```
## 15. Testing
## Проверка
```bash
npm test -- --run
# current release result: 196/196
npm run build
PLAYWRIGHT_BASE_URL=http://127.0.0.1:3101 npm run test:e2e -- e2e/routes.spec.ts e2e/smoke.spec.ts
npm ci && npm test -- --run && npm run build
# E2E: preview :3101 + e2e/routes.spec.ts + e2e/smoke.spec.ts
cd cv && .venv/bin/python test_classify.py && .venv/bin/python test_geometry.py
cd cv && python -m compileall . && python test_classify.py && python test_geometry.py
```
## 16. Deployment
Актуальный релиз: unit **196/196**, build PASS, focused E2E PASS, CV compile + unit без камеры PASS. Прод: `/` и `/documentation` → 200.
Nginx terminates TLS for `arhipovdan.ru` and proxies to Docker `owl-web-1` (`docker-compose.server.yml` + `Dockerfile`) on `127.0.0.1:3100`. Deploy from `main` with build-args for `/version.json`. CV is **not** part of the web container.
## Ограничения
## 17. Submission materials
- инженерный прототип, не industrial-certified ПАК;
- CV не live-интегрирован в web;
- live CV требует D415; на CI — только unit/compile;
- параметры симуляции требуют калибровки на стенде;
- mobile — 2D lite fallback;
- облачная ссылка на доп. материалы — по решению владельца.
| Material | Status |
|---|---|
| Presentation URL | REQUIRED_FROM_OWNER |
| Video demo URL | REQUIRED_FROM_OWNER |
| Cloud folder URL | REQUIRED_FROM_OWNER |
## Материалы
Do not invent links. Runtime site assets remain in Git even when mirrored to cloud.
- Презентация: [`presentation/Owl_Prime_Ozon_Tech_Track_3_FINAL.pdf`](presentation/Owl_Prime_Ozon_Tech_Track_3_FINAL.pdf)
- Production: https://arhipovdan.ru
- Cloud folder: `[ДОБАВИТЬ ССЫЛКУ]`
## 18. Known limitations
## Статус
- Mobile uses 2D lite fallback on current baseline
- CV is a prototype and is not live-integrated into arhipovdan.ru
- Simulation physics is engineering-derived; hardware calibration still required
- Contact routing through CAD diverters not fully validated
- Large presentation/video must be uploaded to cloud by owner
## 19. Branch history policy
**`main` is the canonical complete solution** (web + cleaned CV under `cv/`). Historical branches (`dan_branch`, `drho1y-mvp_1`, …) may remain for audit but are not required to run the product.
**`main` — каноническое полное решение.** Другие ветки исторические и не нужны для запуска web или CV.

View File

@@ -36,7 +36,8 @@ Primary modules: `main.py` (live), `demo.py` (HUD), `classify.py` (rules), `meas
## Classification rules (Track 3)
1. Dimensions must be strictly **> 10×10×10 mm** and **< 450×320×320 mm** → else **C**
2. Else if `circle_ratio 0.8`**D**
2. Else if `circle_ratio > 0.8`**D**
(`K == 0.8` is **not** circular — same strict rule as web `classifier.ts`)
3. Else → **B**
Stabilization: median window + vote → **LOCK**. Uncertain cases fall back to zone **C** after N frames.

View File

@@ -79,13 +79,14 @@ def classify(
"""
Порядок ТЗ:
1) габариты → иначе C (приоритет над кругом)
2) если r_in/r_out >= 0.8 в любом сечении → D
2) если r_in/r_out > 0.8 в любом сечении → D
(K == 0.8 НЕ круг — как в web classifier.ts)
3) иначе → B
"""
dims = _sorted_dims(measurement.length_mm, measurement.width_mm, measurement.height_mm)
passes = check_size(dims, min_mm, max_mm)
ratio = float(measurement.circle_ratio)
circular = ratio >= float(circle_ratio_threshold)
circular = ratio > float(circle_ratio_threshold)
clipped = bool(getattr(measurement, "clipped_by_frame", False))
if not passes or clipped:
@@ -112,7 +113,7 @@ def classify(
circle_ratio=ratio,
passes_size=True,
is_circular=True,
reason=f"круг в сечении: r_in/r_out={ratio:.3f} >= {circle_ratio_threshold}",
reason=f"круг в сечении: r_in/r_out={ratio:.3f} > {circle_ratio_threshold}",
)
return ClassificationResult(
@@ -121,7 +122,7 @@ def classify(
circle_ratio=ratio,
passes_size=True,
is_circular=False,
reason=f"габариты OK, круга нет: r_in/r_out={ratio:.3f} < {circle_ratio_threshold}",
reason=f"габариты OK, круга нет: r_in/r_out={ratio:.3f} <= {circle_ratio_threshold}",
)

View File

@@ -113,12 +113,13 @@ class DecisionStabilizer:
top_m = _median(self._tops)
sec_m = _median(self._secs)
ratio_p75 = _percentile(self._ratios, 75)
# устойчивый круг: медиана >= 0.8 ИЛИ (медиана сечений >= 0.8 и ≥ половины окна сильные)
# устойчивый круг: медиана > 0.8 ИЛИ (медиана сечений > 0.8 и ≥ половины окна сильные)
# K == 0.8 официально НЕ круг (строгое > threshold)
sec_strong = (
sum(1 for x in self._secs if x >= self.enter_circle) / max(1, len(self._secs))
sum(1 for x in self._secs if x > self.enter_circle) / max(1, len(self._secs))
)
circular = ratio >= self.enter_circle or (
sec_m >= self.enter_circle and sec_strong >= 0.55
circular = ratio > self.enter_circle or (
sec_m > self.enter_circle and sec_strong >= 0.55
)
ratio_show = max(ratio, sec_m) if circular else ratio
@@ -153,7 +154,7 @@ class DecisionStabilizer:
reason=(
f"круг: med={ratio:.3f} p75={ratio_p75:.3f} "
f"top={top_m:.3f} sec={sec_m:.3f} strong={sec_strong:.0%} "
f">= {self.enter_circle}"
f"> {self.enter_circle}"
),
)
else:
@@ -165,7 +166,7 @@ class DecisionStabilizer:
is_circular=False,
reason=(
f"не круг: med={ratio:.3f} sec={sec_m:.3f} "
f"strong={sec_strong:.0%} < {self.enter_circle}"
f"strong={sec_strong:.0%} <= {self.enter_circle}"
),
)

View File

@@ -43,11 +43,40 @@ def test_lying_bottle_must_be_D_not_B():
assert r.category.zone == "D"
def test_border_circle():
r = classify_from_dims(100, 50, 50, circle_ratio=0.8)
def test_strict_circle_boundary():
"""Официально: круг только при K > 0.8. K == 0.8 → B (не D)."""
below = classify_from_dims(100, 50, 50, circle_ratio=0.799999)
assert below.category == Category.SUITABLE
assert below.category.zone == "B"
assert below.is_circular is False
exact = classify_from_dims(100, 50, 50, circle_ratio=0.8)
assert exact.category == Category.SUITABLE
assert exact.category.zone == "B"
assert exact.is_circular is False
above = classify_from_dims(100, 50, 50, circle_ratio=0.800001)
assert above.category == Category.NEED_PACK
assert above.category.zone == "D"
assert above.is_circular is True
def test_normal_non_circular_B():
r = classify_from_dims(120, 80, 40, circle_ratio=0.5)
assert r.category == Category.SUITABLE
assert r.category.zone == "B"
def test_normal_circular_above_threshold_D():
r = classify_from_dims(100, 50, 50, circle_ratio=0.81)
assert r.category == Category.NEED_PACK
r2 = classify_from_dims(100, 50, 50, circle_ratio=0.799)
assert r2.category == Category.SUITABLE
assert r.category.zone == "D"
def test_oversized_circular_still_C():
r = classify_from_dims(500, 100, 100, circle_ratio=0.95)
assert r.category == Category.OVERSIZE
assert r.category.zone == "C"
def test_border_dims_strict():
@@ -80,7 +109,10 @@ if __name__ == "__main__":
test_too_small()
test_need_pack_cylinder()
test_lying_bottle_must_be_D_not_B()
test_border_circle()
test_strict_circle_boundary()
test_normal_non_circular_B()
test_normal_circular_above_threshold_D()
test_oversized_circular_still_C()
test_border_dims_strict()
test_dims_order_independent()
print("OK: all classification tests passed")

View File

@@ -20,8 +20,11 @@ Companion to `README.md` and `/documentation`. Not a stage changelog.
| `OPENING_SAFETY_MARGIN_SEC` | 0.15 | same |
| Contact / clear planes | ≈1.0538 / 1.6000 | `buildDiverterPlanes` + mount hinge |
| Classifier min/max | exclusive 10³ / 450×320×320 | `src/domain/classifier.ts` |
| Roundness | K > 0.8 | same |
| Roundness | K > 0.8 (K = 0.8 not circular); web + CV aligned | `classifier.ts` / `cv/classify.py` |
| Belt speed (sim) | 1.0 m/s | runtime / physics helpers |
| Physics timestep | 1/60 s | Rapier world step |
| `CONVEYOR_CAD_URL` | `/models/sorter/conveyor-clean.glb` | `ConveyorCadModel.tsx` |
| Presentation | `presentation/Owl_Prime_Ozon_Tech_Track_3_FINAL.pdf` | 10 slides |
## Active source tree (runtime)
@@ -69,10 +72,10 @@ Grounded only in present sources + current `main` code/tests (not a full officia
|---|---|---|---|---|
| B/C/D bounds & roundness | `official_sources/doc-1783095831.pdf` | web `classifier.ts`, CV `cv/classify.py` | unit tests | PARTIAL (PDF not re-parsed each pass) |
| Digital twin demo | workspace PDFs in `input_info/` | `/` + `/documentation` | e2e smoke/routes, production | PASS (demo present) |
| CAD conveyor | author pack / STL references | `3d_models/`, `conveyor-clean.glb` | checksums in README | PASS (assets present) |
| Real measurement CV | Track 3 camera intent in briefs | `cv/` RealSense+OpenCV | `cv/README.md`, `test_classify.py` | PARTIAL (prototype, not live web) |
| CAD conveyor | author pack / STL references | `3d_models/`, `conveyor-clean.glb` | checksums in `/documentation` | PASS (assets present) |
| Real measurement CV | Track 3 camera intent in briefs | `cv/` RealSense+OpenCV on `main` | `cv/README.md`, `test_classify.py` | PARTIAL (WORKING_PROTOTYPE, not live web) |
| Physical industrial line | scoring/workspace PDFs | web physics + optional MQTT CV | code; contact not fully validated | PARTIAL |
| Presentation / video | platform rules | cloud links | README §17 | NOT_VERIFIED (links missing) |
| Presentation | platform rules | `presentation/Owl_Prime_Ozon_Tech_Track_3_FINAL.pdf` | 10-page PDF in repo | PASS (file present) |
## Real CV prototype

View File

@@ -10,7 +10,10 @@ test.describe('routes', () => {
await page.getByTestId('nav-documentation').first().click();
await expect(page).toHaveURL(/\/documentation\/?$/);
await expect(page.getByTestId('documentation-page')).toBeVisible();
await expect(page.getByTestId('docs-production-status')).toContainText('DATA_ACQUISITION_PACK_READY');
await expect(page.getByTestId('docs-production-status')).toContainText(
'FINAL_ENGINEERING_PROTOTYPE_READY',
);
await expect(page.getByTestId('docs-production-status')).toContainText('WORKING_PROTOTYPE');
await expect(page.getByTestId('nav-documentation').first()).toHaveClass(/active/);
await page.reload();

Binary file not shown.

View File

@@ -4,16 +4,29 @@
* Unverified / planned items are labeled explicitly — never implied complete.
*/
/** Kept for e2e (`docs-production-status`) and acquisition-pack provenance. */
export const PRODUCTION_STATUS = {
acquisitionPackStatus: 'DATA_ACQUISITION_PACK_READY',
webTwinStatus: 'BASELINE_PRESERVED',
projectStatus: 'FINAL_ENGINEERING_PROTOTYPE_READY',
webTwinStatus: 'PRODUCTION_LIVE',
cvStatus: 'WORKING_PROTOTYPE',
cvLiveIntegrated: false,
unitTests: '196/196',
cvTests: 'PASS (no camera)',
productionBuild: 'PASS',
productionUrl: 'https://arhipovdan.ru',
contactPhysics: 'NOT_FULLY_VALIDATED',
officialCompliance: 'PARTIAL_SOURCES_PRESENT',
canonicalBranch: 'main',
} as const;
export const SOLUTION_COMPONENTS = [
'Web digital twin (https://arhipovdan.ru)',
'Real CV working prototype (cv/, RealSense D415 + OpenCV)',
'Physical experimental conveyor stand',
'Author CAD (3d_models/conveer.FCStd)',
'Official B/C/D classifier (web + CV, K > 0.8)',
'Engineering documentation (/documentation)',
] as const;
export const CONFIRMED_LAYOUT = {
workspaceMm: { length: 10000, width: 6000 },
conveyorWidthMm: 500,
@@ -23,7 +36,7 @@ export const CONFIRMED_LAYOUT = {
} as const;
export const CLASSIFIER_BOUNDS = {
status: 'CURRENT_IMPLEMENTATION_VERIFIED_IN_CODE' as const,
status: 'ALIGNED_WEB_AND_CV' as const,
officialSourceReference: 'official_sources/doc-1783095831.pdf',
officialSourceParsedThisPass: false,
minExclusiveMm: { width: 10, depth: 10, height: 10 },
@@ -33,8 +46,63 @@ export const CLASSIFIER_BOUNDS = {
min: '> 10×10×10 мм',
max: '< 450×320×320 мм',
roundness: 'K > 0.8',
exactBoundary: 'K = 0.8 → B (not circular), not D',
},
checkOrder: 'dimensions→C, else circular→D, else B' as const,
checkOrder: 'dimensions→C, else circular (K>0.8)→D, else B' as const,
} as const;
export const CV_PROTOTYPE = {
path: 'cv/',
status: 'WORKING_PROTOTYPE',
hardware: 'Intel RealSense D415',
software: 'Python + OpenCV (+ optional MQTT)',
liveIntegrated: false,
pipeline: [
'depth frame',
'segmentation',
'object contour',
'L×W×H',
'roundness K',
'B/C/D',
'optional MQTT',
] as const,
notes: [
'CV works as a separate prototype under cv/ on main.',
'CV is not connected directly to the live public website.',
'Hardware live validation requires RealSense D415.',
'No-camera unit tests and compileall run in CI/local verification.',
],
} as const;
export const PHYSICAL_STAND = {
status: 'EXPERIMENTAL_PROTOTYPE',
elements: [
'Physical belt conveyor',
'Camera mounting structure above the belt',
'Intel RealSense D415',
'Electronics / control nodes',
'Experimental actuators and printed components',
] as const,
purpose: 'Measurements, calibration, and hardware validation — not claimed as industrial end-to-end certified sorting.',
} as const;
export const ARCHITECTURE = {
realPath: [
'physical product',
'RealSense D415',
'OpenCV measurement',
'classifier (K > 0.8)',
'B/C/D result',
'optional MQTT/controller',
] as const,
digitalPath: [
'digital product',
'simulated measurement',
'same classifier rules',
'route command',
'digital twin',
'B/C/D receiver',
] as const,
} as const;
export const ROUTE_MAPPING = [
@@ -72,6 +140,7 @@ export const PHYSICS_STATUS = {
'Synchronized CAD diverter visual / kinematic targets',
'CCD enabled for light/thin SKUs in runtime and headless sim',
'Visual/physics spawn gating via product asset preload',
'Belt speed target 1.0 m/s; physics timestep 1/60 s',
],
notFullyValidated: [
'Complete contact-only routing through CAD diverters',
@@ -83,18 +152,26 @@ export const PHYSICS_STATUS = {
planned: [
'Visual full belt loop with surface-velocity coupling',
'Controlled tangential friction at 1 m/s',
'Dynamic rigid bodies for divert segment with fixed timestep',
'Per-SKU collider, damping, and friction profiles',
],
} as const;
export const MOBILE_BEHAVIOR = {
desktop: 'Interactive WebGL 3D digital twin',
mobile:
'Capability-based tier: WebGL when viable; SVG/2D lite fallback on low FPS / missing WebGL (not claimed as full 3D)',
} as const;
export const VALIDATION_BOARD = [
{ item: 'Unit tests', status: '196/196 PASS' },
{ item: 'Web unit tests', status: '196/196 PASS' },
{ item: 'Production build', status: 'PASS' },
{ item: 'Active routes / + /documentation', status: 'PASS' },
{ item: 'Focused E2E (/ + /documentation)', status: 'PASS' },
{ item: 'CV compileall', status: 'PASS' },
{ item: 'CV classify/geometry tests', status: 'PASS (no camera)' },
{ item: 'Production / and /documentation', status: 'PASS' },
{ item: 'conveyor-clean.glb checksum', status: 'PASS' },
{ item: 'Author FCStd checksum', status: 'PASS' },
{ item: 'Frozen diverter angles / 0.50 s', status: 'PASS' },
{ item: 'Classifier K > 0.8 (web + CV)', status: 'ALIGNED' },
{ item: 'Full contact physics', status: 'NOT_FULLY_VALIDATED' },
] as const;
@@ -146,7 +223,14 @@ export const OFFICIAL_SOURCE_MATRIX = [
purpose: 'Classifier bounds authority cited by code',
present: true,
canonical: true,
usage: 'Referenced by classifier.ts; not re-parsed this pass',
usage: 'Referenced by classifier.ts and cv/classify.py',
},
{
source: 'presentation/Owl_Prime_Ozon_Tech_Track_3_FINAL.pdf',
purpose: 'Final presentation (10 slides)',
present: true,
canonical: true,
usage: 'Single presentation PDF in repository',
},
{
source: 'input_info/extracted/Постановка_Задача_3_сжато_2.pdf',
@@ -169,14 +253,24 @@ export const RUNTIME_FLOW = [
'RECEIVER',
] as const;
export const CURRENT_LIMITATIONS = [
'Full physical contact sorting through CAD diverters is not fully validated.',
'Belt surface-velocity drive at exactly 1 m/s is planned, not complete.',
'Per-SKU physical parameters still require profiling/calibration.',
'Author CAD horn / transmission incomplete in active GLB.',
'Official compliance is partial: missing extracted task PDF; scoring PDF not re-parsed this pass.',
'Repository retains only the web twin, author CAD, official sources, and active tests.',
export const REPOSITORY_LAYOUT = [
'src/ — web digital twin',
'cv/ — RealSense + OpenCV prototype',
'3d_models/ — author CAD',
'public/ — runtime GLB/STL/draco',
'docs/ — engineering notes',
'presentation/ — final PDF',
'e2e/ + Vitest — tests',
'Docker / nginx — deployment',
] as const;
/** @deprecated alias — historical Gate wording retained for acquisition-pack note */
export const DATA_ACQUISITION_SEQUENCE = ['MEASURE', 'VERIFY', 'FREEZE', 'DESIGN'] as const;
export const CURRENT_LIMITATIONS = [
'Engineering prototype, not an industrial-certified PAK.',
'Real CV prototype is not live-integrated into the public website.',
'Live camera mode requires Intel RealSense D415 hardware.',
'Physical stand parameters still require calibration against the digital twin.',
'Full physical contact sorting through CAD diverters is not fully validated.',
'Belt surface-velocity drive at exactly 1 m/s is not fully validated.',
'Author CAD horn / transmission incomplete in active GLB.',
'Cloud presentation/video links for the platform form are provided separately by the team.',
] as const;

View File

@@ -1,30 +1,37 @@
import AppNav from '../components/AppNav';
import {
ARCHITECTURE,
CAD_PROVENANCE,
CLASSIFIER_BOUNDS,
CONFIRMED_LAYOUT,
CURRENT_LIMITATIONS,
CV_PROTOTYPE,
DIVERTER_KINEMATICS,
MOBILE_BEHAVIOR,
OFFICIAL_SOURCE_MATRIX,
PHYSICAL_STAND,
PHYSICS_STATUS,
PRODUCTION_STATUS,
REPOSITORY_LAYOUT,
ROUTE_MAPPING,
RUNTIME_FLOW,
SOLUTION_COMPONENTS,
VALIDATION_BOARD,
} from '../data/productionStatusSummary';
const SECTIONS = [
{ id: 'about', title: '1. Цель проекта' },
{ id: 'runtime-flow', title: '2. Runtime flow' },
{ id: 'layout', title: '3. Активная линия' },
{ id: 'classifier', title: '4. Классификация' },
{ id: 'routes', title: '5. Маршруты B/C/D' },
{ id: 'diverter', title: '6. Diverter state machine' },
{ id: 'cad', title: '7. CAD provenance' },
{ id: 'physics', title: '8. Физика' },
{ id: 'validation', title: '9. Validation status' },
{ id: 'official', title: '10. Official-source matrix' },
{ id: 'limits', title: '11. Known limitations' },
{ id: 'about', title: '1. Обзор проекта' },
{ id: 'production-web', title: '2. Production web' },
{ id: 'cv', title: '3. Real CV prototype' },
{ id: 'classifier', title: '4. Официальная классификация' },
{ id: 'stand', title: '5. Физический стенд' },
{ id: 'twin', title: '6. Digital twin' },
{ id: 'architecture', title: '7. Архитектура' },
{ id: 'repository', title: '8. Репозиторий' },
{ id: 'validation', title: '9. Validation' },
{ id: 'limits', title: '10. Ограничения' },
{ id: 'routes', title: '11. Маршруты B/C/D' },
{ id: 'official', title: '12. Official sources' },
] as const;
function TocList({ onNavigate }: { onNavigate?: () => void }) {
@@ -55,15 +62,15 @@ export default function DocumentationPage() {
O
</span>
<div className="product-brand-text">
<div className="product-brand-title">Ozon Tech Sorter</div>
<div className="product-brand-sub">Цифровой двойник линии сортировки</div>
<div className="product-brand-title">OWL PRIME</div>
<div className="product-brand-sub">Ozon Tech Track 3 документация</div>
</div>
</div>
<AppNav variant="solid" />
<div className="product-telemetry docs-topbar-meta">
<div className="product-tele-item">
<span className="product-tele-label">Раздел</span>
<span className="product-tele-value">Документация</span>
<span className="product-tele-label">Ветка</span>
<span className="product-tele-value">{PRODUCTION_STATUS.canonicalBranch}</span>
</div>
</div>
</header>
@@ -75,7 +82,7 @@ export default function DocumentationPage() {
</span>
<div>
<h1 className="docs-title">Документация</h1>
<p className="docs-subtitle">Текущее состояние веб-симуляции и инженерный статус</p>
<p className="docs-subtitle">Финальный инженерный статус решения OWL PRIME</p>
</div>
</div>
<AppNav variant="solid" />
@@ -99,143 +106,158 @@ export default function DocumentationPage() {
<main className="docs-main">
<section className="docs-status-banner" data-testid="docs-production-status">
<div className="docs-status-label">Текущий статус</div>
<div className="docs-status-value">{PRODUCTION_STATUS.acquisitionPackStatus}</div>
<div className="docs-status-value">{PRODUCTION_STATUS.projectStatus}</div>
<p className="docs-status-note">
Web twin: {PRODUCTION_STATUS.webTwinStatus}. Unit tests {PRODUCTION_STATUS.unitTests},
build {PRODUCTION_STATUS.productionBuild}. Contact physics:{' '}
{PRODUCTION_STATUS.contactPhysics}. Official compliance:{' '}
{PRODUCTION_STATUS.officialCompliance}.
Web: {PRODUCTION_STATUS.webTwinStatus}. CV: {PRODUCTION_STATUS.cvStatus} (live
integrated: {PRODUCTION_STATUS.cvLiveIntegrated ? 'YES' : 'NO'}). Unit tests{' '}
{PRODUCTION_STATUS.unitTests}, CV tests {PRODUCTION_STATUS.cvTests}, build{' '}
{PRODUCTION_STATUS.productionBuild}. Domain: {PRODUCTION_STATUS.productionUrl}. Contact
physics: {PRODUCTION_STATUS.contactPhysics}.
</p>
</section>
<section id="about" className="docs-section">
<h2>{SECTIONS[0].title}</h2>
<p>
Цифровой двойник линии сортировки Ozon Tech: конвейер, камера/классификация, физическая
маршрутизация и три приёмные категории (B / C / D). Страница <code>/</code> рабочая
веб-симуляция; эта страница каноническая сводка текущего состояния.
OWL PRIME инженерный прототип предварительной сортировки товаров для Ozon Tech Track
3: веб-цифровой двойник, реальный CV-прототип, физический экспериментальный стенд,
авторский CAD и единые правила классификации B/C/D.
</p>
<ul>
{SOLUTION_COMPONENTS.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</section>
<section id="runtime-flow" className="docs-section">
<section id="production-web" className="docs-section">
<h2>{SECTIONS[1].title}</h2>
<p>{RUNTIME_FLOW.join(' → ')}</p>
</section>
<section id="layout" className="docs-section">
<h2>{SECTIONS[2].title}</h2>
<p>
Production: <a href={PRODUCTION_STATUS.productionUrl}>{PRODUCTION_STATUS.productionUrl}</a>
</p>
<ul>
<li>
Рабочая зона (чертёж): {CONFIRMED_LAYOUT.workspaceMm.length} ×{' '}
{CONFIRMED_LAYOUT.workspaceMm.width} мм; лента {CONFIRMED_LAYOUT.conveyorWidthMm} мм /
высота верха {CONFIRMED_LAYOUT.conveyorHeightMm} мм.
<code>/</code> непрерывная симуляция конвейера
</li>
<li>Модули CAD: {CONFIRMED_LAYOUT.modules.join(', ')}.</li>
<li>
<code>/documentation</code> эта инженерная документация
</li>
</ul>
<p>
На desktop интерактивный WebGL 3D twin: CAD-конвейер, цифровое измерение,
классификация B/C/D, CAD-дивертеры и физика Rapier. Mobile: {MOBILE_BEHAVIOR.mobile}.
</p>
<ul>
<li>
Рабочая зона: {CONFIRMED_LAYOUT.workspaceMm.length} × {CONFIRMED_LAYOUT.workspaceMm.width}{' '}
мм; лента {CONFIRMED_LAYOUT.conveyorWidthMm} мм / высота{' '}
{CONFIRMED_LAYOUT.conveyorHeightMm} мм.
</li>
<li>Модули: {CONFIRMED_LAYOUT.modules.join(', ')}.</li>
<li>Приёмники: {CONFIRMED_LAYOUT.receivers.join(', ')}.</li>
<li>Runtime flow: {RUNTIME_FLOW.join(' → ')}</li>
</ul>
</section>
<section id="cv" className="docs-section">
<h2>{SECTIONS[2].title}</h2>
<p>
Path: <code>{CV_PROTOTYPE.path}</code>. Status:{' '}
<code>{CV_PROTOTYPE.status}</code>. Hardware: {CV_PROTOTYPE.hardware}. Software:{' '}
{CV_PROTOTYPE.software}.
</p>
<p>Pipeline: {CV_PROTOTYPE.pipeline.join(' → ')}</p>
<ul>
{CV_PROTOTYPE.notes.map((n) => (
<li key={n}>{n}</li>
))}
</ul>
</section>
<section id="classifier" className="docs-section">
<h2>{SECTIONS[3].title}</h2>
<p>
Status: <code>{CLASSIFIER_BOUNDS.status}</code>
Status: <code>{CLASSIFIER_BOUNDS.status}</code>. Source:{' '}
<code>{CLASSIFIER_BOUNDS.officialSourceReference}</code>.
</p>
<ul>
<li>Min: {CLASSIFIER_BOUNDS.display.min}</li>
<li>Max: {CLASSIFIER_BOUNDS.display.max}</li>
<li>Roundness: {CLASSIFIER_BOUNDS.display.roundness}</li>
<li>
Boundary: <strong>{CLASSIFIER_BOUNDS.display.exactBoundary}</strong>
</li>
<li>Order: {CLASSIFIER_BOUNDS.checkOrder}</li>
<li>Oversized circular product still goes to C (C priority).</li>
</ul>
<p>
OFFICIAL_SOURCE_REFERENCE: <code>{CLASSIFIER_BOUNDS.officialSourceReference}</code>
{CLASSIFIER_BOUNDS.officialSourceParsedThisPass
? ''
: ' — PDF not re-parsed in this documentation pass; bounds match current code/tests.'}
Web (<code>src/domain/classifier.ts</code>) and CV (<code>cv/classify.py</code>) both use
strict <code>K &gt; 0.8</code>.
</p>
</section>
<section id="routes" className="docs-section">
<section id="stand" className="docs-section">
<h2>{SECTIONS[4].title}</h2>
<div className="docs-table-scroll">
<table className="docs-table">
<thead>
<tr>
<th>Category</th>
<th>Physical route</th>
<th>CAD diverter</th>
<th>Angle</th>
</tr>
</thead>
<tbody>
{ROUTE_MAPPING.map((row) => (
<tr key={row.category}>
<td>{row.category}</td>
<td>
<code>{row.physicalRoute}</code>
</td>
<td>{row.activeDiverter}</td>
<td>{row.signedAngleDeg}°</td>
</tr>
))}
</tbody>
</table>
</div>
<p>
Status: <code>{PHYSICAL_STAND.status}</code>. {PHYSICAL_STAND.purpose}
</p>
<ul>
{PHYSICAL_STAND.elements.map((el) => (
<li key={el}>{el}</li>
))}
</ul>
</section>
<section id="diverter" className="docs-section">
<section id="twin" className="docs-section">
<h2>{SECTIONS[5].title}</h2>
<p>{DIVERTER_KINEMATICS.phases.join(' → ')}</p>
<ul>
<li>Rotation duration: {DIVERTER_KINEMATICS.rotationDurationSec.toFixed(2)} s</li>
<li>Safety margin: {DIVERTER_KINEMATICS.openingSafetyMarginSec.toFixed(2)} s</li>
<li>Contact plane S: {DIVERTER_KINEMATICS.contactPlaneS}</li>
<li>Clear plane S: {DIVERTER_KINEMATICS.clearPlaneS.toFixed(4)}</li>
<li>Route command bound to productId; one active product at a time</li>
<li>Close after rear-clear of clear plane</li>
<li>
Invented sorter drive mesh active:{' '}
{DIVERTER_KINEMATICS.generatedMechanismActive ? 'YES' : 'NO'}
</li>
</ul>
</section>
<section id="cad" className="docs-section">
<h2>{SECTIONS[6].title}</h2>
<ul>
<li>
Author: <code>{CAD_PROVENANCE.authorFcstd}</code>
<br />
SHA-256: <code>{CAD_PROVENANCE.authorSha256}</code>
Author CAD: <code>{CAD_PROVENANCE.authorFcstd}</code>
</li>
<li>
Runtime: <code>{CAD_PROVENANCE.runtimeGlb}</code>
<br />
SHA-256: <code>{CAD_PROVENANCE.runtimeSha256}</code>
Runtime conveyor: <code>{CAD_PROVENANCE.runtimeGlb}</code>
</li>
<li>Author servo/holder nodes used in sorter module only.</li>
<li>Invented sorter drive mesh inactive; author CAD diverters only.</li>
<li>Horn / transmission in GLB: {CAD_PROVENANCE.hornTransmissionInGlb}.</li>
<li>Product STL models in <code>public/models/</code></li>
<li>Same B/C/D classifier rules as CV</li>
<li>Rapier physics + CAD diverters (45° / +45°)</li>
<li>Synchronized digital routing into B/C/D receivers</li>
</ul>
</section>
<section id="physics" className="docs-section">
<h2>{SECTIONS[7].title}</h2>
<h3>IMPLEMENTED</h3>
<h3>Physics</h3>
<h4>IMPLEMENTED</h4>
<ul>
{PHYSICS_STATUS.implemented.map((x) => (
<li key={x}>{x}</li>
))}
</ul>
<h3>NOT FULLY VALIDATED</h3>
<h4>NOT FULLY VALIDATED</h4>
<ul>
{PHYSICS_STATUS.notFullyValidated.map((x) => (
<li key={x}>{x}</li>
))}
</ul>
<h3>PLANNED</h3>
<p>
Diverter phases: {DIVERTER_KINEMATICS.phases.join(' → ')} (
{DIVERTER_KINEMATICS.rotationDurationSec.toFixed(2)} s rotation).
</p>
</section>
<section id="architecture" className="docs-section">
<h2>{SECTIONS[6].title}</h2>
<h3>REAL PATH</h3>
<p>{ARCHITECTURE.realPath.join(' → ')}</p>
<h3>DIGITAL PATH</h3>
<p>{ARCHITECTURE.digitalPath.join(' → ')}</p>
</section>
<section id="repository" className="docs-section">
<h2>{SECTIONS[7].title}</h2>
<p>
Canonical branch: <code>{PRODUCTION_STATUS.canonicalBranch}</code>. Other branches are
historical and not required to run the solution.
</p>
<ul>
{PHYSICS_STATUS.planned.map((x) => (
<li key={x}>{x}</li>
{REPOSITORY_LAYOUT.map((line) => (
<li key={line}>{line}</li>
))}
</ul>
</section>
@@ -264,8 +286,45 @@ export default function DocumentationPage() {
</div>
</section>
<section id="official" className="docs-section">
<section id="limits" className="docs-section" data-testid="docs-blockers">
<h2>{SECTIONS[9].title}</h2>
<ul className="docs-blocker-list">
{CURRENT_LIMITATIONS.map((b) => (
<li key={b}>{b}</li>
))}
</ul>
</section>
<section id="routes" className="docs-section">
<h2>{SECTIONS[10].title}</h2>
<div className="docs-table-scroll">
<table className="docs-table">
<thead>
<tr>
<th>Category</th>
<th>Physical route</th>
<th>CAD diverter</th>
<th>Angle</th>
</tr>
</thead>
<tbody>
{ROUTE_MAPPING.map((row) => (
<tr key={row.category}>
<td>{row.category}</td>
<td>
<code>{row.physicalRoute}</code>
</td>
<td>{row.activeDiverter}</td>
<td>{row.signedAngleDeg}°</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
<section id="official" className="docs-section">
<h2>{SECTIONS[11].title}</h2>
<div className="docs-table-scroll">
<table className="docs-table">
<thead>
@@ -293,15 +352,6 @@ export default function DocumentationPage() {
</table>
</div>
</section>
<section id="limits" className="docs-section" data-testid="docs-blockers">
<h2>{SECTIONS[10].title}</h2>
<ul className="docs-blocker-list">
{CURRENT_LIMITATIONS.map((b) => (
<li key={b}>{b}</li>
))}
</ul>
</section>
</main>
</div>
</div>

View File

@@ -3312,17 +3312,24 @@ button:disabled {
}
/* =========================================================
Documentation page
Documentation page — light readable surface (Ozon blue)
========================================================= */
.docs-page {
--docs-bg: #f5f8fc;
--docs-panel: #ffffff;
--docs-text: #101828;
--docs-muted: #344054;
--docs-heading: #0b1f3a;
--docs-blue: #005bff;
--docs-magenta: #f91155;
--docs-line: #d0d5dd;
--docs-status-bg: #e8f1ff;
min-height: 100dvh;
max-width: 100vw;
overflow-x: hidden;
background:
radial-gradient(circle at 12% 0%, rgba(56, 189, 248, 0.12), transparent 32%),
linear-gradient(180deg, #07111d 0%, #050910 100%);
color: var(--text);
background: var(--docs-bg);
color: var(--docs-text);
}
.docs-header {
@@ -3336,9 +3343,8 @@ button:disabled {
gap: 12px 20px;
min-height: 56px;
padding: 14px 20px;
border-bottom: 1px solid var(--line);
background: rgba(5, 9, 16, 0.92);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--docs-line);
background: #ffffff;
}
.docs-brand {
@@ -3354,9 +3360,9 @@ button:disabled {
width: 36px;
height: 36px;
border-radius: 10px;
background: rgba(56, 189, 248, 0.15);
border: 1px solid rgba(56, 189, 248, 0.35);
color: var(--cyan);
background: rgba(0, 91, 255, 0.1);
border: 1px solid rgba(0, 91, 255, 0.35);
color: var(--docs-blue);
font-weight: 800;
flex: 0 0 auto;
}
@@ -3364,11 +3370,13 @@ button:disabled {
.docs-title {
font-size: 1.25rem;
line-height: 1.2;
color: var(--docs-heading);
}
.docs-subtitle {
color: var(--muted);
color: var(--docs-muted);
font-size: 0.85rem;
opacity: 1;
}
.docs-layout {
@@ -3385,9 +3393,9 @@ button:disabled {
top: 84px;
align-self: start;
padding: 12px;
border: 1px solid var(--line);
border: 1px solid var(--docs-line);
border-radius: 12px;
background: rgba(10, 19, 31, 0.7);
background: var(--docs-panel);
font-size: 0.85rem;
}
@@ -3405,16 +3413,17 @@ button:disabled {
.docs-toc-desktop a,
.docs-toc-mobile a {
color: var(--muted);
color: var(--docs-muted);
text-decoration: none;
display: inline-flex;
min-height: 36px;
align-items: center;
opacity: 1;
}
.docs-toc-desktop a:hover,
.docs-toc-mobile a:hover {
color: var(--cyan);
color: var(--docs-blue);
}
.docs-main {
@@ -3432,31 +3441,39 @@ button:disabled {
}
.docs-status-banner {
border: 1px solid rgba(74, 222, 128, 0.35);
background: rgba(20, 48, 32, 0.45);
border: 1px solid rgba(0, 91, 255, 0.28);
background: var(--docs-status-bg);
}
.docs-status-label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
color: var(--docs-blue);
opacity: 1;
}
.docs-status-value {
margin-top: 6px;
font-size: 1.15rem;
font-weight: 700;
color: var(--green);
color: var(--docs-heading);
overflow-wrap: anywhere;
word-break: break-word;
}
.docs-status-note {
margin-top: 10px;
color: var(--muted);
color: var(--docs-muted);
font-size: 0.92rem;
line-height: 1.5;
opacity: 1;
}
.docs-section h2,
.docs-section h3,
.docs-section h4 {
color: var(--docs-heading);
}
.docs-section h2 {
@@ -3464,9 +3481,19 @@ button:disabled {
margin-bottom: 10px;
}
.docs-section a {
color: var(--docs-blue);
}
.docs-section strong {
color: var(--docs-magenta);
font-weight: 700;
}
.docs-section p,
.docs-section li {
color: #c9d8e8;
color: var(--docs-text);
opacity: 1;
font-size: 0.98rem;
line-height: 1.5;
}
@@ -3480,19 +3507,22 @@ button:disabled {
.docs-section code {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 0.88em;
color: var(--cyan-hover);
color: var(--docs-blue);
background: rgba(0, 91, 255, 0.08);
padding: 0.1em 0.35em;
border-radius: 4px;
overflow-wrap: anywhere;
word-break: break-word;
}
.docs-conflict {
margin-top: 12px;
border: 1px solid rgba(245, 158, 11, 0.4);
background: rgba(48, 32, 10, 0.4);
border: 1px solid rgba(249, 17, 85, 0.35);
background: #fff5f8;
}
.docs-blocker-list li {
color: #ffd0d5;
color: var(--docs-text);
}
.docs-table-scroll {
@@ -3507,20 +3537,24 @@ button:disabled {
min-width: 420px;
border-collapse: collapse;
font-size: 0.92rem;
background: var(--docs-panel);
}
.docs-table th,
.docs-table td {
padding: 8px 10px;
border: 1px solid var(--line);
border: 1px solid var(--docs-line);
text-align: left;
vertical-align: top;
white-space: nowrap;
white-space: normal;
color: var(--docs-text);
overflow-wrap: anywhere;
word-break: break-word;
}
.docs-table th {
background: rgba(12, 23, 37, 0.9);
color: var(--muted);
background: #eef4ff;
color: var(--docs-heading);
font-weight: 600;
}
@@ -3689,8 +3723,7 @@ button:disabled {
background: #070d16;
}
.main-page--product .product-topbar,
.docs-page .product-topbar {
.main-page--product .product-topbar {
position: relative;
z-index: 30;
flex: 0 0 auto;
@@ -3706,6 +3739,41 @@ button:disabled {
backdrop-filter: blur(14px);
}
.docs-page .product-topbar {
position: relative;
z-index: 30;
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
height: 68px;
max-height: 72px;
padding: 0 24px;
border-bottom: 1px solid var(--docs-line);
background: #ffffff;
backdrop-filter: none;
}
.docs-page .product-brand-mark {
border-color: rgba(0, 91, 255, 0.35);
background: rgba(0, 91, 255, 0.1);
color: var(--docs-blue);
}
.docs-page .product-brand-title {
color: var(--docs-heading);
}
.docs-page .product-brand-sub,
.docs-page .product-tele-label {
color: var(--docs-muted);
}
.docs-page .product-tele-value {
color: var(--docs-text);
}
.product-brand {
display: flex;
align-items: center;
@@ -3742,8 +3810,7 @@ button:disabled {
color: #8aa0b8;
}
.main-page--product .app-nav-solid,
.docs-page .product-topbar .app-nav-solid {
.main-page--product .app-nav-solid {
flex: 0 0 auto;
display: inline-flex;
gap: 4px;
@@ -3753,14 +3820,38 @@ button:disabled {
background: rgba(8, 16, 26, 0.7);
}
.main-page--product .app-nav-link,
.docs-page .product-topbar .app-nav-link {
.docs-page .product-topbar .app-nav-solid,
.docs-page .docs-header .app-nav-solid {
flex: 0 0 auto;
display: inline-flex;
gap: 4px;
padding: 3px;
border-radius: 10px;
border: 1px solid var(--docs-line);
background: #f2f6ff;
}
.main-page--product .app-nav-link {
min-height: 34px;
padding: 0 16px;
font-size: 13px;
border-radius: 8px;
}
.docs-page .product-topbar .app-nav-link,
.docs-page .docs-header .app-nav-link {
min-height: 34px;
padding: 0 16px;
font-size: 13px;
border-radius: 8px;
color: var(--docs-heading);
}
.docs-page .app-nav-link.active {
background: var(--docs-blue);
color: #ffffff;
}
.product-telemetry {
display: flex;
align-items: stretch;
@@ -3940,11 +4031,9 @@ button:disabled {
white-space: normal;
}
/* Documentation desktop grid aligned to product chrome */
/* Documentation desktop grid — keep light readable surface */
.docs-page {
background:
radial-gradient(circle at 10% 0%, rgba(56, 189, 248, 0.1), transparent 34%),
linear-gradient(180deg, #07111d 0%, #050910 100%);
background: var(--docs-bg);
}
.docs-layout {
@@ -3960,6 +4049,8 @@ button:disabled {
top: 88px;
padding: 14px 12px;
font-size: 0.84rem;
background: var(--docs-panel);
border-color: var(--docs-line);
}
.docs-main {