redundent files deleted
This commit is contained in:
@@ -1,218 +0,0 @@
|
|||||||
# GDS and SVG Generation Logic Path
|
|
||||||
|
|
||||||
This document traces the current code path for generating saved layout YAML,
|
|
||||||
layout preview SVG, and downloadable project GDS when the user clicks
|
|
||||||
`Build Layout` or `Build GDS`.
|
|
||||||
|
|
||||||
Line numbers refer to the files as currently checked in.
|
|
||||||
|
|
||||||
## Build-Time Router Gate
|
|
||||||
|
|
||||||
`python backend/server.py` -> `app.run` starts without importing
|
|
||||||
`mxpic_router`, `mxpic_forge`, Nazca, or gdstk. Login, dashboard, canvas
|
|
||||||
editing, YAML generation, PDK browsing, and project save without preview do not
|
|
||||||
require the external build stack.
|
|
||||||
|
|
||||||
Build actions validate the external stack on demand:
|
|
||||||
|
|
||||||
- `Build GDS` -> `backend/gds_builder.py` -> `require_router_stack()`.
|
|
||||||
- `Build Layout` SVG preview -> `backend/routed_layout_preview.py` ->
|
|
||||||
`require_router_stack(require_gdstk=True)`.
|
|
||||||
|
|
||||||
Important functions:
|
|
||||||
|
|
||||||
- `ensure_router_path` (`backend/router_dependency.py` line 23) adds the sibling
|
|
||||||
`../mxpic_router` checkout to `sys.path` when present.
|
|
||||||
- `require_router_stack` (`backend/router_dependency.py` line 31) imports
|
|
||||||
`mxpic_router`, `nazca`, and the route backend used by `mxpic_router`.
|
|
||||||
The preferred route backend is `mxpic_forge.Route`; if it is absent,
|
|
||||||
`mxpic_router` falls back to Nazca `interconnects.Interconnect`.
|
|
||||||
`gdstk` is checked only when SVG preview generation requests it.
|
|
||||||
|
|
||||||
## Generated Files
|
|
||||||
|
|
||||||
- Saved cell YAML: `database/<username>/layout/<project>/<cell>.yml`
|
|
||||||
- Path helpers: `user_layout_root`, `project_root`, `cell_file_path`
|
|
||||||
(`backend/server.py` lines 124-137).
|
|
||||||
- Saved layout preview SVG: `database/<username>/layout/<project>/<cell>.svg`
|
|
||||||
- Path helper: `cell_svg_path` (`backend/server.py` lines 140-142).
|
|
||||||
- Optional route sidecar: `database/<username>/layout/<project>/<cell>.routes.yml`
|
|
||||||
- Path helper and writer: `cell_routes_path`, `write_route_points_sidecar`
|
|
||||||
(`backend/server.py` lines 145-175).
|
|
||||||
- Downloadable GDS export: `database/_exports/<uuid>/<project>.gds`
|
|
||||||
- Created by `create_export_path` (`backend/pdk_access.py` lines 53-59).
|
|
||||||
|
|
||||||
## Build Layout: Click Button -> YAML -> Router GDS -> SVG
|
|
||||||
|
|
||||||
Compact path:
|
|
||||||
|
|
||||||
`Click Build Layout` -> `handleBuildLayout` (`frontend/canvas.html` line 6209) :
|
|
||||||
`buildYamlForPage` (`frontend/canvas.html` line 6141) generates layout YAML ->
|
|
||||||
POST `/api/save-layout` (`frontend/canvas.html` line 6219) -> `save_layout`
|
|
||||||
(`backend/server.py` line 715) writes `<cell>.yml` -> `create_routed_layout_svg`
|
|
||||||
(`backend/server.py` line 734) -> `mxpic_router.build_project_gds` generates
|
|
||||||
temporary Nazca GDS -> `gdstk.read_gds` + `write_svg` writes `<cell>.svg` ->
|
|
||||||
backend returns `svg_url` -> `openLayoutPreview` (`frontend/canvas.html` line
|
|
||||||
6183) opens the SVG preview tab.
|
|
||||||
|
|
||||||
Detailed path:
|
|
||||||
|
|
||||||
1. The button is rendered only for non-preview pages:
|
|
||||||
- `<button onClick={handleBuildLayout}>` (`frontend/canvas.html` lines
|
|
||||||
6491-6498).
|
|
||||||
|
|
||||||
2. `handleBuildLayout` creates YAML for the active page:
|
|
||||||
- `handleBuildLayout` starts at `frontend/canvas.html` line 6209.
|
|
||||||
- It validates route crossings and calls `buildYamlForPage(activePage)` at
|
|
||||||
line 6215.
|
|
||||||
- `buildYamlForPage` starts at line 6141 and writes:
|
|
||||||
- `schema_version`, `kind`, `coordinate_system: gds_y_up`,
|
|
||||||
`canvas_size`, `project`, `name`, `type`, `version`.
|
|
||||||
- `pins` via `buildCanvasPinsYaml`.
|
|
||||||
- `instances` via `buildInstancesYaml`.
|
|
||||||
- `elements` via `buildElementsYaml`.
|
|
||||||
- `bundles` via `buildBundlesYaml`.
|
|
||||||
|
|
||||||
3. Frontend sends the YAML to Flask:
|
|
||||||
- `fetch('/api/save-layout', ...)` at `frontend/canvas.html` line 6219.
|
|
||||||
- Body includes `project`, `cell`, and `content` at lines 6222-6226.
|
|
||||||
|
|
||||||
4. Flask saves YAML and route sidecar:
|
|
||||||
- Route: `/api/save-layout` (`backend/server.py` line 713).
|
|
||||||
- Function: `save_layout` (`backend/server.py` line 715).
|
|
||||||
- `save_path = cell_file_path(project, cell)` (`backend/server.py` line 724).
|
|
||||||
- Writes the YAML content to disk (`backend/server.py` lines 727-728).
|
|
||||||
- Extracts route points into `<cell>.routes.yml` through
|
|
||||||
`write_route_points_sidecar` (`backend/server.py` line 729).
|
|
||||||
|
|
||||||
5. Flask always uses the router-backed SVG preview:
|
|
||||||
- Preview output path is computed with `cell_svg_path`
|
|
||||||
(`backend/server.py` line 733).
|
|
||||||
- `create_routed_layout_svg(...)` is called unconditionally for preview
|
|
||||||
generation (`backend/server.py` lines 734-742).
|
|
||||||
- There is no production branch based on `bundles.*.links`; no-link and
|
|
||||||
linked layouts both use the same router path.
|
|
||||||
|
|
||||||
6. Routed preview helper converts router GDS into SVG:
|
|
||||||
- `create_routed_layout_svg` starts at `backend/routed_layout_preview.py`
|
|
||||||
line 14.
|
|
||||||
- It loads the YAML string, gets the target cell name, and imports
|
|
||||||
`mxpic_router.build_project_gds` (`backend/routed_layout_preview.py` lines
|
|
||||||
25-32).
|
|
||||||
- It builds a temporary GDS with `target_cell_name=cell_name`
|
|
||||||
(`backend/routed_layout_preview.py` lines 37-46).
|
|
||||||
- It reads that temporary GDS with `gdstk.read_gds`
|
|
||||||
(`backend/routed_layout_preview.py` line 47).
|
|
||||||
- It writes the saved preview SVG with `top_cells[0].write_svg(output_path)`
|
|
||||||
(`backend/routed_layout_preview.py` line 51).
|
|
||||||
|
|
||||||
Inside sibling router checkout:
|
|
||||||
|
|
||||||
- `mxpic_router.build_project_gds` starts at
|
|
||||||
`../mxpic_router/mxpic_router/builder.py` line 12.
|
|
||||||
- `_load_project_specs` reads all saved `.yml` / `.yaml` files from the project
|
|
||||||
directory (`../mxpic_router/mxpic_router/builder.py` line 46).
|
|
||||||
- `load_cell_spec` reads YAML with `yaml.safe_load`
|
|
||||||
(`../mxpic_router/mxpic_router/eda_loader.py` line 80).
|
|
||||||
- `parse_cell_dict` converts YAML into `CellSpec`, including `pins`,
|
|
||||||
`elements`, `instances`, and `bundles`
|
|
||||||
(`../mxpic_router/mxpic_router/eda_loader.py` lines 85-157).
|
|
||||||
- `_build_cell` creates `with nd.Cell(name=spec.name) as top`
|
|
||||||
(`../mxpic_router/mxpic_router/builder.py` line 70).
|
|
||||||
- `_build_cell` places basic, local, or PDK GDS instances and registers pins
|
|
||||||
(`../mxpic_router/mxpic_router/builder.py` lines 73-108).
|
|
||||||
- `_route_link` draws routed geometry for each bundle link when links exist
|
|
||||||
(`../mxpic_router/mxpic_router/builder.py` line 269).
|
|
||||||
- `nd.export_gds(topcells=[built_cells[top.name]], filename=output_path)`
|
|
||||||
writes the temporary routed GDS (`../mxpic_router/mxpic_router/builder.py`
|
|
||||||
line 37).
|
|
||||||
|
|
||||||
## Build GDS: Click Button -> Saved YAML -> Router GDS
|
|
||||||
|
|
||||||
Compact path:
|
|
||||||
|
|
||||||
`Click Build GDS` -> Project Tree button calls `onBuildGds`
|
|
||||||
(`frontend/canvas.html` line 2738) -> `handleBuildGds`
|
|
||||||
(`frontend/canvas.html` line 6287) -> POST `/api/build-gds`
|
|
||||||
(`frontend/canvas.html` line 6294) -> `build_gds`
|
|
||||||
(`backend/server.py` line 775) -> `build_project_gds`
|
|
||||||
(`backend/gds_builder.py` line 26) : read saved `.yml` files for project
|
|
||||||
validation -> `_build_with_mxpic_router` (`backend/gds_builder.py` line 47) ->
|
|
||||||
`mxpic_router` creates `nd.Cell` objects and exports `.gds` -> backend returns
|
|
||||||
`download_url` -> frontend clicks a temporary download link.
|
|
||||||
|
|
||||||
Detailed path:
|
|
||||||
|
|
||||||
1. The `Build GDS` button is in the project tree header:
|
|
||||||
- `<button className="build-gds-btn" onClick={onBuildGds}>`
|
|
||||||
(`frontend/canvas.html` lines 2738-2739).
|
|
||||||
- `onBuildGds={handleBuildGds}` is passed into `LeftPanel`
|
|
||||||
(`frontend/canvas.html` line 6354).
|
|
||||||
|
|
||||||
2. `handleBuildGds` calls the backend:
|
|
||||||
- Function starts at `frontend/canvas.html` line 6287.
|
|
||||||
- It validates route crossings for all non-preview pages
|
|
||||||
(`frontend/canvas.html` lines 6289-6290).
|
|
||||||
- It POSTs only `{ project: currentProjectName }` to `/api/build-gds`
|
|
||||||
(`frontend/canvas.html` lines 6294-6298).
|
|
||||||
|
|
||||||
Important behavior:
|
|
||||||
|
|
||||||
- `Build GDS` does not serialize the current in-memory canvas first.
|
|
||||||
- It reads YAML files that already exist on disk.
|
|
||||||
- Those YAML files are produced by `Build Layout` or by the separate `Save`
|
|
||||||
button, where `handleSaveProjectLayouts` saves every non-preview page with
|
|
||||||
`preview: false` (`frontend/canvas.html` lines 6253-6284).
|
|
||||||
|
|
||||||
3. Flask creates a temporary export path and calls the GDS builder:
|
|
||||||
- Route: `/api/build-gds` (`backend/server.py` line 773).
|
|
||||||
- Function: `build_gds` (`backend/server.py` line 775).
|
|
||||||
- Old exports are cleaned (`backend/server.py` line 781).
|
|
||||||
- The project directory is resolved and validated
|
|
||||||
(`backend/server.py` lines 782-784).
|
|
||||||
- `create_export_path` creates `database/_exports/<uuid>/<project>.gds`
|
|
||||||
(`backend/server.py` line 785, `backend/pdk_access.py` lines 53-59).
|
|
||||||
- `build_project_gds(...)` is called (`backend/server.py` lines 786-792).
|
|
||||||
|
|
||||||
4. `backend/gds_builder.py` delegates to `mxpic_router`:
|
|
||||||
- `build_project_gds` starts at `backend/gds_builder.py` line 26.
|
|
||||||
- `_load_project_cells(project_dir)` reads every saved `.yml` / `.yaml`
|
|
||||||
file for validation (`backend/gds_builder.py` line 73).
|
|
||||||
- `_build_with_mxpic_router` starts at `backend/gds_builder.py` line 47.
|
|
||||||
- `_build_with_mxpic_router` calls `ensure_router_path` and imports
|
|
||||||
`mxpic_router.build_project_gds` (`backend/gds_builder.py` lines 54-55).
|
|
||||||
- There is no production fallback to local gdstk or local Nazca builders.
|
|
||||||
|
|
||||||
5. `mxpic_router` builds the final GDS:
|
|
||||||
- `mxpic_router.build_project_gds` starts at
|
|
||||||
`../mxpic_router/mxpic_router/builder.py` line 12.
|
|
||||||
- `_build_cell` creates one `nd.Cell` per `CellSpec`
|
|
||||||
(`../mxpic_router/mxpic_router/builder.py` line 70).
|
|
||||||
- `_resolve_pdk_asset` finds component YAML/GDS in the active PDK root
|
|
||||||
(`../mxpic_router/mxpic_router/builder.py` line 509).
|
|
||||||
- `_route_link` draws routed geometry when saved bundle links exist
|
|
||||||
(`../mxpic_router/mxpic_router/builder.py` line 269).
|
|
||||||
- `nd.export_gds(...)` writes the output GDS
|
|
||||||
(`../mxpic_router/mxpic_router/builder.py` line 37).
|
|
||||||
|
|
||||||
## PDK Asset Resolution
|
|
||||||
|
|
||||||
The active PDK root still comes from the EDA backend request/session state:
|
|
||||||
|
|
||||||
- `current_pdk_root` uses `pdk_root_for_session`
|
|
||||||
(`backend/server.py` lines 196-198).
|
|
||||||
- `pdk_root_for_session` chooses the root based on the logged-in user group
|
|
||||||
(`backend/pdk_access.py` lines 43-50).
|
|
||||||
- Preview and GDS generation pass that PDK root into `mxpic_router`.
|
|
||||||
- `mxpic_router._resolve_pdk_asset` resolves component YAML/GDS under that root
|
|
||||||
(`../mxpic_router/mxpic_router/builder.py` line 509).
|
|
||||||
|
|
||||||
## Download Completion
|
|
||||||
|
|
||||||
After GDS build:
|
|
||||||
|
|
||||||
`build_gds` returns `download_url` (`backend/server.py` line 803) ->
|
|
||||||
`handleBuildGds` creates a temporary `<a>` and clicks it
|
|
||||||
(`frontend/canvas.html` lines 6309-6316) -> `/api/exports/<export_id>/<filename>`
|
|
||||||
serves the GDS (`backend/server.py` lines 812-829) -> the temporary export
|
|
||||||
folder is deleted when the response closes (`backend/server.py` line 829).
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
<!--
|
|
||||||
Description: Project documentation for setup, deployment, and operational guidance.
|
|
||||||
Inside functions: N/A - documentation content.
|
|
||||||
Developer : Qin Yue @ 2026
|
|
||||||
Organization : OptiHK Limited
|
|
||||||
-->
|
|
||||||
# mxPIC EDA Intranet Deployment
|
|
||||||
|
|
||||||
## Build-Time Router Stack
|
|
||||||
|
|
||||||
The Flask server can launch for login, dashboard, canvas editing, YAML
|
|
||||||
generation, and PDK browsing without importing `mxpic_router` or `mxpic_forge`.
|
|
||||||
Build actions require `mxpic_router` and Nazca. When `mxpic_forge.Route` is not
|
|
||||||
available, routing falls back to Nazca `interconnects.Interconnect`; SVG preview
|
|
||||||
generation also requires `gdstk`.
|
|
||||||
|
|
||||||
## Start on the office LAN
|
|
||||||
|
|
||||||
1. On the host computer, open PowerShell in this repository.
|
|
||||||
2. Set a persistent secret key:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$env:MXPIC_SECRET_KEY = "replace-with-a-long-random-secret"
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Start the server:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
.\run_intranet_server.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
The app listens on `0.0.0.0:3000`, so other users can open:
|
|
||||||
|
|
||||||
```text
|
|
||||||
http://<host-computer-ip>:3000
|
|
||||||
```
|
|
||||||
|
|
||||||
Find the host IP with:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
ipconfig
|
|
||||||
```
|
|
||||||
|
|
||||||
Use the IPv4 address on the company LAN adapter.
|
|
||||||
|
|
||||||
## Windows firewall
|
|
||||||
|
|
||||||
If coworkers cannot connect, allow inbound TCP port `3000` on the host computer.
|
|
||||||
|
|
||||||
## Accounts
|
|
||||||
|
|
||||||
Default local accounts:
|
|
||||||
|
|
||||||
```text
|
|
||||||
admin / 123456
|
|
||||||
engineer / 123456
|
|
||||||
```
|
|
||||||
|
|
||||||
Change these passwords from the dashboard profile panel before regular use.
|
|
||||||
|
|
||||||
Each user stores projects under:
|
|
||||||
|
|
||||||
```text
|
|
||||||
database/<username>/layout
|
|
||||||
```
|
|
||||||
|
|
||||||
## Useful environment variables
|
|
||||||
|
|
||||||
```text
|
|
||||||
MXPIC_HOST=0.0.0.0
|
|
||||||
MXPIC_PORT=3000
|
|
||||||
MXPIC_DEBUG=0
|
|
||||||
MXPIC_SECRET_KEY=<long random string>
|
|
||||||
MXPIC_COOKIE_SECURE=0
|
|
||||||
MXPIC_PDK_PUBLIC_ROOT=<path-to-public-foundries>
|
|
||||||
MXPIC_PDK_ATLAS_ROOT=<path-to-atlas-foundries>
|
|
||||||
```
|
|
||||||
|
|
||||||
Set `MXPIC_COOKIE_SECURE=1` only when serving through HTTPS.
|
|
||||||
|
|
||||||
PDK component metadata, GDS assets, and `technology.yml` manifests are all read
|
|
||||||
from these role-scoped roots. If the variables are not set, the backend defaults
|
|
||||||
to `../opt_pdk_public/foundries` for normal users/developers and
|
|
||||||
`../opt_pdk_atlas/foundries` for managers.
|
|
||||||
@@ -1,581 +0,0 @@
|
|||||||
# PDK, Technology, and Xsection Loading Logic Path
|
|
||||||
|
|
||||||
This document explains how the current code loads:
|
|
||||||
|
|
||||||
- PDK component information: component folders, component YAML, images, and GDS assets.
|
|
||||||
- Technology information: selected foundry/technology for each project.
|
|
||||||
- Xsection information: routing cross-section defaults and Nazca layer bindings from `technology.yml`.
|
|
||||||
|
|
||||||
Line numbers refer to the current codebase at the time this document was written.
|
|
||||||
|
|
||||||
## Mental Model
|
|
||||||
|
|
||||||
There are three related but separate data paths.
|
|
||||||
|
|
||||||
1. Component PDK assets
|
|
||||||
- Source: role-controlled PDK trees:
|
|
||||||
- `opt_pdk_public/foundries`
|
|
||||||
- `opt_pdk_atlas/foundries`
|
|
||||||
- Controlled by user group through `pdk_root_for_session`
|
|
||||||
(`backend/pdk_access.py` line 43).
|
|
||||||
- Used for library browsing, component metadata, component images, and final
|
|
||||||
component GDS resolution.
|
|
||||||
|
|
||||||
2. Technology registry and manifests
|
|
||||||
- Source: the same role-controlled PDK trees as component assets.
|
|
||||||
- Path: `<active_role_pdk_root>/<foundry>/<technology>/technology.yml`.
|
|
||||||
- Used to list available technologies, save the project technology choice,
|
|
||||||
load route defaults, and register Nazca layers/xsections.
|
|
||||||
|
|
||||||
3. Saved project layout YAML
|
|
||||||
- Source: `database/<username>/layout/<project>/<cell>.yml`.
|
|
||||||
- Contains placed component paths and `bundles.*.links[*].xsection`.
|
|
||||||
- Consumed by `mxpic_router` when building SVG preview GDS or downloadable GDS.
|
|
||||||
|
|
||||||
Important distinction:
|
|
||||||
|
|
||||||
- The active role PDK root is now the single source of truth.
|
|
||||||
- Normal/developer users read from `opt_pdk_public/foundries`.
|
|
||||||
- Manager users read from `opt_pdk_atlas/foundries`.
|
|
||||||
- The selected project technology scopes the library browser and selects which
|
|
||||||
`technology.yml` is loaded from that same role root.
|
|
||||||
|
|
||||||
## Active Roots
|
|
||||||
|
|
||||||
Backend startup defines the important paths:
|
|
||||||
|
|
||||||
- `REPO_ROOT` (`backend/server.py` line 36) points two levels above
|
|
||||||
`backend/`.
|
|
||||||
- There is no separate technology-manifest copy inside `mxpic_EDA`.
|
|
||||||
|
|
||||||
Role-based PDK root:
|
|
||||||
|
|
||||||
`Flask session user_group` -> `current_pdk_root`
|
|
||||||
(`backend/server.py` line 196) -> `pdk_root_for_session`
|
|
||||||
(`backend/pdk_access.py` line 43) -> `pdk_root_for_group`
|
|
||||||
(`backend/pdk_access.py` line 27) : choose:
|
|
||||||
|
|
||||||
- manager -> `MXPIC_PDK_ATLAS_ROOT` or `../opt_pdk_atlas/foundries`.
|
|
||||||
- developers/user -> `MXPIC_PDK_PUBLIC_ROOT` or `../opt_pdk_public/foundries`.
|
|
||||||
|
|
||||||
## Technology Selection Path
|
|
||||||
|
|
||||||
Compact path:
|
|
||||||
|
|
||||||
`Dashboard opens` -> `loadTechnologies` (`frontend/dashboard.html` line 1238)
|
|
||||||
-> GET `/api/technologies` -> `list_technologies`
|
|
||||||
(`backend/server.py` line 441) : scan `current_pdk_root()` ->
|
|
||||||
return `{ foundry, technology, id, label }` -> dashboard fills the technology
|
|
||||||
select.
|
|
||||||
|
|
||||||
Detailed path:
|
|
||||||
|
|
||||||
1. The dashboard calls `/api/technologies`:
|
|
||||||
- `loadTechnologies` starts at `frontend/dashboard.html` line 1238.
|
|
||||||
- It stores `data.technologies` and creates `<option>` values using
|
|
||||||
`tech.id`, for example `Silterra/EMO1_2ML_CU_Al_RDL`.
|
|
||||||
|
|
||||||
2. The backend scans the technology registry:
|
|
||||||
- Route: `/api/technologies` (`backend/server.py` line 439).
|
|
||||||
- Function: `list_technologies` (`backend/server.py` line 441).
|
|
||||||
- It scans `current_pdk_root()`.
|
|
||||||
- A technology directory is exposed only when it contains `technology.yml`.
|
|
||||||
- For each valid directory pair it returns:
|
|
||||||
- `foundry`
|
|
||||||
- `technology`
|
|
||||||
- `id: foundry/technology`
|
|
||||||
- `label: foundry / technology`
|
|
||||||
|
|
||||||
3. Creating a project stores the selected technology:
|
|
||||||
- Dashboard create click posts to `/api/projects`
|
|
||||||
(`frontend/dashboard.html` line 1303).
|
|
||||||
- Body contains `{ name, technology }`.
|
|
||||||
- Backend route: `/api/projects` (`backend/server.py` line 603).
|
|
||||||
- Function: `create_project` (`backend/server.py` line 605).
|
|
||||||
- It writes `.project.json` with:
|
|
||||||
- `name`
|
|
||||||
- `technology`
|
|
||||||
- Metadata helper path: `project_meta_path`
|
|
||||||
(`backend/server.py` line 228).
|
|
||||||
- Metadata writer: `write_project_meta`
|
|
||||||
(`backend/server.py` line 242).
|
|
||||||
|
|
||||||
Saved metadata location:
|
|
||||||
|
|
||||||
`database/<username>/layout/<project>/.project.json`
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "mxpic_project_1",
|
|
||||||
"technology": "Silterra/EMO1_2ML_CU_Al_RDL"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Technology Manifest Loading Path
|
|
||||||
|
|
||||||
Compact path:
|
|
||||||
|
|
||||||
`Canvas opens project` -> `loadProject` (`frontend/canvas.html` line 5030)
|
|
||||||
-> GET `/api/projects/<project>` -> `get_project`
|
|
||||||
(`backend/server.py` line 632) : returns saved `technology` -> frontend
|
|
||||||
`loadTechnologyManifest` (`frontend/canvas.html` line 4056) -> GET
|
|
||||||
`/api/technologies/<foundry>/<technology>/manifest` -> `read_technology_manifest`
|
|
||||||
(`backend/technology_manifest.py` line 27) -> return full `technology.yml`
|
|
||||||
manifest.
|
|
||||||
|
|
||||||
Detailed path:
|
|
||||||
|
|
||||||
1. Canvas loads project metadata:
|
|
||||||
- `loadProject` fetches `/api/projects/<project>`
|
|
||||||
(`frontend/canvas.html` line 5033).
|
|
||||||
- Backend `get_project` returns:
|
|
||||||
- project name
|
|
||||||
- saved cells
|
|
||||||
- `technology: read_project_meta(project_name).get("technology")`
|
|
||||||
(`backend/server.py` line 653).
|
|
||||||
|
|
||||||
2. Canvas loads the selected technology manifest:
|
|
||||||
- `loadTechnologyManifest` starts at `frontend/canvas.html` line 4056.
|
|
||||||
- It expects technology IDs in `foundry/technology` format.
|
|
||||||
- It fetches:
|
|
||||||
`/api/technologies/<foundry>/<technology>/manifest`.
|
|
||||||
- If missing or invalid, it logs an error and uses
|
|
||||||
`FALLBACK_TECHNOLOGY_MANIFEST`.
|
|
||||||
|
|
||||||
3. Backend validates and returns the manifest:
|
|
||||||
- Route:
|
|
||||||
`/api/technologies/<foundry>/<technology>/manifest`
|
|
||||||
(`backend/server.py` line 465).
|
|
||||||
- Function: `get_technology_manifest`
|
|
||||||
(`backend/server.py` line 467).
|
|
||||||
- It calls `read_technology_manifest`
|
|
||||||
(`backend/technology_manifest.py` line 27).
|
|
||||||
- The expected path is built by `technology_manifest_path`
|
|
||||||
(`backend/technology_manifest.py` line 16):
|
|
||||||
|
|
||||||
```text
|
|
||||||
<active_role_pdk_root>/<foundry>/<technology>/technology.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Backend manifest validation:
|
|
||||||
- `technology.yml` must exist.
|
|
||||||
- `manifest.xsections` must be a dictionary.
|
|
||||||
- `manifest.defaults` must be a dictionary.
|
|
||||||
- Otherwise the API returns 404 with a `TechnologyManifestError`.
|
|
||||||
|
|
||||||
## Current `technology.yml` Structure
|
|
||||||
|
|
||||||
Active example:
|
|
||||||
|
|
||||||
`opt_pdk_public/foundries/Silterra/EMO1_2ML_CU_Al_RDL/technology.yml`
|
|
||||||
|
|
||||||
For manager sessions, the equivalent file is read from
|
|
||||||
`opt_pdk_atlas/foundries`.
|
|
||||||
|
|
||||||
Important sections:
|
|
||||||
|
|
||||||
- `schema_version` (`technology.yml` line 7).
|
|
||||||
- `foundry` (`technology.yml` line 8).
|
|
||||||
- `technology` (`technology.yml` line 9).
|
|
||||||
- `layers` (`technology.yml` line 18).
|
|
||||||
- `routing_types` (`technology.yml` line 41).
|
|
||||||
- `defaults` (`technology.yml` line 44).
|
|
||||||
- `xsections` (`technology.yml` line 50).
|
|
||||||
|
|
||||||
Current xsections:
|
|
||||||
|
|
||||||
- `strip` (`technology.yml` line 51).
|
|
||||||
- `rib_low` (`technology.yml` line 58).
|
|
||||||
- `metal_1` (`technology.yml` line 67).
|
|
||||||
- `metal_2` (`technology.yml` line 74).
|
|
||||||
|
|
||||||
Meaning of the main fields:
|
|
||||||
|
|
||||||
- `layers`: maps technology layer names to GDS layer/datatype pairs.
|
|
||||||
- `routing_types`: choices shown by the route editor.
|
|
||||||
- `defaults`: default route `xsection`, `family`, `width`, `radius`, and
|
|
||||||
`routing_type`.
|
|
||||||
- `xsections`: per-cross-section defaults and layer bindings.
|
|
||||||
- `constants` and `source_class`: currently kept as manifest metadata. The
|
|
||||||
active frontend and router code do not consume them directly.
|
|
||||||
|
|
||||||
## Frontend Xsection Usage
|
|
||||||
|
|
||||||
Compact path:
|
|
||||||
|
|
||||||
`technology.yml xsections` -> backend manifest API -> `technologyManifest`
|
|
||||||
state (`frontend/canvas.html` line 3756) -> route editor dropdown and route
|
|
||||||
defaults -> saved YAML `bundles.output_bus.links[*].xsection`.
|
|
||||||
|
|
||||||
Detailed path:
|
|
||||||
|
|
||||||
1. Fallback manifest:
|
|
||||||
- `FALLBACK_TECHNOLOGY_MANIFEST` is defined in
|
|
||||||
`frontend/canvas-helpers.js` line 105.
|
|
||||||
- It includes fallback `strip`, `rib_low`, `metal_1`, and `metal_2`
|
|
||||||
xsections.
|
|
||||||
|
|
||||||
2. Canvas state:
|
|
||||||
- `projectTechnology` state is declared at `frontend/canvas.html` line 3755.
|
|
||||||
- `technologyManifest` state is declared at `frontend/canvas.html` line 3756.
|
|
||||||
|
|
||||||
3. Route defaults:
|
|
||||||
- `getXsectionInfo` (`frontend/canvas-helpers.js` line 132) looks up
|
|
||||||
`manifest.xsections[xsection]`.
|
|
||||||
- `createRouteSettings` (`frontend/canvas-helpers.js` line 138) merges:
|
|
||||||
- manifest defaults
|
|
||||||
- selected xsection defaults
|
|
||||||
- user overrides saved on the edge
|
|
||||||
- Resulting route state always has:
|
|
||||||
- `xsection`
|
|
||||||
- `family`
|
|
||||||
- `width`
|
|
||||||
- `radius`
|
|
||||||
- `routing_type`
|
|
||||||
|
|
||||||
4. Changing xsection in the route editor:
|
|
||||||
- Right panel builds available xsections from
|
|
||||||
`Object.keys(technologyManifest.xsections)`
|
|
||||||
(`frontend/canvas.html` line 2999).
|
|
||||||
- The xsection select calls `updateRouteXsection`
|
|
||||||
(`frontend/canvas.html` line 3037).
|
|
||||||
- `updateRouteXsection` refreshes family, default width, and default radius
|
|
||||||
(`frontend/canvas-helpers.js` line 167).
|
|
||||||
|
|
||||||
5. Link toolbar choices:
|
|
||||||
- `linkXsectionChoices` is built from `technologyManifest.xsections`
|
|
||||||
(`frontend/canvas.html` line 3775).
|
|
||||||
- It prefers the order `strip`, `rib_low`, `metal_1`, `metal_2`, then any
|
|
||||||
extra xsections.
|
|
||||||
|
|
||||||
## Component PDK Library Loading Path
|
|
||||||
|
|
||||||
Compact path:
|
|
||||||
|
|
||||||
`Canvas needs library` -> `fetchLibrary` (`frontend/canvas.html` line 4576)
|
|
||||||
-> GET `/api/library?project=<project>` -> `pdk_root_for_request_project`
|
|
||||||
(`backend/server.py` line 222) -> `scoped_pdk_root_for_project`
|
|
||||||
(`backend/server.py` line 201) -> `findComps`
|
|
||||||
(`backend/server.py` line 262) -> frontend library tree.
|
|
||||||
|
|
||||||
Detailed path:
|
|
||||||
|
|
||||||
1. Frontend requests the library:
|
|
||||||
- `fetchLibrary` calls `/api/library?project=<currentProjectName>`
|
|
||||||
(`frontend/canvas.html` line 4576).
|
|
||||||
|
|
||||||
2. Backend chooses the PDK root for this request:
|
|
||||||
- Route: `/api/library` (`backend/server.py` line 854).
|
|
||||||
- Function: `getLib` uses `pdk_root_for_request_project`.
|
|
||||||
- `pdk_root_for_request_project` starts at `backend/server.py` line 222.
|
|
||||||
- If a project is supplied, it calls `scoped_pdk_root_for_project`.
|
|
||||||
- Otherwise it uses `current_pdk_root`.
|
|
||||||
|
|
||||||
3. Project scoping:
|
|
||||||
- `scoped_pdk_root_for_project` starts at `backend/server.py` line 201.
|
|
||||||
- It reads the project `.project.json` technology field.
|
|
||||||
- It splits the value into `foundry/technology`.
|
|
||||||
- It scopes the role root to:
|
|
||||||
|
|
||||||
```text
|
|
||||||
<active_role_pdk_root>/<foundry>/<technology>
|
|
||||||
```
|
|
||||||
|
|
||||||
- It only returns that scoped path if it exists and stays under the active
|
|
||||||
role root.
|
|
||||||
- If not valid, it falls back to the active role root.
|
|
||||||
|
|
||||||
4. Component scanning:
|
|
||||||
- `findComps` starts at `backend/server.py` line 262.
|
|
||||||
- It walks the chosen PDK directory.
|
|
||||||
- Any folder containing a `.yml` file is treated as one component leaf.
|
|
||||||
- It stores component metadata:
|
|
||||||
- folder path
|
|
||||||
- first component YAML filename
|
|
||||||
- category
|
|
||||||
- relative component path
|
|
||||||
- `addCompsToTree` (`backend/server.py` line 290) converts this flat map
|
|
||||||
into the nested library tree returned to the canvas.
|
|
||||||
|
|
||||||
Important detail:
|
|
||||||
|
|
||||||
`getLib` scans the project-scoped directory but calls:
|
|
||||||
|
|
||||||
```python
|
|
||||||
findComps(comps_root, current_pdk_root())
|
|
||||||
```
|
|
||||||
|
|
||||||
This means the visible library is limited to the selected project technology,
|
|
||||||
but each component `__path__` is stored relative to the base role PDK root, not
|
|
||||||
relative to the scoped technology directory.
|
|
||||||
|
|
||||||
For example, if the active role PDK root is:
|
|
||||||
|
|
||||||
```text
|
|
||||||
opt_pdk_public/foundries
|
|
||||||
```
|
|
||||||
|
|
||||||
and the project technology is:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Silterra/EMO1_2ML_CU_Al_RDL
|
|
||||||
```
|
|
||||||
|
|
||||||
a component path saved into layout YAML can include:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Silterra/EMO1_2ML_CU_Al_RDL/<category>/<component>/<component>
|
|
||||||
```
|
|
||||||
|
|
||||||
This is important because build and preview pass the base role PDK root into
|
|
||||||
`mxpic_router`, and the saved component path can still resolve under that base
|
|
||||||
root.
|
|
||||||
|
|
||||||
## Component Metadata and Image Loading
|
|
||||||
|
|
||||||
Component YAML path:
|
|
||||||
|
|
||||||
`Canvas load/drop component` -> `loadComponentMetadata`
|
|
||||||
(`frontend/canvas.html` line 4082) -> GET
|
|
||||||
`/api/component/<component_name>?project=<project>` -> `readCompYaml`
|
|
||||||
(`backend/server.py` line 345) -> return component YAML.
|
|
||||||
|
|
||||||
Details:
|
|
||||||
|
|
||||||
- Route: `/api/component/<component_name>` (`backend/server.py` line 869).
|
|
||||||
- The backend uses the same project-scoped PDK root logic as `/api/library`.
|
|
||||||
- `readCompYaml` walks the selected PDK root and matches the component folder
|
|
||||||
basename.
|
|
||||||
- If the PDK root is under `opt_pdk_public` or `opt_pdk_atlas`, legacy
|
|
||||||
component metadata with `ports` is normalized to `pins`.
|
|
||||||
|
|
||||||
Component image path:
|
|
||||||
|
|
||||||
`Canvas thumbnail` -> `/api/component/<component_name>/image?project=<project>`
|
|
||||||
-> `find_component_dir` (`backend/server.py` line 367) -> first
|
|
||||||
`.png`, `.jpg`, `.jpeg`, or `.svg` in the component folder.
|
|
||||||
|
|
||||||
Route:
|
|
||||||
|
|
||||||
- `/api/component/<component_name>/image` (`backend/server.py` line 878).
|
|
||||||
|
|
||||||
## Saved YAML: How PDK Paths and Xsections Are Written
|
|
||||||
|
|
||||||
Compact path:
|
|
||||||
|
|
||||||
`Build Layout` or `Save` -> `buildYamlForPage`
|
|
||||||
(`frontend/canvas.html` line 6141) -> `findComponentPath`
|
|
||||||
(`frontend/canvas.html` line 3659) -> `buildInstancesYaml`
|
|
||||||
(`frontend/canvas-helpers.js` line 665) -> saved `instances.*.component`.
|
|
||||||
|
|
||||||
Component path details:
|
|
||||||
|
|
||||||
- `findComponentPath` searches the loaded library tree.
|
|
||||||
- If a component node has `__path__`, that path is used.
|
|
||||||
- `buildInstanceYaml` (`frontend/canvas-helpers.js` line 643) writes:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
instances:
|
|
||||||
<instance_name>:
|
|
||||||
component: <component path from library>
|
|
||||||
```
|
|
||||||
|
|
||||||
- Forge and basic components are special:
|
|
||||||
- Forge components save the forge component type.
|
|
||||||
- Basic components save names such as `waveguide`, `90 bend`, or `taper`.
|
|
||||||
- Normal PDK components save the PDK library path.
|
|
||||||
|
|
||||||
Xsection path details:
|
|
||||||
|
|
||||||
`Canvas edges` -> `buildBundlesYaml` (`frontend/canvas-helpers.js` line 988)
|
|
||||||
-> `createRouteSettings` (`frontend/canvas-helpers.js` line 138) -> saved
|
|
||||||
bundle links:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
bundles:
|
|
||||||
output_bus:
|
|
||||||
routing_type: euler_bend
|
|
||||||
links:
|
|
||||||
- from: <instance>:<pin>
|
|
||||||
to: <instance>:<pin>
|
|
||||||
xsection: strip
|
|
||||||
family: optical
|
|
||||||
width: 0.45
|
|
||||||
radius: 10
|
|
||||||
routing_type: euler_bend
|
|
||||||
```
|
|
||||||
|
|
||||||
The saved `xsection` field is the bridge from frontend route editing into
|
|
||||||
`mxpic_router` and Nazca.
|
|
||||||
|
|
||||||
## Build-Time Technology and PDK Loading
|
|
||||||
|
|
||||||
Both SVG preview and final GDS now use the router-backed path.
|
|
||||||
|
|
||||||
### SVG Preview
|
|
||||||
|
|
||||||
Compact path:
|
|
||||||
|
|
||||||
`POST /api/save-layout` -> `save_layout` (`backend/server.py` line 715)
|
|
||||||
-> `create_routed_layout_svg` (`backend/routed_layout_preview.py` line 14)
|
|
||||||
-> `mxpic_router.build_project_gds` -> temporary GDS -> `gdstk.write_svg`.
|
|
||||||
|
|
||||||
Arguments passed from `save_layout`:
|
|
||||||
|
|
||||||
- `pdk_root=current_pdk_root()` (`backend/server.py` line 737).
|
|
||||||
- `technology_manifest_path=technology_manifest_path_for_project(project)`
|
|
||||||
(`backend/server.py` line 739).
|
|
||||||
- `prefer_full_gds=prefer_full_gds_for_session(session)`
|
|
||||||
(`backend/server.py` line 740).
|
|
||||||
|
|
||||||
The preview helper:
|
|
||||||
|
|
||||||
- Imports `mxpic_router.build_project_gds`
|
|
||||||
(`backend/routed_layout_preview.py` line 32).
|
|
||||||
- Builds a temporary GDS with the same PDK root and technology manifest path
|
|
||||||
(`backend/routed_layout_preview.py` lines 39-45).
|
|
||||||
- Reads that GDS with `gdstk`.
|
|
||||||
- Writes the SVG preview (`backend/routed_layout_preview.py` line 51).
|
|
||||||
|
|
||||||
### Final GDS
|
|
||||||
|
|
||||||
Compact path:
|
|
||||||
|
|
||||||
`POST /api/build-gds` -> `build_gds` (`backend/server.py` line 775)
|
|
||||||
-> `backend.gds_builder.build_project_gds` (`backend/gds_builder.py` line 26)
|
|
||||||
-> `_build_with_mxpic_router` (`backend/gds_builder.py` line 47)
|
|
||||||
-> `mxpic_router.build_project_gds`.
|
|
||||||
|
|
||||||
Arguments passed from `build_gds`:
|
|
||||||
|
|
||||||
- `current_pdk_root()` (`backend/server.py` line 789).
|
|
||||||
- `technology_manifest_path=technology_manifest_path_for_project(project)`
|
|
||||||
(`backend/server.py` line 790).
|
|
||||||
- `prefer_full_gds=prefer_full_gds_for_session(session)`
|
|
||||||
(`backend/server.py` line 791).
|
|
||||||
|
|
||||||
`technology_manifest_path_for_project`:
|
|
||||||
|
|
||||||
- Starts at `backend/server.py` line 183.
|
|
||||||
- Reads the project technology from `.project.json`.
|
|
||||||
- Builds:
|
|
||||||
|
|
||||||
```text
|
|
||||||
<active_role_pdk_root>/<foundry>/<technology>/technology.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
- Returns the path only if it exists and stays under `current_pdk_root()`.
|
|
||||||
|
|
||||||
## Inside `mxpic_router`
|
|
||||||
|
|
||||||
Compact path:
|
|
||||||
|
|
||||||
`mxpic_router.build_project_gds` (`../mxpic_router/mxpic_router/builder.py`
|
|
||||||
line 12) -> `load_technology_manifest`
|
|
||||||
(`../mxpic_router/mxpic_router/technology.py` line 6) ->
|
|
||||||
`apply_technology_manifest` (`../mxpic_router/mxpic_router/technology.py`
|
|
||||||
line 13) -> Nazca `nd.add_layer`, `nd.add_xsection`,
|
|
||||||
`nd.add_layer2xsection`.
|
|
||||||
|
|
||||||
Detailed path:
|
|
||||||
|
|
||||||
1. Build starts:
|
|
||||||
- `build_project_gds` starts at
|
|
||||||
`../mxpic_router/mxpic_router/builder.py` line 12.
|
|
||||||
- It imports Nazca as `nd`.
|
|
||||||
- It loads the manifest from `technology_manifest_path`
|
|
||||||
(`../mxpic_router/mxpic_router/builder.py` line 23).
|
|
||||||
- It applies the manifest to Nazca
|
|
||||||
(`../mxpic_router/mxpic_router/builder.py` line 24).
|
|
||||||
|
|
||||||
2. Technology manifest to Nazca:
|
|
||||||
- `load_technology_manifest` returns `{}` if the path is missing
|
|
||||||
(`../mxpic_router/mxpic_router/technology.py` line 6).
|
|
||||||
- `apply_technology_manifest` does nothing for an empty manifest.
|
|
||||||
- For each `manifest.layers` entry it calls:
|
|
||||||
|
|
||||||
```python
|
|
||||||
nd.add_layer(name=name, layer=(layer, datatype), overwrite=True)
|
|
||||||
```
|
|
||||||
|
|
||||||
- For each `manifest.xsections` entry it calls:
|
|
||||||
|
|
||||||
```python
|
|
||||||
nd.add_xsection(name=xsection)
|
|
||||||
nd.add_layer2xsection(...)
|
|
||||||
```
|
|
||||||
|
|
||||||
- Supported layer binding keys are:
|
|
||||||
- `growx`
|
|
||||||
- `growy`
|
|
||||||
- `leftedge`
|
|
||||||
- `rightedge`
|
|
||||||
|
|
||||||
3. Project YAML parsing:
|
|
||||||
- `_load_project_specs` reads saved `.yml` / `.yaml` files
|
|
||||||
(`../mxpic_router/mxpic_router/builder.py` line 46).
|
|
||||||
- `load_cell_spec` reads YAML
|
|
||||||
(`../mxpic_router/mxpic_router/eda_loader.py` line 80).
|
|
||||||
- `parse_cell_dict` converts dictionaries into `CellSpec`
|
|
||||||
(`../mxpic_router/mxpic_router/eda_loader.py` line 85).
|
|
||||||
|
|
||||||
4. Link xsections:
|
|
||||||
- `LinkSpec.xsection` defaults to `strip`
|
|
||||||
(`../mxpic_router/mxpic_router/eda_loader.py` line 51).
|
|
||||||
- `BundleSpec.xsection` defaults to `strip`
|
|
||||||
(`../mxpic_router/mxpic_router/eda_loader.py` line 66).
|
|
||||||
- Bundle-level `xsection` is read at
|
|
||||||
`../mxpic_router/mxpic_router/eda_loader.py` line 137.
|
|
||||||
- Link-level `xsection` overrides bundle-level xsection at
|
|
||||||
`../mxpic_router/mxpic_router/eda_loader.py` line 142.
|
|
||||||
- If `family` is missing, `_family_from_xsection` maps metal-like xsections
|
|
||||||
to `electrical`, otherwise `optical`
|
|
||||||
(`../mxpic_router/mxpic_router/eda_loader.py` line 171).
|
|
||||||
|
|
||||||
5. PDK asset resolution:
|
|
||||||
- `_resolve_pdk_asset` starts at
|
|
||||||
`../mxpic_router/mxpic_router/builder.py` line 509.
|
|
||||||
- It first tries the saved component path directly under `pdk_root`.
|
|
||||||
- If no direct directory exists, it scans the whole PDK root by component
|
|
||||||
folder basename.
|
|
||||||
- It reads the component YAML if present.
|
|
||||||
- It chooses GDS through `_find_gds`
|
|
||||||
(`../mxpic_router/mxpic_router/builder.py` line 533).
|
|
||||||
- Normal users prefer `<component>_BB.gds`; manager sessions can prefer
|
|
||||||
`<component>.gds`.
|
|
||||||
|
|
||||||
6. Route generation:
|
|
||||||
- `_route_link` starts at
|
|
||||||
`../mxpic_router/mxpic_router/builder.py` line 269.
|
|
||||||
- It creates the route object with:
|
|
||||||
|
|
||||||
```python
|
|
||||||
Route(radius=link.radius or 10, width=link.width, xs=link.xsection, PCB=...)
|
|
||||||
```
|
|
||||||
|
|
||||||
- Optical and electrical routes both use `link.xsection`; metal routes also
|
|
||||||
enable PCB-style routing for `metal_1` and `metal_2`.
|
|
||||||
- Nazca can generate geometry on the correct layers because
|
|
||||||
`apply_technology_manifest` already registered the xsection layer bindings.
|
|
||||||
|
|
||||||
## Current Fallbacks and Observations
|
|
||||||
|
|
||||||
- If a project has no saved technology, the frontend uses
|
|
||||||
`FALLBACK_TECHNOLOGY_MANIFEST`.
|
|
||||||
- If `/api/technologies/<foundry>/<technology>/manifest` fails, the frontend
|
|
||||||
also uses `FALLBACK_TECHNOLOGY_MANIFEST`.
|
|
||||||
- If build-time `technology_manifest_path_for_project` cannot find
|
|
||||||
`technology.yml`, `mxpic_router` receives no manifest path and Nazca
|
|
||||||
registration is skipped.
|
|
||||||
- The library browser is project-scoped by selected technology, but preview and
|
|
||||||
GDS build pass the base role PDK root to `mxpic_router`.
|
|
||||||
- This works because normal saved component paths include
|
|
||||||
`foundry/technology/...` relative to the base role PDK root.
|
|
||||||
- Manually edited YAML with only a component folder name can still resolve, but
|
|
||||||
the router will scan the whole active PDK root and use the first matching
|
|
||||||
folder basename.
|
|
||||||
- The legacy local GDS/SVG preview builder and local PDK GDS registry have been
|
|
||||||
removed from `mxpic_EDA`; active build and preview actions call the
|
|
||||||
router-backed API wrappers instead.
|
|
||||||
Reference in New Issue
Block a user