312 lines
13 KiB
Markdown
312 lines
13 KiB
Markdown
# apsis-book
|
|
|
|
[简体中文](./README.zh.md) | English
|
|
|
|
The web service layer of ApsisRS. It presents the ApsisRS computational
|
|
kernels for commercial spaceflight and satellite internet systems in a
|
|
browser: the concept documentation, a knowledge graph, the interface
|
|
documentation extracted from the library source, and an online tool set that
|
|
runs the kernels on parameters the user supplies.
|
|
|
|
The service builds to a single binary that embeds every asset it serves. It
|
|
does not read a content directory or a frontend build at run time, and it
|
|
does not need either to be present next to the binary.
|
|
|
|
## The running service
|
|
|
|
The service is deployed at <https://apsis.focus1024.com/>. It serves this
|
|
project: the same documents, graphs, interface catalogue, and tools, from the
|
|
same build. Nothing about it is particular to a deployment, because
|
|
everything it serves is inside the binary, so a copy built from the source is
|
|
the same service.
|
|
|
|
The deployed instance opens in Chinese and can be switched to English from
|
|
the top bar.
|
|
|
|
## What it serves
|
|
|
|
| Section | Content | Route prefix |
|
|
|---------|---------|--------------|
|
|
| Concepts | Documents on the time scales, the reference frames, the element formats, and the propagation models, with the units and the applicable range of each | `/api/docs` |
|
|
| Knowledge graph | The concepts and their relationships, plus the crate structure of the library and the algorithm flow graphs | `/api/graph` |
|
|
| Interface | The public interface of the ApsisRS layers the service builds against, extracted from the source doc comments | `/api/api` |
|
|
| Online tools | TLE parsing, SGP4 propagation, track sampling, time scale conversion, coordinate conversion, frame rotation, Keplerian conversion, and Hohmann transfer planning | `/api/tools` |
|
|
|
|
## Design constraints
|
|
|
|
The service follows the constraints of the workspace it belongs to.
|
|
|
|
- **One runtime dependency.** The only entry in `[dependencies]` is
|
|
`apsis-rs`, through a path dependency. The HTTP server, the JSON codec,
|
|
the router, the Markdown renderer, and the graph model are all written
|
|
against the standard library.
|
|
- **No `unsafe`.** The crate does not contain an unsafe block.
|
|
- **No panic in a request path.** A malformed request, a bad parameter, or
|
|
a failing computation produces a response, not a panic. The worker that
|
|
serves a request catches a panic in the handler as a final guard and
|
|
answers with a server error.
|
|
- **Self-contained comments.** Every comment explains the code, the
|
|
algorithm, the interface, or the project. No comment refers to a private
|
|
document, an instruction, or a plan.
|
|
|
|
## Building
|
|
|
|
The frontend must be built before the release binary so that its output can
|
|
be embedded. A release build does this automatically.
|
|
|
|
Every built asset carries a content hash in its name, so replacing the binary
|
|
replaces those names. A browser therefore cannot be holding a stale bundle:
|
|
the new bundle is requested under a URL it has never seen. The entry point
|
|
is served with `no-cache, must-revalidate`, and hashed assets with
|
|
`public, max-age=31536000, immutable`. The service registers no service
|
|
worker.
|
|
|
|
```sh
|
|
# Development: a debug build embeds whatever is in frontend/dist and does
|
|
# not run the frontend build.
|
|
cargo build
|
|
|
|
# Release: build.rs builds the frontend and embeds the result.
|
|
cargo build --release
|
|
```
|
|
|
|
The binary is at `target/release/apsis-book`.
|
|
|
|
Environment variables:
|
|
|
|
| Variable | Effect |
|
|
|----------|--------|
|
|
| `APSIS_BOOK_HOST` | Interface to bind, default `127.0.0.1` |
|
|
| `APSIS_BOOK_PORT` | Port to bind, default `8080` |
|
|
| `APSIS_BOOK_ADDRESS` | Bind address as `HOST:PORT`, overriding the two above |
|
|
| `APSIS_BOOK_WORKERS` | Worker threads, default 4 |
|
|
| `APSIS_BOOK_SKIP_FRONTEND` | Set to `1` to skip the frontend build in a release build |
|
|
| `APSIS_BOOK_FRONTEND_DIR` | Frontend directory, default `frontend` |
|
|
|
|
Command line options override the environment:
|
|
|
|
```sh
|
|
# Bind every interface on port 8080.
|
|
apsis-book --host 0.0.0.0 --port 8080
|
|
|
|
# The combined form is equivalent.
|
|
apsis-book --address 0.0.0.0:8080
|
|
```
|
|
|
|
`--host` and `--port` may be given together. `--address` cannot be combined
|
|
with either of them, because the order in which they would apply is not
|
|
defined. A host may be a literal address, an IPv6 literal in brackets, or a
|
|
name the operating system resolves.
|
|
|
|
### Language
|
|
|
|
The interface is bilingual and opens in Chinese. The reader switches to
|
|
English with the control in the top bar; the choice is stored in the browser
|
|
and applies to every page.
|
|
|
|
The backend carries every user facing string in both languages, so the switch
|
|
takes effect without a request. A concept document is written once in English
|
|
as `content/concepts/<id>.md` and translated as
|
|
`content/concepts/<id>.zh.md`. A document without a translation is shown in
|
|
English to every reader, with a note saying so.
|
|
|
|
## Running
|
|
|
|
```sh
|
|
./target/release/apsis-book --address 127.0.0.1:8080
|
|
```
|
|
|
|
### Docker
|
|
|
|
The image carries the binary and nothing else, because the binary carries
|
|
everything it serves. The build context is the directory above this one,
|
|
because the crate depends on `apsis-rs` by path:
|
|
|
|
```sh
|
|
# From the workspace root.
|
|
docker build -f apsis-book/Dockerfile -t apsis-book:0.1.0 .
|
|
docker run --rm -p 8080:8080 apsis-book:0.1.0
|
|
```
|
|
|
|
The command names the host explicitly. The program defaults to the loopback
|
|
address, where it answers only itself, so a container started without it would
|
|
serve nothing to anyone.
|
|
|
|
`docker-compose.yml` does the same thing with the settings a long-running
|
|
service wants: it restarts unless stopped, mounts the filesystem read-only
|
|
because nothing is written to it, drops the ability to gain privileges, and
|
|
probes the service every thirty seconds.
|
|
|
|
```sh
|
|
cd apsis-book
|
|
docker compose up --build
|
|
```
|
|
|
|
The probe is written against the shell rather than against a tool, because
|
|
the image carries no HTTP client: it carries one binary and nothing else, and
|
|
adding a client for a probe would undo that. `bash` opens the connection,
|
|
writes the request and reads the status line back.
|
|
|
|
There is no `HEALTHCHECK` in the image itself. Probing `GET /health` is what an orchestrator
|
|
should do, and a probe inside the container would mean adding an HTTP client
|
|
to an image that needs no other tool. A Kubernetes probe says the same thing
|
|
without one:
|
|
|
|
```yaml
|
|
livenessProbe:
|
|
httpGet: { path: /health, port: 8080 }
|
|
```
|
|
|
|
The image is built for the architecture it is built on. A multi-architecture
|
|
image would need a builder per architecture; the `ARG`s at the top of the
|
|
Dockerfile are the versions, and `--build-arg RUST_VERSION=` changes one.
|
|
|
|
Open `http://127.0.0.1:8080`. The JSON interfaces are available without the
|
|
frontend:
|
|
|
|
- `GET /health`
|
|
- `GET /api/docs`, `GET /api/docs/<id>`, `GET /api/docs/search?q=`
|
|
- `GET /api/graph`, `GET /api/graph/crates`, `GET /api/graph/flows`, `GET /api/graph/<flow>`
|
|
- `GET /api/api`, `GET /api/api/crates/<name>`, `GET /api/api/items/<name>`
|
|
- `GET /api/tools`, `GET /api/tools/<id>`, `POST /api/tools/<id>/run`
|
|
|
|
A tool call takes a JSON object of parameters and returns a shaped result:
|
|
|
|
```sh
|
|
curl -s http://127.0.0.1:8080/api/tools/hohmann-transfer/run \
|
|
-H 'content-type: application/json' \
|
|
-d '{"from_radius": 6778, "to_radius": 42164}'
|
|
```
|
|
|
|
## Content layout
|
|
|
|
The content is a superset of what any one library implements. It has two
|
|
groups.
|
|
|
|
**概念 / Concepts** describes the field. The layers follow the ones the
|
|
library is built in, because that is the order a reader meets the subject:
|
|
|
|
| Layer | Subject |
|
|
|-------|---------|
|
|
| 导论 Introduction | Scope, organisation, and how to read a page |
|
|
| 基础层 Foundations | Units, dimensions, numerical method, error |
|
|
| 物理层 Physics | Time scales and reference frames, which are two unrelated subjects and are both held here |
|
|
| 轨道层 Orbit | Elements, element formats, perturbations, propagators, manoeuvres |
|
|
| 射频与传播 Radio and propagation | Doppler, link budgets, the medium, interference |
|
|
| 星座与网络 Constellation and network | Constellations, access, topology, handover, routing |
|
|
| 运行层 Operations | Telemetry, tracking, command, planning |
|
|
| 仿真层 Simulation | Scenarios, events, and what a result means |
|
|
|
|
**实现 / Implementation** describes what one particular library implements
|
|
of the above, including how it is organised and where its algorithms come
|
|
from. Keeping it separate is what makes the concept set independent: a page
|
|
about link budgets does not change when a library changes.
|
|
|
|
A document belongs to the layer named by the directory it is in, so adding a
|
|
document means creating it under its layer and listing it in
|
|
`chapters.json`.
|
|
|
|
## Regenerating the interface catalog
|
|
|
|
The interface documentation is generated from the ApsisRS sources rather than
|
|
written by hand. The doc comments there are written in English and then in
|
|
Chinese, and the extractor splits the two halves, so both languages come from
|
|
the comment beside the code and neither can fall behind what it describes. Run the extractor from this directory when the sources
|
|
change, then rebuild:
|
|
|
|
```sh
|
|
scripts/extract_api.sh
|
|
cargo build --release
|
|
```
|
|
|
|
The script reads the workspace at `../apsis-rs` by default; set
|
|
`APSIS_RS_WORKSPACE` to point at a checkout elsewhere. It requires Python 3.8
|
|
or later. The Rust build does not run it and does not depend on Python.
|
|
|
|
## Editing the content
|
|
|
|
| Path | Contents |
|
|
|------|----------|
|
|
| `content/concepts/chapters.json` | The groups, their layers, and the reading order, with bilingual titles |
|
|
| `content/concepts/<layer>/*.md` | Documents, one directory per layer; the directory gives the layer, the file name gives the identifier |
|
|
| `content/concepts/<layer>/*.zh.md` | Chinese translations, named for the file they translate |
|
|
| `content/graph/concepts.json` | The concept graph |
|
|
| `content/flows/*.json` | The algorithm flow graphs |
|
|
| `content/api.json` | The generated interface catalog, in both languages; do not edit by hand |
|
|
|
|
A formula is written between dollar signs, as `$$...$$` for one on a line of
|
|
its own and `$...$` for one within a sentence. That is the notation every
|
|
Markdown mathematics renderer reads, and it is what the documents use: a
|
|
formula is mathematics, and a fenced code block is not the place for it. The
|
|
service translates the notation to MathML, which the browser lays out itself.
|
|
|
|
A document header declares `title`, `summary`, `tags`, and `prerequisites`.
|
|
The identifier and the layer are not declared: the identifier is the file
|
|
name and the layer is the directory, so the two cannot disagree with the
|
|
tree. A cross reference is written `[[id]]` or `[[id|label]]` and renders as
|
|
a link that the frontend routes without a page load.
|
|
|
|
## Layout
|
|
|
|
```text
|
|
apsis-book/
|
|
├── build.rs embed the frontend build and the text content
|
|
├── Cargo.toml one runtime dependency: apsis-rs
|
|
├── content/ authored documents, graphs, and generated catalog
|
|
│ └── concepts/<layer>/ documents, one directory per layer
|
|
├── Dockerfile the release image, in three stages
|
|
├── Dockerfile.dockerignore what the image build is not sent
|
|
├── docker-compose.yml runs the image with the settings it wants
|
|
├── frontend/ Svelte and TypeScript, built by Vite
|
|
│ ├── src/lib/ API client, router, components, formatting
|
|
│ ├── src/pages/ one component per route
|
|
│ └── dist/ build output, embedded by build.rs
|
|
├── scripts/extract_api.sh interface catalog generator
|
|
├── scripts/render_audit.mjs renders every route and reports what is wrong
|
|
├── src/
|
|
│ ├── api/ the router and the JSON payloads
|
|
│ ├── docs/ content, Markdown, documents, interface catalog
|
|
│ ├── embedded/ access to the embedded frontend assets
|
|
│ ├── graph/ the knowledge graph and the flow graphs
|
|
│ ├── http/ request, response, and server
|
|
│ ├── tools/ the tool registry, the handlers, the result shape
|
|
│ ├── json.rs the JSON codec
|
|
│ ├── lib.rs the modules, as a library
|
|
│ ├── testkit.rs the few internals the tests reach
|
|
│ ├── main.rs the process entry point
|
|
│ └── state.rs the parsed content, shared by every worker
|
|
└── tests/ every test; `src/` holds none
|
|
├── service.rs end to end tests against the built binary
|
|
└── <module>.rs the module tests
|
|
```
|
|
|
|
## Testing
|
|
|
|
```sh
|
|
cargo test # unit tests and end to end tests
|
|
cargo clippy --all-targets
|
|
cargo fmt --check
|
|
cd frontend && npm run check
|
|
```
|
|
|
|
A release is also checked by rendering the interface: `node
|
|
scripts/render_audit.mjs` opens every route in both languages, both themes,
|
|
and two widths, and reports a page that does not load, a page with almost no
|
|
text, a page that scrolls sideways, or a formula that loses a character. It
|
|
needs a Chrome and a release build, writes what it finds to
|
|
`/tmp/matrix.jsonl`, and resumes rather than starting over if it is
|
|
interrupted.
|
|
|
|
The end to end tests start the built binary on an ephemeral port and make
|
|
plain TCP requests against it.
|
|
|
|
## License
|
|
|
|
GNU General Public License, version 3 or later, and that alone. See
|
|
`LICENSE`.
|
|
|
|
The service depends on `apsis-rs`, which is offered under MIT or
|
|
Apache-2.0. Both of those permit redistribution inside a GPL work, so the
|
|
two are distributed together under these terms. No other licence is
|
|
offered for this service.
|