diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index ed88c27..9bd7ba3 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -36,7 +36,10 @@ export default defineConfig({ { text: 'ATLAS', link: 'https://atlas.sissa.it/' }, { text: 'ARGOS', link: 'https://argos.sissa.it/' }, ] }, - { text: 'Home', link: '/' }, + { text: 'Apps', items: [ + { text: 'To be added', link: '/' }, + { text: 'To be added 2', link: '/' }, + ] }, ], search: { provider: 'local', @@ -176,6 +179,24 @@ export default defineConfig({ ] }, ] + }, + { + text: 'Apps', + collapsed: false, + items: [ + { + text: 'marinAI', + collapsed: false, + items: [ + { text: 'Overview', link: '/apps/marinai/overview' }, + { text: 'Backend', link: '/apps/marinai/backend' }, + { text: 'API Reference', link: '/apps/marinai/api-reference' }, + { text: 'Frontend', link: '/apps/marinai/frontend' }, + { text: 'Deployment', link: '/apps/marinai/deployment' }, + { text: 'Glossary', link: '/apps/marinai/glossary' }, + ] + }, + ] } ], diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css index ff8cdd2..778c6b7 100644 --- a/docs/.vitepress/theme/custom.css +++ b/docs/.vitepress/theme/custom.css @@ -79,4 +79,196 @@ width: 0.95em; height: 0.95em; flex: 0 0 auto; +} + +/* ============================================================ + Home Hero — dramatic centered layout with a watermark logo + Scoped to .VPHomeHero / .VPHome so no other page is affected. + The long selector chains outrank VitePress' scoped hero styles. + ============================================================ */ + +/* Home page fills the viewport exactly — no vertical scroll */ +.VPHome { + margin-bottom: 0 !important; +} + +/* Tall, vertically centered hero */ +.VPHome .VPHomeHero.VPHero { + position: relative; + display: flex; + align-items: center; + min-height: 100vh; + padding-top: calc(var(--vp-nav-height) + 48px); + padding-bottom: 64px; +} + +.VPHome .VPHomeHero.VPHero.has-image .container { + position: static; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + text-align: center; +} + +/* Watermark logo — absolutely fills the hero, faint, behind the content */ +.VPHome .VPHomeHero.VPHero.has-image .image { + position: absolute; + inset: 0; + z-index: 0; + order: 0; + flex-grow: 0; + display: flex; + align-items: center; + justify-content: center; + margin: 0; + min-height: 0; + opacity: 0.16; + pointer-events: none; + transform: none; +} + +.dark .VPHome .VPHomeHero.VPHero.has-image .image { + opacity: 0.22; +} + +.VPHome .VPHomeHero.VPHero.has-image .image-container { + width: min(82vw, 800px); + height: min(82vw, 800px); + transform: none; +} + +.VPHome .VPHomeHero.VPHero.has-image .image-bg { + display: none; +} + +.VPHome .VPHomeHero.VPHero.has-image .image-src { + max-width: 100%; + max-height: 100%; +} + +/* Content sits above the watermark */ +.VPHome .VPHomeHero.VPHero.has-image .main { + position: relative; + z-index: 1; + order: 1; + width: 100%; + max-width: 920px; + margin: 0 auto; + align-items: center; + text-align: center; +} + +/* Hero brand logo (image) replaces the old text title */ +.VPHome .VPHomeHero.VPHero.has-image .name { + max-width: none; + margin: 0 auto; + line-height: 0; + text-align: center; +} + +.VPHome .VPHomeHero.VPHero.has-image .name .hero-logo-text { + display: block; + width: clamp(300px, 66vw, 720px); + height: auto; + margin: 0 auto; +} + +/* The brand SVG has black text; flip it to white (keeping brand orange) + when the dark theme is active so it stays readable. */ +.dark .VPHome .VPHomeHero.VPHero.has-image .name .hero-logo-text { + filter: invert(1) hue-rotate(180deg); +} + +/* Subtitle: "Documentation & Standards" — smaller than the logo */ +.VPHome .VPHomeHero.VPHero.has-image .text { + max-width: none; + margin: 10px auto 0; + font-size: clamp(1.25rem, 3.4vw, 2.2rem); + line-height: 1.25; + letter-spacing: 0.02em; + font-weight: 600; + text-align: center; +} + +/* Tagline */ +.VPHome .VPHomeHero.VPHero.has-image .tagline { + max-width: 720px; + margin: 0 auto; + padding-top: 28px; + font-size: clamp(1.1rem, 2.7vw, 1.7rem); + line-height: 1.5; + text-align: center; +} + +/* Big, centered call-to-action buttons */ +.VPHome .VPHomeHero.VPHero.has-image .actions { + justify-content: center; + padding-top: 44px; + gap: 6px; +} + +/* Icons used inside the hero's call-to-action buttons */ +:root { + --hero-icon-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14'/%3E%3Cpath d='m13 6 6 6-6 6'/%3E%3C/svg%3E"); + --hero-icon-github: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='black'%3E%3Cpath d='M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z'/%3E%3C/svg%3E"); +} + +.VPHome .VPHomeHero.VPHero.has-image .VPButton { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.55em; + border-radius: 999px; + padding: 0 48px; + line-height: 68px; + font-size: 1.28rem; + font-weight: 600; +} + +.VPHome .VPHomeHero.VPHero.has-image .VPButton::before { + content: ""; + display: block; + flex: 0 0 auto; + width: 1.25em; + height: 1.25em; + background-color: currentColor; + -webkit-mask-image: var(--hero-icon); + mask-image: var(--hero-icon); + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; +} + +.VPHome .VPHomeHero.VPHero.has-image .VPButton.brand { + --hero-icon: var(--hero-icon-arrow); +} + +.VPHome .VPHomeHero.VPHero.has-image .VPButton.alt { + --hero-icon: var(--hero-icon-github); +} + +/* Responsive */ +@media (max-width: 640px) { + .VPHome .VPHomeHero.VPHero { + padding-top: calc(var(--vp-nav-height) + 32px); + } + + .VPHome .VPHomeHero.VPHero.has-image .image-container { + width: 88vw; + height: 88vw; + } + + .VPHome .VPHomeHero.VPHero.has-image .actions { + padding-top: 32px; + } + + .VPHome .VPHomeHero.VPHero.has-image .VPButton { + padding: 0 30px; + line-height: 56px; + font-size: 1.05rem; + } } \ No newline at end of file diff --git a/docs/apps/marinai/api-reference.md b/docs/apps/marinai/api-reference.md new file mode 100644 index 0000000..319e6b3 --- /dev/null +++ b/docs/apps/marinai/api-reference.md @@ -0,0 +1,340 @@ +--- +outline: deep +--- + +# marinAI — API Reference + +All business endpoints live under `/v1` and require Keycloak authentication. The only public endpoint is the health check. Request and response shapes are defined by the [backend data model](./backend#data-model-pydantic). + +## API Reference (v1) + +All business endpoints require authentication. The only public endpoint is the health check. + +### Health + +```text +GET /v1/ +``` + +Response: + +```json +{ + "message": "Parametric Propeller API - running", + "supported_versions": ["/v1"] +} +``` + +### Default curves + +```text +GET /v1/curves/init +``` + +Returns an object with six entries: + +```json +{ + "chord": { + "control": [{"x": 0.25, "y": 0.03}], + "curve": [{"x": 0.25, "y": 0.03}] + } +} +``` + +The frontend uses this both on initial load and for reset. + +The response model also permits optional `num_blades` and `diameter` metadata in the frontend type, but the current backend response does not provide them. The frontend therefore retains its existing blade-count and diameter values after reset. + +### Curve upload and fitting + +```text +POST /v1/curves/upload +Content-Type: multipart/form-data +``` + +Fields: + +- `csv_file`: required project-table CSV +- `cp_file`: optional control-point CSV + +Behavior: + +- Rejects a missing or non-`.csv` project file with 400. +- Rejects a non-`.csv` control-point file with 400. +- Uses uploaded control points directly where supplied. +- Fits cubic Bézier curves elsewhere. +- Converts processing failures into HTTP 500. +- Returns radii, raw data, fitted control points, interpolated curves, and RMSE values. + +### Curve recomputation + +```text +POST /v1/curves/{dataset} +Content-Type: application/json +``` + +Request: + +```json +{ + "controlPoints": [ + {"x": 0.25, "y": 0.03} + ] +} +``` + +Response: + +```json +{ + "curvePoints": [ + {"x": 0.25, "y": 0.03} + ] +} +``` + +The `{dataset}` path variable is validated against `PLOT_KEYS`: `chord`, `pitch`, `rake`, `skew`, `camber`, `thickness`. An unknown name returns HTTP 404 with the list of valid parameters, so `/v1/curves/anything` no longer behaves like `/v1/curves/chord`. The interpolation itself is the same regardless of dataset (the math only depends on the control points). + +### CSV-to-IGES propeller generation + +```text +POST /v1/propeller/iges +Content-Type: multipart/form-data +``` + +Fields: + +- `csv_file`: required global-properties CSV +- `start_r`: default `0.25` +- `end_r`: default `1.0` +- `filename`: default `blade.iges` + +Validation: + +- CSV filename is required and must end in `.csv`. +- Radial bounds must satisfy `0.0 <= start_r < end_r`. +- Output names without `.igs` or `.iges` receive `.iges`. + +`InvalidGeometryError` is allowed to propagate to the 422 handler. Other failures become HTTP 500. + +### Parametric STL generation + +```text +POST /v1/propeller/stl +Content-Type: application/json +``` + +The request is a full `ParametricInput` object. The endpoint always uses the backend’s fixed 12-station `RADII`, not the client’s `n_sections`. + +Successful response: + +```json +{ + "stl_url": "/v1/exports/.stl" +} +``` + +### Parametric IGES generation + +```text +POST /v1/propeller/parametric/iges +Content-Type: application/json +``` + +The request is again a full `ParametricInput` object. Successful response: + +```json +{ + "iges_url": "/v1/exports/.iges" +} +``` + +The backend README endpoint table does not yet list this newer endpoint. + +### Export download + +```text +GET /v1/exports/{filename} +``` + +The endpoint serves STL, IGES/IGS, or generic files from `EXPORT_DIR` (default `/tmp/exports`). Missing files return 404. + +### Section table + +```text +POST /v1/sections +Content-Type: application/json +``` + +The request is a `ParametricInput` object. The response depends on `include_control_points`: + +- `false`: CSV named `tabella_progetto.csv` +- `true`: ZIP named `tabella_progetto.zip`, containing: + - `tabella_progetto.csv` + - `control_points.csv` + +The section table uses a dynamically generated `np.linspace(start_r, end_r, n_sections)` range, unlike the fixed STL radii. + +Control-point CSV columns are: + +```text +parameter,cp_index,cp_x,cp_y +``` + +### Three-dimensional section points + +```text +POST /v1/sections/points3d +Content-Type: application/json +``` + +The endpoint: + +1. Builds the parametric blade. +2. Calls `apply_transformations`. +3. Writes upper and lower points for each section. +4. Uses the corrected true section radius in the first column. + +CSV columns are: + +```text +radius,side,index,x,y,z +``` + +`side` is either `upper` or `lower`. Coordinate rows are emitted tip-first because BladeX stores them tip-to-hub. + +### Error behavior + +| Situation | Current HTTP status | +|---|---| +| Missing bearer credentials | 403 from FastAPI’s `HTTPBearer` behavior | +| Invalid/expired bearer token | 401 | +| Token without `sub` | 401 | +| Pydantic request validation failure | 422 | +| Negative camber/thickness geometry | 422 | +| Missing export file | 404 | +| Unknown `{dataset}` on curve update | 404 | +| Invalid upload extension or radial bounds | 400 | +| Unhandled CSV or geometry-processing failure | 500 | + +The distinction between a missing token and an invalid token matters. The frontend’s retry logic is designed around receiving 401 for supplied-token problems. + +## Authentication (Keycloak) + +### Configuration + +The backend reads these variables from the process environment or local `.env`: + +| Variable | Purpose | +|---|---| +| `KEYCLOAK_URL` | Keycloak server base URL | +| `KEYCLOAK_REALM` | Realm issuing access tokens | +| `KEYCLOAK_CLIENT_ID` | Client ID used only when audience verification is enabled | +| `KEYCLOAK_VERIFY_AUDIENCE` | `true` or `false`; default `false` | + +A local `.env` file exists but is ignored by Git. Only `.env.example` is tracked. The example supplies no credential values. + +Import-time behavior is strict: + +- Missing `KEYCLOAK_URL` or `KEYCLOAK_REALM` raises `RuntimeError`. +- `KEYCLOAK_VERIFY_AUDIENCE=true` without `KEYCLOAK_CLIENT_ID` raises `RuntimeError`. + +This means the backend intentionally refuses to start without usable Keycloak configuration. + +### Verification model + +`core/auth/deps.py`: + +1. Loads dotenv configuration. +2. Builds the issuer as `{KEYCLOAK_URL}/realms/{KEYCLOAK_REALM}`. +3. Builds the JWKS URL as `{issuer}/protocol/openid-connect/certs`. +4. Uses FastAPI `HTTPBearer`. +5. Uses PyJWT `PyJWKClient` with cached keys and a one-hour lifespan. +6. Decodes bearer tokens using RS256. +7. Verifies issuer always. +8. Verifies audience only when explicitly enabled. +9. Requires a `sub` claim. +10. Returns a typed `UserPayload`. +11. Converts other token problems to HTTP 401. + +`UserPayload` permits unknown extra claims through Pydantic `extra="allow"`, while requiring `sub` and `exp` and optionally carrying `preferred_username`, `email`, and `aud`. + +Router-level dependencies protect: + +- `routers/v1/curves.py` +- `routers/v1/exports.py` +- `routers/v1/sections.py` + +Only `routers/v1/health.py` remains public. + +The backend never needs `KEYCLOAK_CLIENT_SECRET`. It validates presented access tokens using public realm keys; it does not exchange authorization codes or refresh tokens. + +## CSV Formats + +### Project-table CSV + +Expected headers: + +```text +Radii (adim),Chord lengths,Skew angles,Pitch,Rake,Camber,Thickness +``` + +Sample project tables include Fincantieri-style and GBI-style datasets. The parser expects exact header strings and converts every field to floating-point numbers. + +### Control-point CSV + +Expected headers: + +```text +parameter,cp_index,cp_x,cp_y +``` + +Unknown parameters are ignored. Indexes are sorted per parameter. If a property has override points, its RMSE is reported as zero. + +### Exported section CSV + +Generated headers are: + +```text +Radii (adim),Chord lengths,Skew angles,Pitch,Rake,Camber,Thickness +``` + +Values are formatted with `%.15g`. + +### Profile-coordinate CSV + +`sections_properties.csv` contains literal Python-style coordinate arrays in quoted fields: + +```text +Radii (adim),X upper coordinates,Y upper coordinates,X lower coordinates,Y lower coordinates +``` + +The legacy reader (`read_sections_properties`) parses those fields with `ast.literal_eval`, scales them by chord, constructs custom profiles, and normalizes chord length. This format is offline-only; the live API does not accept it. + +### Global-properties sample caveat + +The checked-in `data/csv/global_properties.csv` currently contains only: + +```text +Radii (adim),Chord lengths,Pitch,Rake,Skew angles +``` + +It does **not** contain the `Camber` and `Thickness` columns required by `read_global_properties` / `read_project_csv`. Consequently, that specific sample cannot be used directly with the CSV-to-IGES global-properties reader or the project-curve fitter in its present form — use `tabella_progetto_FC.csv` or `tabella_progetto_GBI.csv` (canonical 7-column headers) instead. + +### Control-point JSON format + +Offline control-point JSON (`save/load_control_points`) uses keys such as: + +```text +chord_x +chord_y +chord_mapping +pitch_x +pitch_y +... +``` + +Mappings identify composite Bézier segments. C1 continuity across segments is not automatic and must be enforced manually. + +`load_control_points` currently expects all six standard properties to be present. A JSON file missing one of those keys raises a key error. The live `POST /v1/curves/{dataset}` API uses `CurveRequest{controlPoints}` with no mapping field — composite mappings are offline-only. diff --git a/docs/apps/marinai/backend.md b/docs/apps/marinai/backend.md new file mode 100644 index 0000000..a29314d --- /dev/null +++ b/docs/apps/marinai/backend.md @@ -0,0 +1,333 @@ +--- +outline: deep +--- + +# marinAI — Backend + +The marinAI backend is a FastAPI application ("Parametric Propeller API"). It owns all engineering math and CAD generation: Bézier curve interpolation and fitting, parametric blade construction with BladeX, and STL/IGES export with pythonOCC. The [API reference](./api-reference) documents every endpoint; [deployment](./deployment) covers running it. + +## Overview + +The backend is a FastAPI application named **“Parametric Propeller API.”** + +Its main responsibilities are: + +- Supply default Bézier control points and curves. +- Recompute a curve when control points are edited. +- Fit cubic Bézier curves to an uploaded propeller table. +- Optionally use an uploaded control-point table directly. +- Build parametric blade geometry. +- Generate STL and IGES propeller artifacts. +- Serve previously generated export files. +- Generate section-property CSV and 3D section-point CSV outputs. +- Validate geometry and return client errors for physically invalid input. +- Require Keycloak authentication for all business endpoints. + +The public health endpoint is intentionally left unauthenticated. + +## Tech Stack & Environment + +The environment is defined by `marinai_amd.yml` (pinned, x86_64) and `marinai_arm.yml` (minimal, ARM64), using conda environment `marinai_env` and Python 3.10. The old single `marinai.yml` no longer exists; the backend README still references it and is stale on this point. Docker builds select the file via the `CONDA_ENV_FILE` build arg (see [Run, Test, Deploy](./deployment#run-test-deploy)). + +| Layer | Important packages | +|---|---| +| Web framework | FastAPI 0.116.1, Starlette 0.47.3, Uvicorn 0.35.0 | +| Request validation | Pydantic 2.11.9, Pydantic Core 2.33.2 | +| File uploads | python-multipart 0.0.20 | +| Authentication | PyJWT with cryptography support 2.10.1, python-dotenv 1.0.1 | +| Numerical calculation | NumPy 2.2.2, SciPy 1.15.1 (pinned in `_amd`; floating in `_arm`) | +| Plotting/offline diagnostics | Matplotlib 3.10.0 | +| CAD geometry | pythonOCC-core 7.8.1.1 (conda) + OCCT 7.8.1 (`_amd` pinned; `_arm` floating `pythonocc-core`) | +| VTK-related conda packages | VTK 9.2.6, including VTK IO FFmpeg packages (`_amd` only; `_arm` omits VTK) | +| Blade modeling | BladeX, installed separately from its Git repository (both env files) | + +The repository pins PyJWT with cryptography support because authentication depends on `PyJWKClient` and RS256 verification. + +An important environment hazard is the unrelated legacy PyPI package named `jwt`, especially version 1. Although neither `marinai_amd.yml` nor `marinai_arm.yml` lists it, that package uses the same top-level `jwt` module name as PyJWT and can shadow PyJWT. If installed, `from jwt import PyJWKClient` can fail and prevent application startup. The repository README now warns against installing it. + +## Directory Structure + +```text +marinAI-backend/ +├── app.py +├── Dockerfile # Micromamba build; ARG CONDA_ENV_FILE selects _amd/_arm +├── marinai_amd.yml # Pinned x86_64 env (python 3.10.13, OCC/VTK/numpy/fastapi stack) +├── marinai_arm.yml # Minimal floating ARM64 env (python=3.10, no VTK/scipy/pyjwt pins) +├── .env.example +├── .gitignore +├── README.md # Stale: still references marinai.yml, test.py/test_api.sh, check_env +├── CHANGELOG.md # Stale: stops before Keycloak/sections/UUID/validation/Docker work +├── package-lock.json # Nearly empty stub; this is not a Node project +├── .github/workflows/ # image_amd64_creation (tags v*-amd), image_arm64_creation, pr_tester (Trivy) +├── core/ +│ ├── auth/ +│ │ ├── deps.py # Keycloak bearer-token dependency +│ │ └── schemas.py # Typed JWT payload +│ ├── exceptions.py # InvalidGeometryError +│ ├── schemas/ +│ │ ├── models.py # API data models and validation +│ │ └── constants.py # Parameter keys, defaults, radii +│ ├── services/ +│ │ ├── blade_builder.py +│ │ ├── curves.py +│ │ ├── exporter.py +│ │ └── fitting.py +│ └── utils/ +│ ├── domain/ +│ │ ├── bezier.py +│ │ └── blade_geometry.py +│ ├── io/ +│ │ ├── csv_reader.py +│ │ └── control_points.py +│ └── plotting/ +│ └── bezier_plots.py +├── routers/ +│ └── v1/ +│ ├── health.py +│ ├── curves.py +│ ├── exports.py +│ └── sections.py +├── scripts/ +│ ├── build_bezier.py +│ ├── build_bezier_interactive.py +│ └── plot_default.py +└── data/ + ├── csv/ + ├── iges/ + └── marinAI_Logo.png +``` + +## Data Model (Pydantic) + +### Six radial properties + +Each blade is described by six scalar properties sampled along normalized radius, `r/R`. + +| Property | Frontend/UI meaning | Backend key | +|---|---|---| +| Chord | Blade width/area distribution, normalized as `c/D` | `chord` | +| Pitch | Axial advance per revolution, normalized as `P/D`; affects thrust and angle of attack | `pitch` | +| Rake | Axial section position, normalized as `z/D`; affects root stress and cavitation behavior | `rake` | +| Skew | Angular section offset in degrees; spreads load and can reduce noise/vibration | `skew` | +| Camber | Mean-line curvature relative to chord, `f/c`; affects sectional lift | `camber` | +| Thickness | Maximum section thickness relative to chord, `t/c`; affects strength and cavitation | `thickness` | + +In the API model, each property is a list of `{x, y}` control points: + +- `x` is normally a normalized radial station. +- `y` is the property value at that station. + +Pydantic accepts variable-length point lists. The backend does not require all six properties to have the same number of control points. + +### Canonical constants + +Important backend constants live in `core/schemas/constants.py`. + +- `PLOT_KEYS`: `chord`, `pitch`, `rake`, `skew`, `camber`, `thickness` +- `N_CURVE_POINTS`: `100` +- Default 12-station `RADII`: + - `0.25` + - `0.35` + - `0.4` + - `0.5` + - `0.6` + - `0.7` + - `0.8` + - `0.85` + - `0.9` + - `0.95` + - `0.975` + - `1.0` + +The fixed `RADII` array is used for STL and parametric IGES generation. Section-table endpoints instead use a user-controlled number of linearly spaced stations between `start_r` and `end_r`. + +The default starting design has four control points per property: + +- Skew ranges from `-10` to `11`, so negative values are legitimate for skew. +- Camber defaults to zero at all four control points. +- Thickness declines from root to tip. +- Chord, pitch, and rake have smooth nonzero defaults. + +`INITIAL_POINTS` is backend state and the frontend’s `/curves/init` reset source. + +### `ParametricInput` validation + +`ParametricInput` contains: + +- Six control-point lists +- `start_r`, default `0.25` +- `end_r`, default `1.0` +- `n_sections`, default `12` +- `n_blades`, default `5` +- `filename`, default `blade.iges` +- `include_control_points`, default `false` + +The current validation rules are: + +- `start_r >= 0` +- `start_r < end_r` +- `n_sections >= 2` +- `3 <= n_blades <= 7` +- Every camber control point must satisfy `y >= 0` +- Every thickness control point must satisfy `y >= 0` + +The camber/thickness rule is important. A Bézier curve remains inside the convex hull of its control points, so nonnegative camber and thickness control points prevent their interpolated properties from becoming negative. This turns a previously common invalid-geometry failure into an early FastAPI 422 validation error. + +Validation does not prohibit negative chord, pitch, rake, or skew control points. Skew, in particular, can legitimately be negative. The UI clamps dragged x values to `[0.25, 1.0]` and clamps camber/thickness y to `>= 0` in `DraggablePlot`, but it still permits nonmonotonic x ordering; `np.interp` assumes suitable x ordering, so malformed or highly nonmonotonic control-point x values remain a potential source of incorrect interpolation rather than a guaranteed validation error. + +## The Math: Bézier Curves + +### General Bézier implementation + +`core/utils/domain/bezier.py` implements Bézier curves of arbitrary order using Bernstein polynomials. + +For control points \(P_0 \dots P_n\) and parameter \(t\): + +```text +B(t) = sum(C(n,i) * (1-t)^(n-i) * t^i * P_i) +``` + +Important methods include: + +- `get_points(n_points)`: sample the curve. +- `evaluate(t)`: evaluate supplied parameter values. +- `plot(ax, ...)`: draw curves and annotate endpoints. +- `compute_property(radii)`: linearly interpolate sampled Bézier values onto requested radii. +- `from_curve(x, y, order)`: solve a least-squares system for interior control points while holding endpoints fixed. + +The production API does **not** always use cubic curves, even where surrounding comments say “cubic.” `interpolate_curve` constructs a Bézier whose order equals one less than the number of supplied control points. Thus: + +- Four points produce a cubic curve. +- More or fewer points produce a different-degree curve. +- Fewer than two points are returned unchanged. + +The CSV fitting path is specifically cubic, but the direct curve-update path is arbitrary-order interpolation. + +### CSV fitting + +`core/services/fitting.py` fits one cubic Bézier per property: + +1. Read `Radii (adim)` and the six project-table properties. +2. Fix the first control point to the first data point. +3. Fix the last control point to the last data point. +4. Parameterize the data chordwise from cumulative arc length. +5. Use `scipy.optimize.least_squares` to solve for the two interior control points. +6. Constrain interior-point x coordinates to remain within the data’s x range. +7. Sample 100 points along the fitted curve. +8. Return fitted control points, sampled curve points, raw data, and RMSE. + +If the caller supplies control points directly through the optional control-point CSV, the service skips fitting for those properties and reports `rmse: 0.0`. + +The fitter assumes the input rows are in a usable radial order. It does not explicitly sort or validate monotonic x values. + +### Composite Bézier support and caveats + +The codebase supports `CompositeBezier` objects for offline or experimental use, including: + +- Multiple segments +- Per-segment ranges +- A central control-point list +- A segment mapping +- JSON persistence with `{key}_x`, `{key}_y`, and optional `{key}_mapping` + +The live API curve path does not consume composite mappings. It treats supplied control points as one Bézier object. + +`CompositeBezier.from_curve` defaults `orders` to a cubic per segment when it is omitted: + +```python +if orders is None: + orders = [3] * (len(x_ranges) - 1) +``` + +An earlier version had `if orders == None: orders == [3]` (a comparison, not an assignment), which left `orders` as `None` and crashed any caller that did not pass it. Callers may still pass `orders` explicitly, and the offline interactive script does so. + +## Services + +### From control points to a `BladeX.Blade` + +`build_blade_from_input` performs these steps: + +1. Convert each property’s control points into a `Bezier`. +2. Evaluate all six properties at the requested radii. +3. Build airfoil sections from thickness and camber distributions. +4. Construct a `BladeX.Blade` from: + - Sections + - Radii + - Chord lengths + - Rake + - Pitch + - Skew angles + +The function returns two values; its docstring was updated to match: + +```text +blade, props +``` + +### Section construction + +`build_sections` uses a NACA baseline profile and reshapes it per radius using the requested maximum thickness and camber. + +Important details: + +- The API’s parametric path uses NACA baseline digits `'5407'`. +- `build_sections` itself defaults to `'2408'`. +- One offline script explicitly uses `'2408'` with constant 8% thickness and 2% camber. +- Values between `-1e-12` and zero are clamped to zero to absorb floating-point edge effects. +- Materially negative camber or thickness raises `InvalidGeometryError`. +- The final tip station becomes a zero-thickness degenerate plate. + +The Fincantieri-style `generate_radii` distribution concentrates stations near the tip using successively smaller intervals. It is used by legacy/global-property flows, not by the fixed 12-station STL path. + +### BladeX coordinate ordering and the section-radius fix + +BladeX’s cylindrical section generation iterates sections and radii in reverse. Consequently: + +```text +blade_coordinates_up[0] -> tip / largest radius +blade_coordinates_up[-1] -> root / smallest radius +``` + +The current `/v1/sections/points3d` implementation explicitly compensates by pairing coordinate index `idx` with `r[::-1][idx]`. Therefore the first `radius` column now contains the actual radial station for those coordinates. The offline `plot_default.py` script contains the same ordering assumption and reverses coordinate arrays before surface plotting. + +This replaces the older mirrored-label behavior in which coordinate blocks were labeled with the opposite end’s radius. The frontend parser was changed at the same time to consume the corrected first column directly. + +### STL and IGES generation + +The newest STL path no longer calls BladeX’s `Propeller.tmp_stl`; it uses a local OCC implementation that mirrors that behavior while exposing more meshing control. + +For STL: + +1. Build the parametric blade. +2. Add a cylindrical shaft with radius `250` and height `300`. +3. Construct a `Propeller` with the requested blade count. +4. Sew each blade’s upper, lower, root, and tip faces with a `1e-2` sewing tolerance. +5. Mesh with linear deflection `0.02` and angular deflection `0.3`. +6. Write a binary STL. +7. Copy it to `EXPORT_DIR/.stl`. +8. Return a relative `/v1/exports/.stl` URL. + +For parametric IGES: + +1. Build the same blade and propeller. +2. Sew each blade’s upper, lower, root, and tip faces with a `1e-2` tolerance and collect them with the shaft into a single compound, written with `IGESControl_Writer` (IGES is B-Rep, so no meshing). This mirrors the STL writer and deliberately avoids BladeX’s `Propeller.shape`/`Blade.generate_solid`, which sews at `1e-7` and hard-casts the result to a `TopoDS_Shell` — a geometry-dependent crash (`Standard_TypeMismatch`) whenever the faces do not close into one shell. +3. Copy it to `EXPORT_DIR/.iges`. +4. Return a relative `/v1/exports/.iges` URL. + +For CSV-based global IGES: + +1. Read global properties from an uploaded CSV (one value per table row). +2. Generate Fincantieri-style radii. +3. Fit one composite Bézier per property to the table and resample the properties onto the generated radii (mirroring the interactive editor’s global flow). Previously the raw per-row arrays were passed straight to `build_sections` alongside the longer generated radii, causing an `IndexError`. +4. Build sections and a `Blade`, then call `blade.build()` (transforms + OCC faces), rotate 180 degrees about z, and write the IGES with `blade.export_iges(..., include_le_curves=True)`. +5. Copy the artifact into `EXPORT_DIR` and return a `FileResponse` from there, so removing the temporary build directory cannot delete the file mid-stream. The download name is preserved. + +### Export-file handling + +Generated STL, parametric IGES, and CSV-to-IGES artifacts use UUID filenames under a local export directory. + +- The directory is configurable via `EXPORT_DIR` (default `/tmp/exports`), so deployments can point it at persistent storage. +- A best-effort retention sweep (`EXPORT_RETENTION_HOURS`, default `24`) runs before each new export and deletes older files, so the directory cannot grow without bound. +- The downloader sanitizes the requested filename with `os.path.basename`, preventing straightforward directory traversal. Missing files return 404. +- Downloads are authenticated but not scoped per user; anyone possessing a UUID URL can download that export (UUIDs are unguessable, but this remains a design caveat). +- With the default `/tmp` location, restarting or clearing `/tmp` still invalidates returned URLs — point `EXPORT_DIR` at persistent storage for durable links. diff --git a/docs/apps/marinai/deployment.md b/docs/apps/marinai/deployment.md new file mode 100644 index 0000000..1a6b054 --- /dev/null +++ b/docs/apps/marinai/deployment.md @@ -0,0 +1,108 @@ +--- +outline: deep +--- + +# marinAI — Deployment + +How to run marinAI locally, how the Docker/CI images are built, and the full backend + frontend configuration reference. + +## Run, Test, Deploy + +From the repository README (stale: it still says `marinai.yml`), the intended setup is: + +```bash +# x86_64 (pinned/reproducible) +conda env create -f marinai_amd.yml +# ARM64 (minimal/floating) +conda env create -f marinai_arm.yml +conda activate marinai_env +# BladeX is already in the _amd pip section; for _arm install it explicitly: +pip install git+https://github.com/mathLab/BladeX.git +``` + +Then copy `.env.example` to `.env`, populate Keycloak values, and run: + +```bash +uvicorn app:app --reload +``` + +The default local service is available at port 8000. FastAPI also exposes interactive documentation under `/docs` and `/redoc`. + +Operational notes: + +- CORS currently permits only local frontend origins: + - `http://localhost:5173` + - `http://127.0.0.1:5173` + - `http://localhost:3000` +- Credentials, methods, and headers are broadly allowed. +- Deployed frontend origins must be added before non-local browser use. +- `git diff --check` was clean for the reviewed backend tree. +- No dedicated backend test suite is present. + +Backend run checklist: + +1. Create and activate the conda environment from `marinai_amd.yml` (x86_64) or `marinai_arm.yml` (ARM64). +2. Install BladeX separately if using `_arm`. +3. Copy `.env.example` to ignored `.env`. +4. Populate Keycloak URL and realm. +5. Run Uvicorn from the repository root. +6. Use `/docs` for interactive API documentation. + +### Docker and CI Images + +- `Dockerfile` is a single micromamba 2.3.2 build: installs `git` + `libgl1`, installs `${CONDA_ENV_FILE}` into `base`, copies the repo, exposes 8000, runs `uvicorn app:app --host 0.0.0.0 --port 8000`. +- `.github/workflows/image_amd64_creation.yml` builds `linux/amd64` with `CONDA_ENV_FILE=marinai_amd.yml` on tags `v*-amd` (`:latest-amd` floating tag). +- `.github/workflows/image_arm64_creation.yml` builds `linux/arm64` with `CONDA_ENV_FILE=marinai_arm.yml`. +- `.github/workflows/pr_tester.yml` is Trivy scanning only — there is still no backend test suite. + +Local browser use requires one of the configured CORS origins. Production requires additional frontend origins and persistent export storage. + +## Scripts & Sample Data + +The `scripts/` directory contains development and legacy tools, not production API code. + +| Script | Purpose | Current caveat | +|---|---|---| +| `build_bezier.py` | Batch blade generation from control-point JSON | Expects ignored `blades/control_points_composite.json`; sample paths may not exist | +| `build_bezier_interactive.py` | Interactive matplotlib Bézier editor | GUI/profiling-oriented; supports global-property and control-point initialization | +| `plot_default.py` | Offline 2D/3D/default-propeller plots | Input construction fixed: it now copies the `Point` lists from `INITIAL_POINTS` instead of reconstructing them with `Point(**p)` | + +Both build scripts and the CSV→IGES service now use the current BladeX API (`blade.build()` to create the OCC faces, then `blade.export_iges(...)`); the older `apply_transformations()`/`generate_iges_blade()` calls no longer exist upstream. The unused `exporter.blade_to_compound` / `generate_stl_from_blade` helpers still reference the removed `_generate_upper_face` method and are dead code. + +The backend README’s script documentation is stale. It mentions a `test.py` and `test_api.sh` that are not present as described, references `python -m scripts.check_env` where `scripts/check_env.py` is absent, omits `POST /v1/propeller/parametric/iges`, and does not accurately describe the current scripts. Its JSON control-point example (7-point `chord_x/y/mapping`) also does not match the current 4-point-per-property `INITIAL_POINTS` with no mapping. + +## Configuration Reference + +### Backend + +| Variable | Required | Purpose | +|---|---|---| +| `KEYCLOAK_URL` | Yes | Keycloak server base URL | +| `KEYCLOAK_REALM` | Yes | Token-issuing realm | +| `KEYCLOAK_CLIENT_ID` | Only when audience verification is enabled | Expected audience | +| `KEYCLOAK_VERIFY_AUDIENCE` | No | Defaults to `false`; enables audience checking when `true` | + +The backend uses no Keycloak client secret. + +### Frontend + +The tracked `.env.example` currently defines: + +```text +BASE_PATH +NEXT_PUBLIC_BASE_PATH +NEXT_PUBLIC_API_URL +NEXT_PUBLIC_APP_VERSION +AUTH_SECRET +KEYCLOAK_URL +KEYCLOAK_REALM +KEYCLOAK_CLIENT_ID +KEYCLOAK_CLIENT_SECRET +POST_LOGOUT_REDIRECT_URL +``` + +It no longer contains the older external-portal (`NEXT_PUBLIC_AUTH_LOGIN_URL`) or cookie-domain variables. The example currently includes nonblank `NEXT_PUBLIC_API_URL=http://localhost:8000` and nonblank local-network `KEYCLOAK_URL=http://192.168.1.244:9090`, while leaving `BASE_PATH`, `NEXT_PUBLIC_BASE_PATH`, `AUTH_SECRET`, realm, client, secret, and redirect values blank. The ignored local `.env.local` file is not documented here. + +The frontend uses `KEYCLOAK_CLIENT_SECRET` only in server-side Auth.js provider and refresh-token operations. It must not be exposed as a public `NEXT_PUBLIC_` variable. + +`NEXT_PUBLIC_APP_VERSION` is included in `.env.example` (currently blank); it drives the version footer in the notifications popover and is used for release tagging. When unset, `page.tsx` falls back to `v${packageJson.version}` (`v1.0.0`). `BASE_PATH` and `NEXT_PUBLIC_BASE_PATH` support subpath deployment. Browser code reads `NEXT_PUBLIC_BASE_PATH` through `src/utils/basePath.ts` (`withBasePath()`), while `next.config.js` and the proxy use the non-public `BASE_PATH` for routing. Note the split: `Dockerfile` only bakes `NEXT_PUBLIC_API_URL` (+ unused `NEXT_PUBLIC_AUTH_URL`) via `API_URL`/`AUTH_URL` build args — Keycloak and base-path vars must be supplied at runtime, not baked in. diff --git a/docs/apps/marinai/frontend.md b/docs/apps/marinai/frontend.md new file mode 100644 index 0000000..ef63c55 --- /dev/null +++ b/docs/apps/marinai/frontend.md @@ -0,0 +1,471 @@ +--- +outline: deep +--- + +# marinAI — Frontend + +The marinAI frontend is a Next.js single-page propeller-design workspace. It never performs propeller mathematics or CAD generation itself: it orchestrates [backend](./backend) calls and visualizes their results through draggable Bézier panels, a 3D STL viewer, and a 2D sections view. + +## Overview + +The frontend is a Next.js single-page propeller-design workspace. + +Its responsibilities are: + +- Authenticate the user through Keycloak-backed Auth.js. +- Display six draggable Bézier parameter panels. +- Send curve edits to the backend. +- Render the generated propeller STL in 3D. +- Display derived 2D airfoil sections and section statistics. +- Support undo, redo, reset, CSV import, randomization, and exports. +- Save and recall named blade snapshots. +- Surface in-app release notes through the navbar notifications menu. +- Provide an onboarding tutorial. +- Attach fresh bearer tokens to backend requests. +- Download binary artifacts through authenticated browser Blob URLs. + +The frontend never performs propeller mathematics or CAD generation itself. It orchestrates backend calls and visualizes their results. + +## Tech Stack & Environment + +From `package.json`: + +| Layer | Important packages | +|---|---| +| Framework | Next.js ^16.2.9, React 19, React DOM 19 | +| UI | Material UI 9, Material Icons 9, Emotion 11 | +| Design system | `@fast-computing/fast-graphics` ^1.8.0 | +| Curve plots | plotly.js-dist-min ^3.3.0 and react-plotly.js ^2.6.0 bindings | +| 3D rendering | @kitware/vtk.js ^37.0.4 | +| Icons | lucide-react ^1.48.0 (navbar/sidebar/profile icons) | +| Release notes | react-markdown ^10.1.0 + remark-gfm ^4.0.1 (notification popover bodies) | +| Authentication | NextAuth/Auth.js 5 beta 32 (`next-auth` ^5.0.0-beta.32) | +| Fonts | @next/font ^14.2.15 (code imports `next/font/google`) | +| Language/tooling | TypeScript ^5.6.3, Webpack ^5.97.1 + css-loader/style-loader/ts-loader | + +The frontend README's theme-installation instruction says to install fast-graphics 1.6.1 (`npm install @fast-computing/fast-graphics@1.6.1`), while `package.json` requires `^1.8.0`. That version mismatch is another stale-documentation issue. The README's `create-tag.yml` / `NEXT_PUBLIC_APP_VERSION` workflow reference is also stale (no such workflow in the repo). + +The configured npm scripts are: + +```text +npm run dev +npm run build +npm run start +npm run lint +``` + +Frontend operation: + +1. Install dependencies with `npm ci`. +2. Provide an npm registry token for the private FAST graphics package if required. +3. Configure the ignored local environment file. +4. Ensure the backend is reachable at `NEXT_PUBLIC_API_URL`. +5. Run the development server and open the configured Next.js port. + +The checked-in frontend README documents `NEXT_PUBLIC_API_URL` + `NEXT_PUBLIC_APP_VERSION` (default `http://localhost:8000`, example `v0.1.0`) plus the `NPM_TOKEN`/`.npmrc` theme install. It does not cover the current Keycloak, session-secret, base-path, logout-redirect, or release-tag requirements. + +## Directory Structure + +Important current paths include: + +```text +marinAI-frontend/ +├── src/app/ +│ ├── layout.tsx +│ ├── page.tsx # Composes Sidebar + Navbar + SnapshotRail + StlViewer/SectionsView +│ ├── globals.css +│ ├── login/route.ts # Internal login entry point (GET -> 302 Keycloak URL) +│ └── api/ +│ ├── auth/[...nextauth]/route.ts # NextAuth handlers (GET+POST) +│ ├── logout/route.ts +│ ├── session-token/route.ts +│ └── user/route.ts # Profile endpoint (name/email/username only) +├── src/ +│ ├── auth.ts +│ ├── proxy.ts # Route guard (protects only /); no middleware.ts +│ ├── constants/ +│ │ ├── propeller.ts # datasets, panel sizes, MAX_HISTORY=50, SNAPSHOT_SLOTS=5, NAVBAR_HEIGHT=64, SIDEBAR_RIGHT_EDGE=116 +│ │ └── tutorial.ts # 9-step TOUR_STEPS + STORAGE_KEY +│ ├── hooks/ +│ │ ├── usePropellerData.ts # init/curves/upload (debounce 300ms) +│ │ ├── useSectionsData.ts # parallel points3d + tabella refresh +│ │ ├── useHistory.ts # undo/redo stack (cap 50) +│ │ ├── useSnapshots.ts # session-only named blade snapshots (BladeSnapshot + WebP preview) +│ │ ├── useNotifications.ts # loads/parses public/notifications.md, tracks seen ids +│ │ ├── usePanelInteraction.ts +│ │ └── useDebouncedCallback.ts +│ ├── types/ +│ │ ├── propeller.ts # PlotKey, AllPoints/Curves, HistorySnapshot, BladeSnapshot, PanelStates +│ │ ├── notifications.ts # NotificationTag + Notification +│ │ ├── sections.ts +│ │ ├── next-auth.d.ts +│ │ └── vtk.d.ts +│ ├── utils/ +│ │ ├── auth.ts # token cache + authorizedFetch +│ │ ├── basePath.ts # NEXT_PUBLIC_BASE_PATH + withBasePath() helper +│ │ ├── notifications.ts # parseNotifications() markdown parser +│ │ ├── points.ts # clone/bounds/randomize +│ │ ├── parseSections.ts # points3d + tabella parsing/merging +│ │ └── layout.ts +│ ├── env.d.ts +│ └── main.tsx # Legacy Vite entry, unused by App Router (excluded in tsconfig) +├── components/ +│ ├── Navbar.tsx # Fixed 64px header: title, undo/redo, render, 3D/sections toggle, +│ │ # color, blades stepper, import/export, guide, NotificationsMenu, ProfileMenu +│ ├── ProfileMenu.tsx # Account popover (GET /api/user + logout) +│ ├── NotificationsMenu.tsx # Bell button + release-notes popover (unread badge, markdown body) +│ ├── Sidebar.tsx # Left dock: 6 parameter toggles + reset/randomize tab +│ ├── SnapshotRail.tsx # Bottom-center snapshot slots (1-8, default 5, session-only, named) +│ ├── SnapshotNameDialog.tsx # Asks for a label before saving a snapshot slot +│ ├── PlotPanel.tsx +│ ├── DraggablePlot.tsx +│ ├── StlViewer.tsx # vtk.js viewer + capturePreview() for snapshots +│ ├── SectionsView.tsx +│ ├── Tutorial.tsx # FastTour wrapper (9 steps) +│ ├── CsvImportDialog.tsx +│ ├── SectionDownloadDialog.tsx +│ └── SpinningPropeller.tsx +├── types/plotdata.ts # Legacy PlotDataDict, unused +├── public/ # marinai-icon.png, fast logos, marinAI_Logo.png, icons/{6 params}.png, +│ # notifications.md (release-notes source for NotificationsMenu) +├── Dockerfile # 3-stage node:22-alpine (deps/build/runner, standalone output) +├── .trivyignore.yaml # License exceptions for sharp transitive binaries +├── next.config.js # standalone + BASE_PATH (non-public) + styledComponents +├── package.json +├── tsconfig.json # excludes src/main.tsx +├── tsconfig.tsbuildinfo # Tracked generated-file state +├── AGENTS.md # Boilerplate only, no project rules +├── CLAUDE.md +├── CHANGELOG.md # Stale: stops before auth/proxy/sections/snapshot work +└── .env.example +``` + +There is no `components/Toolbar.tsx`, `components/BottomRightOverlay.tsx`, `components/TopRightControls.tsx`, or `components/ViewModeToggle.tsx` — those roles were consolidated into `Navbar.tsx` (+ `ProfileMenu.tsx`, `NotificationsMenu.tsx`) and `SnapshotRail.tsx` (+ `SnapshotNameDialog.tsx`). There is no `src/middleware.ts` or root `middleware.ts`; the guard lives in `src/proxy.ts`. + +`src/main.tsx` is legacy. It mounts a nonexistent `./pages/App`, but the actual application uses the Next.js App Router through `src/app/page.tsx`. It is explicitly excluded in `tsconfig.json` (`exclude: ["node_modules","src/main.tsx"]`) and should eventually be removed to avoid confusion. + +## Authentication & SSO Flow + +The current working tree has moved toward the frontend acting as its own OIDC client rather than relying on an external portal. + +### Login + +The internal route `src/app/login/route.ts`: + +1. Reads an optional `callbackUrl`, defaulting to `/`. +2. Calls `signIn("keycloak", { redirectTo: callbackUrl, redirect: false })`. +3. Receives the generated Keycloak authorization URL. +4. Returns HTTP 302 with that URL in `Location`. + +The route guard in `src/proxy.ts` protects only `/` (plus `//` edge) and redirects unauthenticated/errored requests to the internal `/login` route while preserving the requested callback URL. Other paths pass through; the matcher skips `api`, `_next/static`, `_next/image`, `favicon.ico`, `images`, and `videos`. + +### NextAuth session + +`src/auth.ts` configures: + +- Keycloak provider +- JWT session strategy +- Host trust +- Access-token, refresh-token, expiration, ID-token, name, email, and username propagation +- Session exposure through `accessToken`, `idToken`, `error`, and `username` +- Token refresh beginning 60 seconds before expiration +- Deduplicated concurrent refresh attempts +- Self-healing behavior for transient token-endpoint failures +- Permanent `RefreshAccessTokenError` only for unrecoverable refresh failures such as invalid grants or expired refresh tokens + +The `src/app/api/auth/[...nextauth]/route.ts` exposes the generated GET and POST handlers. + +### Session-token endpoint + +`src/app/api/session-token/route.ts` returns either: + +```json +{ + "accessToken": "", + "reason": null +} +``` + +or HTTP 401 with: + +```json +{ + "accessToken": null, + "reason": "no_session" +} +``` + +or: + +```json +{ + "accessToken": null, + "reason": "refresh_failed" +} +``` + +This distinction lets the client decide whether to retry or redirect to login. + +### Profile endpoint + +The `src/app/api/user/route.ts` returns only identity information: + +```json +{ + "name": "...", + "email": "...", + "username": "..." +} +``` + +It deliberately avoids exposing tokens merely to display a user profile. + +### Logout + +The current logout route: + +1. Reads the Auth.js session before clearing cookies. +2. Uses the session ID token for Keycloak RP-initiated logout when available. +3. Falls back to a configured post-logout URL or the application root. +4. Clears every matching Auth.js session-cookie variant, including chunked variants. +5. Preserves the `secure` cookie attribute behavior for `__Secure-` and `__Host-` cookies. +6. Supports both GET and POST. + +Without RP-initiated logout, Keycloak could retain an SSO session and silently log the user back in, so the ID-token flow is significant. + +### Browser token cache and retry + +`src/utils/auth.ts` implements an in-memory access-token cache with: + +- Expiry decoding from the JWT payload. +- A 60-second refresh window matching the server callback. +- Single-flight `/api/session-token` requests. +- Limited retries for network or 5xx failures. +- Fallback to the last known token so transient failures become backend 401s rather than tokenless 403s. +- `clearCachedToken` after a backend 401. +- Exactly one retry with a refreshed token. +- A guarded single redirect to login when renewal has failed. + +`authorizedFetch` is now the standard backend-call wrapper. An older `authHeader` helper remains exported but is no longer used by the main page or data hooks. + +## State, Hooks & Data Flow + +### Page-level state + +`src/app/page.tsx` owns the main application state, including: + +- Control points for all six properties +- Interpolated curves +- STL object URL +- Blade count, defaulting to five (clamped 3–7 in page and navbar) +- Diameter display, defaulting to one meter +- Editable project title, defaulting to `Untitled project` +- Three-dimensional versus sections view (`sectionsN` default 25; 3D-points download uses fixed `n_sections=12`) +- Section counts and export-dialog state (`sectionNValue` default 12) +- CSV-import dialog state +- Rendering and IGES-export indicators +- Snapshot slots (`useSnapshots(SNAPSHOT_SLOTS=5)`, `savingIndex` while capturing, optional user label) +- Tutorial replay state +- Snackbar error state +- Model color +- Panel drag clamp `SIDEBAR_RIGHT_EDGE=116` keeps floating panels clear of the left dock + +The page composes specialized hooks rather than implementing every workflow inline. + +### Propeller data hook + +`usePropellerData`: + +- Fetches `/v1/curves/init` on startup. +- Splits each property’s `control` and `curve` data. +- Updates one backend curve after local control-point edits. +- Debounces curve updates by 300 milliseconds. +- Imports project and optional control-point CSV files. +- Exposes `applyAllPoints` for replacing every property simultaneously. +- Stores raw imported CSV points separately. +- Reports errors through the page-level snackbar. + +The backend currently does not return `num_blades` or `diameter` metadata from `/curves/init`, so those frontend fields remain unchanged during reset. + +### Sections-data hook + +`useSectionsData`: + +- Sends the same request body to: + - `/v1/sections/points3d` + - `/v1/sections` +- Parses both CSV responses. +- Merges geometry and tabular statistics by radius. +- Supports silent background refresh after rendering. +- Maintains separate loading and refreshing indicators. + +Because both responses are generated from the same request body, their station counts and radius ranges stay consistent. + +### History, panels, and debouncing + +`useHistory`: + +- Snapshots control points and curves. +- Supports undo and redo. +- Resets on CSV import. +- Caps history at 50 snapshots (`MAX_HISTORY=50` in `src/constants/propeller.ts`). + +`useSnapshots`: + +- Session-only in-memory blade snapshots (`BladeSnapshot{points, curves, preview, name?, savedAt}`). +- Starts with `SNAPSHOT_SLOTS=5` slots; the rail allows 1–8 via add/remove (minimum 1). +- Previews are WebP data URLs from `StlViewer.capturePreview()` (256 px wide); a reload clears them (kept out of localStorage). +- Saving opens `SnapshotNameDialog` for an optional label; the label shows as the slot tooltip and falls back to the slot number. + +`usePanelInteraction`: + +- Tracks panel visibility, position, z-order, and control-point-editor state. +- Supports dragging floating panels. +- Anchors right-side panels on mount. +- Integrates with the tutorial’s temporary panel behavior. + +`useDebouncedCallback`: + +- Prevents every pointer movement from issuing a backend request. +- Always invokes the latest callback after the delay. + +### Bounds and randomization + +`computeBounds` derives `start_r` and `end_r` from the minimum and maximum control-point x values across all properties. It falls back to `0.25` and `1.0` when no points exist. + +The newer `randomizeAllPoints` preserves each control point’s radial x position and replaces y values using smooth generated profiles. It includes multiple chord-planform families, monotonic thickness decay, gradual pitch variation, tip-biased skew, small rake growth, and small camber growth. Camber and thickness are clamped to nonnegative values so randomized designs pass backend validation. + +Randomization calls both `applyAllPoints` and `updateRender` without awaiting either before continuing. This is fire-and-forget concurrency, so curve refresh and STL regeneration proceed in parallel rather than in a strictly ordered sequence. + +### Notifications + +`useNotifications` powers the navbar bell: + +- Fetches `public/notifications.md` (via `withBasePath`) with `cache: "no-store"`, sharing a module-level cache and a single in-flight request across consumers. +- Parses it with `parseNotifications()` and reports `notifications`, `loading`, `error`, `unreadCount`, `unreadIds`, and `markAllSeen`. +- Read state persists in `localStorage` under `marinai.notificationsSeen` (a JSON array of notification ids). + +The markdown format: one `## Title` section per notice, followed by an HTML-comment metadata block `id: … | date: YYYY-MM-DD | version: x.y.z | tag: feature|fix|info|breaking`, then the Markdown body. Sections missing `id` or title are skipped; entries are sorted newest-first by date. `NotificationTag` is `feature | fix | info | breaking`, which drives the chip colour in the popover. + +## Components + +### Curve editing + +`Sidebar` is a left floating dock: six 64px parameter toggles (`/icons/${dataset}.png`) plus a narrow tab below with Reset (`RotateCcw`) and Randomize (`Dices`). All buttons share the busy-disabled state. + +`PlotPanel`: + +- Shows a draggable floating panel per property. +- Displays parameter-specific physical explanations. +- Shows loading, initialization-error, and no-data states. +- Contains a numeric X/Y control-point editor. +- Delegates curve rendering and manipulation to `DraggablePlot`. + +`DraggablePlot`: + +- Uses Plotly for curves and draggable control points. +- Uses axis labels with engineering units. +- Uses a local Lagrange preview while interacting. +- Sends durable changes to the backend after interaction settles. +- Avoids update loops by distinguishing programmatic plot updates from genuine user edits. +- Only reapplies annotations when they actually differ, avoiding Plotly relayout feedback loops. +- Uses refs to avoid stale closures in asynchronous Plotly handlers. + +### Three-dimensional view + +`StlViewer` implements a resilient vtk.js pipeline: + +- Generic render window and trackball camera. +- STL reader. +- Optional Windowed-Sinc smoothing. +- Point-normal computation. +- Actor/mapper lifecycle management. +- Resize handling without triggering invalid premature renders. +- Load tokens and disposal guards to avoid race conditions from rapid STL changes. +- An explicit three-light camera-following rig. +- Polished-metal-style Blinn-Phong material parameters. +- Theme-selected model color and background. +- Render-only-when-ready behavior to avoid `No input!` errors. + +Current smoothing defaults are 20 iterations and a `0.005` pass band. `StlViewer` also exposes `capturePreview(maxWidth=256, quality=0.9)` (WebP, render scale 0.4) used for snapshot-slot thumbnails. The old `topInset` prop and its camera-panning logic were removed; the model is centred with a plain `resetCamera()`. + +### Sections view + +`SectionsView`: + +- Projects 3D upper and lower section points onto a section-aligned 2D frame. +- Reconstructs upper, lower, mean-line, and chord information. +- Normalizes coordinates by chord. +- Corrects y-axis orientation when necessary. +- Detects reversed leading/trailing-edge orientation from maximum-thickness location. +- Uses Plotly for airfoil overlays and section statistics. +- Parses backend CSV into tip-to-hub ordered section profiles. + +The parser consumes the corrected first-column radius directly. It no longer applies the old `start + end − label` transformation. + +### Navbar, snapshots, and view controls + +There is no separate `Toolbar` / `BottomRightOverlay` / `TopRightControls` / `ViewModeToggle` component — all of those roles now live in `Navbar.tsx` (+ `ProfileMenu.tsx`) and `SnapshotRail.tsx`. + +`Navbar` is a fixed 64px header (`NAVBAR_HEIGHT=64`, `CONTROL_HEIGHT=44`) with logical groups: + +- Left: app icon + editable project title, Undo/Redo (`Undo2`/`Redo2`, gated by `canUndo`/`canRedo`), Update Render (`Play`). +- Center: 3D/Sections segmented toggle (`FastSegmentedToggle`, `data-tour="toolbar-toggle"`). +- Right: model-color picker, blades stepper (3–7, `Minus`/`Plus`), CSV import, Export dropdown (IGES / STL / Sections CSV / 3D points), Guide (`BookOpen`, hidden below 1540px), `NotificationsMenu` (bell + unread badge), and `ProfileMenu`. +- All controls share one `busy = loadingInit || isRendering || isExportingIges || sectionsLoading || importingCsv` disabled state (export menu also auto-closes while busy). + +`SnapshotRail` is a bottom-center slot bar (not bottom-right): + +- 64px tiles (`TILE=64`), 1–8 slots (`MIN_SLOTS=1`, `MAX_SLOTS=8`), starting at 5 (`SNAPSHOT_SLOTS=5`). +- Click an empty slot to open `SnapshotNameDialog` and save a named snapshot (captures a WebP preview via `StlViewer`); click a filled slot for a Recall / Empty menu. +- The slot number sits in the corner; the saved name is the hover tooltip. The rail's `+`/`−` buttons add or remove slots at the end. +- Snapshots are session-only; a page reload clears them. + +UI/theming note (current working tree): the floating docks and panels use a near-black translucent surface (`#080c0f` at 0.85 alpha) with hairline `#ffffff` borders instead of drop shadows; several navbar buttons dropped the `animated` prop; and the explicit `MuiTooltip` style override was removed from `selectedTheme`. + +### Profile, guide, dialogs, and status UI + +`ProfileMenu` (replacing the old `TopRightControls`) is an account button + popover: + +- Fetches `${basePath}/api/user`, displays the best available display name (`name || username || email || "Signed in"`, `Loading…` while fetching) and secondary email. +- Offers a single Log out button (`onLogout` → `/api/logout`). No external-portal navigation. + +`NotificationsMenu` is the bell button + `Popover`: + +- Shows an unread `Badge` (`unreadCount`), fetches release notes via `useNotifications`, and renders each notice's Markdown body through `react-markdown` + `remark-gfm`. +- Opening the panel marks everything seen after a short delay (clearing the badge); unread rows stay highlighted for the session, and a "Mark all as read" action is available. +- The app version (`NEXT_PUBLIC_APP_VERSION`) is shown in the footer when set. + +`Tutorial` (a `FastTour` wrapper, storage key `marinai.tutorialDone`) provides nine guided steps: + +1. Welcome (`welcome`) +2. Parameter plots (`sidebar`) +3. Curve editing (`panel`) +4. View modes (`toolbar-toggle`) +5. CSV import (`toolbar-import`) +6. Update Render (`toolbar-render`) +7. Export (`toolbar-export`) +8. Blade snapshots (`snapshots`) +9. Completion (`done`) + +It persists completion in local storage, supports manual replay, measures highlighted elements, responds to resizing/scrolling, and can temporarily reveal the chord panel. + +`CsvImportDialog` accepts a required sections CSV and an optional control-point CSV. `SectionDownloadDialog` selects station count and whether to bundle control points into a ZIP file. `SpinningPropeller` supplies themed loading and export overlays. The 3D/sections switch is part of `Navbar` (a `FastSegmentedToggle`), not a separate `ViewModeToggle` component. + +## Backend API Wiring + +The frontend hardcodes backend API major version `v1` in its hooks, matching the backend router prefix. + +| User action | Backend call | +|---|---| +| Initial load or reset | `GET /v1/curves/init` | +| Drag a control point | `POST /v1/curves/{dataset}` with `{controlPoints}` | +| Replace all points through Randomize | One `POST /v1/curves/{dataset}` per property | +| Import CSV | `POST /v1/curves/upload` with multipart `csv_file` and optional `cp_file` | +| Render propeller | `POST /v1/propeller/stl`, then authenticated `GET` of returned `stl_url` | +| Export IGES | `POST /v1/propeller/parametric/iges`, then authenticated `GET` of returned `iges_url` | +| Download section table | `POST /v1/sections`, then save CSV or ZIP Blob | +| Download 3D points | `POST /v1/sections/points3d`, then save CSV Blob | +| Refresh sections view | Parallel `POST /v1/sections/points3d` and `POST /v1/sections` | + +Binary responses are always fetched with `authorizedFetch`, converted to Blobs, exposed through revocable object URLs, and downloaded through temporary anchor elements. The application avoids unauthenticated direct links because they cannot carry the required bearer token. diff --git a/docs/apps/marinai/glossary.md b/docs/apps/marinai/glossary.md new file mode 100644 index 0000000..e5c4c27 --- /dev/null +++ b/docs/apps/marinai/glossary.md @@ -0,0 +1,34 @@ +--- +outline: deep +--- + +# marinAI — Glossary + +Terms used across the marinAI documentation. + +- **r/R:** Normalized radial station, from hub toward tip. +- **Bézier control point:** A draggable point shaping a parametric curve. +- **Bernstein polynomial:** Basis function used to evaluate Bézier curves. +- **RMSE:** Root-mean-square fitting error reported for CSV-derived curves. +- **Chord:** Section width distribution, normalized by diameter. +- **Pitch:** Axial advance per revolution, normalized by diameter. +- **Rake:** Axial section offset, normalized by diameter. +- **Skew:** Angular section offset, normally in degrees. +- **Camber:** Section mean-line curvature relative to chord. +- **Thickness:** Maximum section thickness relative to chord. +- **BladeX:** External blade/propeller geometry library. +- **pythonOCC:** Python bindings for OpenCASCADE CAD operations. +- **STL:** Triangulated mesh format used for visualization and manufacturing workflows. +- **IGES:** CAD boundary-representation exchange format. +- **Tabella progetto:** Section-property project table exported as CSV or ZIP. +- **JWKS:** JSON Web Key Set used to obtain Keycloak public signing keys. +- **RS256:** RSA signature algorithm used for the validated access tokens. +- **Auth.js/NextAuth:** Frontend authentication/session framework. +- **OIDC:** OpenID Connect login protocol layered on OAuth 2. +- **RP-initiated logout:** Logout flow that also terminates the Keycloak SSO session using an ID-token hint. +- **HTTP 422:** Validation or invalid-geometry client error used by the current backend. +- **Degenerate tip plate:** Zero-thickness closure section used at the blade tip. +- **Navbar:** Fixed 64px header consolidating title, history, render, view toggle, color, blades, import/export, guide, notifications, and profile. +- **BladeSnapshot / SnapshotRail:** Session-only named blade states with WebP previews (5 slots default, 1–8 allowed); distinct from the 50-step undo history. +- **Notification:** A release-notes entry parsed from `public/notifications.md` and shown in the navbar bell popover; read state lives in `localStorage`. +- **Base path:** Optional subpath deployment. `NEXT_PUBLIC_BASE_PATH` (browser, via `withBasePath()`) and `BASE_PATH` (Next routing/proxy) select the public and server prefixes respectively. diff --git a/docs/apps/marinai/overview.md b/docs/apps/marinai/overview.md new file mode 100644 index 0000000..67724db --- /dev/null +++ b/docs/apps/marinai/overview.md @@ -0,0 +1,96 @@ +--- +outline: deep +--- + +# marinAI — Overview + +marinAI is a parametric marine-propeller design application for naval architects iterating on radial blade geometry. It is built from two repositories: **marinAI-backend** (FastAPI service: Bézier math, blade construction, 3D export) and **marinAI-frontend** (Next.js UI: interactive curve editing, 3D STL viewer, 2D section viewer, downloads). + +## What marinAI Is + +marinAI is a parametric marine-propeller design loop: + +1. Represent six blade properties as functions of normalized radius: + - `chord` + - `pitch` + - `rake` + - `skew` + - `camber` + - `thickness` +2. Let the user shape those functions by dragging Bézier control points. +3. Recompute curves on the backend whenever control points change. +4. Build a three-dimensional propeller blade from the curves. +5. Render the propeller in the browser. +6. Export CAD and tabular artifacts: + - STL for visualization, printing, and mesh use + - IGES for CAD exchange + - Project-table CSV or ZIP files + - Three-dimensional section-point CSV files +7. Protect the design backend with Keycloak authentication. + +The intended users are naval architects and propeller designers iterating on radial blade geometry. The application combines an engineering backend with a highly interactive visual frontend. + +## System Architecture + +### High-level flow + +```text +Browser / Next.js frontend + | + | HTTPS requests with Keycloak bearer tokens + v +FastAPI backend (/v1) + | + |-- Bézier math with NumPy/SciPy + |-- Blade construction with BladeX + |-- CAD/mesh export with pythonOCC + | + v +Responses: +- JSON control points and curves +- STL/IGES files +- CSV/ZIP section data +``` + +Authentication involves Keycloak in two complementary places: + +```text +Browser + | + | 1. OIDC sign-in through the frontend's own NextAuth/Keycloak integration + v +Frontend Next.js server + - Maintains an Auth.js session + - Refreshes Keycloak access tokens + - Exposes the current access token through /api/session-token + | + | 2. Sends the access token as Authorization: Bearer + v +FastAPI backend + - Verifies the token against the Keycloak realm JWKS + - Does not perform the browser login itself + - Does not need the Keycloak client secret +``` + +### Repository responsibilities + +| Concern | Backend | Frontend | +|---|---|---| +| Bézier interpolation and fitting | Yes | No; it displays server results | +| Blade geometry construction | Yes, through BladeX | No | +| STL/IGES generation | Yes, through pythonOCC and BladeX | No; it renders/downloads results | +| Interactive curve editing | No | Yes, through draggable Plotly panels | +| 3D visualization | No | Yes, through vtk.js | +| User login and session management | No | Yes, through NextAuth/Keycloak | +| Token validation | Yes, through Keycloak JWKS | Indirectly, by supplying tokens | +| File downloads | Serves generated files | Uses authenticated `fetch`, then browser Blob URLs | + +## End-to-End Workflows + +**A. Design from scratch** — open the app; the route guard sends an unauthenticated visitor to internal `/login`, then Keycloak OIDC returns to the workspace. The tutorial may appear, default curves load and auto-render, control-point drags refresh curves, **Update Render** regenerates the STL, the sections view stays synchronized, and STL/IGES/CSV artifacts can be exported. + +**B. Start from a project CSV** — open **Import CSV**, upload a project-table CSV and optionally a control-point CSV, review the fitted or directly supplied Bézier curves, reset history as needed, render, and export. + +**C. Export for CAD or analysis** — use STL for browser visualization or mesh workflows; parametric IGES for CAD handoff; `tabella_progetto.csv` or ZIP for radial property tables; `sections_points3d.csv` for upper/lower three-dimensional section coordinates. + +**D. Reset and undo safely** — reset reloads backend defaults and rerenders; undo and redo traverse up to 50 snapshots without affecting browser navigation; CSV import clears the snapshot history. Blade snapshot slots (5 by default, up to 8, optionally named) are separate from undo history and last only for the current session. diff --git a/docs/index.md b/docs/index.md index 0d056ce..1ce3250 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,7 +2,7 @@ layout: home hero: - name: "FAST-Docs" + name: 'FAST Computing' text: "Documentation & Standards" tagline: "High-performance technical documentation built for speed and clarity." image: @@ -12,19 +12,8 @@ hero: - theme: brand text: Get Started link: /introduction - -features: - - title: ⚡️ Lightning Fast - details: Built with Vite and Vue 3, ensuring your documentation loads instantly for your users. - - title: 🔎 Smart Search - details: Integrated local search with high-speed indexing for finding answers in seconds. - - title: ⚙️ Industry Standards - details: Optimized for clarity, accessibility, and following modern documentation best practices. + - theme: alt + text: GitHub + link: https://github.com/FAST-Computing + target: _blank --- - - \ No newline at end of file diff --git a/docs/public/logotext_fast_oneline.svg b/docs/public/logotext_fast_oneline.svg new file mode 100644 index 0000000..74906ad --- /dev/null +++ b/docs/public/logotext_fast_oneline.svg @@ -0,0 +1,919 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +