Commit Graph

43 Commits

Author SHA1 Message Date
dev 081c7d9275 Add the release image
The service embeds everything it serves, so the image carries one binary and
nothing else. The build is in three stages: the frontend is built with Node,
the binary is built with Rust, and the runtime holds the binary and the C
library it links against.

The build context is the directory above this one, because the crate depends
on `apsis-rs` by path; the ignore file is named beside the Dockerfile so that
it applies to that context without living in it. What it keeps out is the
build output of both projects, the installed dependencies, and the working
notes — the first two because the image produces its own, and a copy from the
host would replace the newly built one.

Two things the image has to get right, and both are stated where they happen.
The command names the host explicitly, because the program defaults to the
loopback address where it answers only itself, so a container started without
it would serve nothing to anyone. And the toolchain file is removed inside
the builder, because it asks for the `stable` channel by name and rustup
would fetch it over the network; the image already provides the toolchain the
project builds with.

There is no health check. 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.

The image could not be built here: this machine cannot reach Docker Hub, so
no base image can be pulled. What was checked is the definition itself, which
the builder parses, and every command in it, which was run directly from the
workspace root — the frontend build, `cargo build --release --locked` with
the frontend build skipped, and the binary started with the host and port the
image uses.
2026-10-02 14:24:27 +08:00
dev f8954e6381 Finish the comments in both languages, and take the render audit in
The last three modules — the document model, the Markdown renderer, and the
formula renderer — were the largest, and every file with comments now
carries them in English and then in Chinese, as the library does. Three
thousand eight hundred and seventy comment lines across forty-three files.

The audit that renders every route is now part of the project rather than a
script kept outside it, because a release needs it and the next person
should not have to write it again. It is documented in the README beside
the other script.

Two faults of my own were found and fixed while writing the comments. A
blank line between two doc comment lines detaches the second block from the
item it documents; the compiler says so and it was said of ninety-three
such lines, which are now empty doc lines. And a few blocks were inserted
with the wrong indentation, which  then tidied.
2026-10-02 13:52:09 +08:00
dev 89f1b0bc9a Write the comments in both languages, part eight 2026-10-02 13:37:46 +08:00
dev 8b37a187aa Write the comments in both languages, part seven 2026-10-02 13:36:29 +08:00
dev e899a1f520 Write the comments in both languages, part six 2026-10-02 13:34:23 +08:00
dev 5592e127e9 Write the comments in both languages, part five 2026-10-02 13:32:41 +08:00
dev b30203fc29 Write the comments in both languages, part four 2026-10-02 13:31:14 +08:00
dev 6bc8b1e355 Write the comments in both languages, part three 2026-10-02 13:28:02 +08:00
dev e6065dd30e Write the comments in both languages, part two
All twenty test files and the tool handlers. The tests that share the
paragraph explaining why they live in tests/ now share its translation
too, so the same four lines are written once per file rather than
paraphrased.
2026-10-02 13:25:02 +08:00
dev a360238a95 Write the comments in both languages, part one
The comments in this service were written in English alone, while the
library it belongs to writes every comment in English and then in Chinese.
The smaller modules are done here: the library root, the bilingual text,
the shared state, the embedded content and assets, the three HTTP modules,
the entry point, the test seams, the tool labels, and the interface
catalogue reader.

A doc comment carries the English text, an empty doc line, and then the
Chinese. An inline comment carries the English line and then the Chinese.
Both are how the library already writes them.

The empty doc line matters and was wrong at first. A blank line between
the two halves is not the same thing: it detaches the second half from the
item it documents, and the compiler says so. A hundred of them were
rewritten as empty doc lines.
2026-10-02 13:17:41 +08:00
dev 34c0789f2c Remove the working screenshots and name the page after the service
Three things found in the check before a release. A directory of
screenshots taken while working on the interface had been committed; it
is removed and ignored, because it is a working note rather than part of
the service. Two branches kept pointing at the history from before the
licence changed, which is the only place that history was still reachable;
they are gone. The page title still named the service the way it was named
before it became apsis-book.

The binary was checked from an empty directory with no content tree and no
frontend build beside it: it serves every route from what it carries.
2026-10-02 13:05:37 +08:00
dev c869a2145d Take the interface formulas from the library, now written as math
The library writes its formulas between dollar signs, so the catalogue
generated from it carries them that way and the interface documentation
draws them as mathematics. Eighty-eight of them, in both languages.

Two things this confirms about the change made here. The reader that
guessed whether a code block held a formula has gone, and with it the
defect that led to removing it: a Rust example in a doc comment, in a
fence with no language on it and holding an equals sign, was being drawn
as a formula. It is drawn as a code block again, which is what it is.

The check on the formulas was looking for a script marker anywhere in
the text a page draws, which reported the escaped underscore of
`\text{body\_radius}` as an unattached subscript. It looks for the marker
where it would actually mean that, as an operator left in the output, and
the escaped underscore is what it is: the name of a parameter, drawn as it
was written.
2026-10-02 12:56:00 +08:00
dev f117f20930 Write the notation in the text, not in a code span
A quantity was written in an inline code span, as in `0.002 m^2/kg`, so
the caret was drawn literally and the span looked like code. A hundred and
ninety-five of them are now inline formulas, and with them the powers of
ten, the Greek letters, the expressions with an operator in them, and the
symbols with a subscript. Single letters went the same way, except the
three that name a classification in the format, which are data rather
than mathematics and stay monospaced.

Drawing a code block as a formula was removed rather than refined. The
guess was wrong in practice: a doc comment in the library carries a Rust
example in a fence with no language on it, and the example holds an equals
sign, so the page drew a program as a formula and reported the underscores
in its identifiers as script markers. A code block is now drawn as a code
block and nothing else, and the reader that guessed is gone, with five
hundred lines of parser and twenty-three tests that tested it.

The check on the formulas was comparing the source with itself. The source
is carried beside the MathML for a reader to copy, and both the Rust check
and the audit were reading the whole span, so a formula that lost every
number still passed. Both now take the MathML alone, and the audit counts
only the text of a leaf element, because a browser drops text that has no
element around it and counting that text made the check pass whatever the
renderer produced. With the check made real it found the program drawn as
a formula, which is what led to the guess being removed.
2026-10-02 12:45:56 +08:00
dev ec92981257 Attach the script to the group it belongs to
A bracketed group did not take the power that followed it. The branches
for `(`, `[` and `{` returned from the reader before the call that
attaches superscripts and subscripts, so `(a + b)^2` was set with the
caret as an ordinary symbol and the 2 beside it, and
`\left( x \right)^{2/3}` came out as `(x)^2/3` with the caret visible.
Inside a fraction it was worse: `\frac{1}{\left(1 - e^2\right)^{3/2}}`
put the `3/2` after the fraction, so the exponent hung outside it. A
bracketed group is a base like any other and is now given its scripts by
the same call as everything else.

Two more of the same kind were found while fixing it. A style command
such as `\mathbf` dropped the subscript of `\mathbf{a}_{...}`, and the
vertical bars were not known at all: `\lvert` and `\rvert` were shown as
the letters of their own names, which also lost the power after them.

The check on the formulas compared the characters and so missed all of
this, because every character was present and in the wrong place. It now
also asserts what the notation means: a caret and an underscore say what
to raise or lower and are never drawn, so one that reaches the output is
a script that was never attached. That single assertion found the two
further cases on its first run.
2026-10-02 12:20:12 +08:00
dev ec78ac009b Let a formula be copied as the formula
Copying a formula from a page gave one character per line: the SGP4 a_1
block arrived as a hundred and thirty-two lines for a hundred and
thirty-two characters. That is how a browser copies MathML, which is one
element per token and is walked one element at a time.

The rendering was not the fault. The block was measured in place and the
six rows each hold their two cells side by side, with the tokens of a row
sharing a baseline, and a screenshot agrees. What was wrong is what a
reader gets when they take the formula away.

The source is therefore carried beside the MathML in an element that can
be selected, and the MathML is taken out of selection. A copy now yields
the notation the formula was written in, on one line, which is what a
reader can paste into a tool that understands it.

The source is trimmed when it is rendered, because the delimiters of a
display formula sit on their own lines and the newline after the opening
pair was arriving as a leading space.
2026-10-02 12:06:11 +08:00
dev 9caa542b52 Write the formulas in the notation Markdown has for them
A formula was written as a fenced code block, which is what a plain text
file allows and is not what Markdown has for mathematics. A code block is
code: the reader of the source sees a formula presented as code, and the
renderer had been taught to guess which blocks held equations, which is a
guess that is wrong whenever a block holds something else.

Every formula now stands between dollar signs, which is the notation every
Markdown mathematics renderer reads, and the guess is gone from the
renderer. A hundred and four blocks became two hundred and four formulas
in the two languages.

Three things that were in a code block were not formulas and are now what
they are. A paragraph of geometry was prose. A table of the exponent
notation is a table, because its two columns are a written form and what
it means. Two blocks are left as they were: the layer diagram, which is a
diagram, and a sample element set, which has to keep its column alignment.

The renderer learned what the notation needs and did not have: matrix
environments, with an ampersand between the columns of a row and a double
backslash between the rows; the floor and ceiling marks; the modulo
operator; a mark above an argument, as in the time derivative of Omega and
a barred coefficient; the text command, whose argument is words and was
being set one letter at a time; and the spacing commands.

The check on the formulas covered the concept documents and not the
interface documentation, so every formula there was unexamined. It covers
both now, and it treats a command name and an environment name as
notation, which is what they are: neither is drawn as the letters of its
own name.
2026-10-02 11:52:17 +08:00
dev 73b5f3de63 Wrap a long identifier, and add the rendered page to the audit
A doc comment names an item by its full path, as in
`NoiseTemperature::from_noise_figure_db`. A path has no space in it, so
the column could shrink and the word could not, and the interface page of
a crate scrolled sideways on a phone. The body of a document now breaks a
word rather than overflow.

The audit that renders every route now covers the whole interface: 744
combinations, being ninety-three routes in both languages and both themes
at two widths, and it passes. It reports four things, which are the four
ways a page has failed here before: a page that does not load, a page
with almost no text, a page that scrolls sideways, and a formula that
loses a letter or a digit.

Two mistakes of the audit itself are fixed rather than worked around.

It awaited a `Runtime.evaluate` that called `location.reload`. Such a
call destroys its own execution context, so the reply never arrives, the
promise never settles and the run hangs until the environment kills it.
The preferences are now set once per language on a page that stays put,
and each route is reached by an ordinary navigation.

It gave the browser a function whose comment contained a backtick. The
backtick closed the template literal that held the function, so the script
did not parse at all.

The formula comparison now treats a command name as notation, as it
already did for the operators: \Delta draws a Greek letter and \sqrt a
radical sign, so neither is drawn as the letters of its own name.
2026-10-02 11:22:02 +08:00
dev 4957368aa3 Give every piece of a formula an element, and check the page for it
A formula is written as plain text and translated to MathML. Text left
bare inside a row is not drawn by the browser, so a token without an
element disappears: every multi-digit number did. `a = floor((14 - month)
/ 12)` came out as `a = floor ( ( - month ) )`, and `year + 4800` as
`year + `. The markup looked reasonable at a glance, and the numbers were
simply gone.

The classification now covers every kind of text and has no bare output.
A number of any length is `mn`; one letter of any script is an identifier,
so Greek stays a variable rather than becoming upright text; an operator,
including one written with its spacing, is `mo`; a space is an explicit
`mspace`; and anything else is `mtext` rather than nothing.

A test walks every document in both languages and compares the characters
the page would show against the characters the formula was written with.
It found three more formulas dropping a sign, and it now guards the whole
set: ninety-six formula blocks, checked on every run rather than by
looking at a page. Only letters and digits are compared, because the
other characters in a formula source are notation — `^`, `_`, `*`,
`/`, braces, and the word `sqrt`, which is drawn as a radical sign.
2026-10-02 10:42:51 +08:00
dev 196727c778 Take the interface translation from the apsis-rs doc comments
`apsis-rs` now writes each doc comment in English and then in Chinese, in
the one comment beside the code. The extractor splits the two halves as it
reads them and writes both to the catalogue, so the interface
documentation carries both languages and no translation file is maintained
beside it.

This replaces `content/api.zh.json`, which held the Chinese keyed by item
name. That arrangement was unsound: a name is not unique. `new` is
defined by fifty-nine crates and does not mean the same thing in each, so
a translation filed under the bare name could be shown against the wrong
documentation. Splitting the comment removes the question, because the
translation is attached to the item rather than looked up by a name.

The coverage went from 434 of 1202 items to all of them, and the crate
summaries from 15 of 40 to all of them. A test now asserts that no item in
the committed catalogue is left without Chinese, so a crate that is not
translated fails the build rather than reading as English to a Chinese
reader.

Two defects surfaced while doing this.

The JSON codec decoded one string character by validating the whole
remaining buffer as UTF-8. That is correct and quadratic, and it repeated
for every character of the document: the catalogue took twenty-nine
seconds to parse once it grew, and the service could not start. It now
validates only the sequence at hand, which is six milliseconds.

The module documentation was joined into one line before it was split,
which destroyed both the paragraph structure and the boundary between the
languages, so the English half came out empty and the Chinese half held
the English text.
2026-10-02 09:47:02 +08:00
dev ff69c1cd63 Move every test out of src, as apsis-rs does
The service is a library with a thin binary over it, which is what makes
the move possible: an integration test is a separate crate and reaches
only what the library makes public, so the modules now live in
`src/lib.rs` and `main.rs` is the entry point. `src/` holds no test
code, which is the shape `apsis-rs` already has.

Eleven items are internal to a module and a test reaches them anyway.
Those stay `pub(crate)` where they are defined and are offered through
`src/testkit.rs`, which is the one place a caller is expected to look for
them. A wrapper is also the only way to offer a private thing outward:
Rust does not let visibility be widened twice, so a `pub(crate)` item
cannot be re-exported as `pub`. That keeps the seams in one file instead
of scattering them through the modules under test.

The 146 module tests and the 23 end to end tests all still run, which is
the number this move had to preserve: the 146 are spread over eighteen
files in `tests/`, one per module that had them.
2026-10-02 01:56:21 +08:00
dev ed2b779d7b Reformat after the rename
`APSIS_BOOK` is one character longer than `APSIS_WEB`, which pushes a
few lines past the width rustfmt allows.
2026-10-02 01:29:36 +08:00
dev cd2f4de8ba Rename the service to apsis-book
The service was named for what it is: a web service. It also serves the
concept documentation as a book, which is the larger part of what a
reader comes for, and the name is shorter without losing what the
service does.

The crate, the binary, the environment variables, the storage keys, the
help text, the documentation, and the history all carry the new name.
2026-10-02 01:24:42 +08:00
dev 0a8f00a8fa web: license as GPL alone, and give every document a Chinese form
The service was offered under MIT or Apache-2.0. It is now under the
GNU General Public License, version 3 or later, and that alone: the two
other licence files are removed and the manifest names one licence.
`apsis-rs` is still MIT or Apache-2.0; both permit redistribution
inside a GPL work, so the two may be distributed together under these
terms.

Every document now has a Chinese form. The concept set already had one
for each of its thirty-one documents; the README and the design notes
did not, and both now do.

The design notes carried two sections under the same heading and two
under another, so the second copy of each was unreachable from the
outline. They are merged into one section apiece, keeping every distinct
decision and dropping the two lead-ins that said the same thing twice.
2026-10-02 00:55:00 +08:00
dev 7d2cf4ba73 web: fix four defects the audit found
A bracketed group was never given its power, so `(1 - e^2)^2` and every
line of that shape were shown as source. An operator written with its
spacing was emitted as bare text, and a browser drops bare text inside a
row, which lost the sign of a difference: `1 - e^2` came out as `1e^2`.
A parenthesis holding words was read as a note about the equation rather
than as part of it, so `(cos theta)` and `(m * g)` broke the lines that
held them; the parser now decides, and the column a note is set in is
what marks it. A vertical bar ended an absolute value only from the
outside, so `| f'(x) | * | x| / | f(x) |` did not read.

A grid track of `minmax(28rem, 1fr)` is wider than a phone, so the
interface page of a crate scrolled sideways. The track now takes
`min(28rem, 100%)` and shrinks to the width it has.

The equations of the documents went from seventy being set as formulas
to ninety-six. What is left is pseudocode and matrices, which are source
rather than formulas.
2026-10-02 00:35:10 +08:00
dev c61f453d9e web: key a translation by its crate, and drop the facade from the catalog
A translation was filed under the item name alone. The name is not
unique: `new` is used by fifty-nine crates and `kind` by fifteen, and
they do not all mean the same thing by it, so a name-only translation
could be shown against the wrong documentation. A translation is now
looked up by `crate::name` first, then by `Type::name` for two items
of one crate that share a name, and only then by the bare name.

The facade crate sits directly in `crates` and re-exports every layer.
It defines nothing of its own, so listing it repeated every item of the
layers under a second name. The generator skips it and the catalog
carries forty crates.

The first batch of Chinese translations is in, covering the foundation,
time, coordinate, geodesy, frames, Kepler, SGP4 and linear algebra
crates.
2026-10-01 23:44:55 +08:00
dev 63c77aff69 web: read the formulas the documents already write, and take the crates from the manifest
The documents write their formulas as plain text in a fenced block,
`delta_v = v_e * ln (m_0 / m_f)`, which was shown as source. They were
written before the dollar form existed. A plain block holding equations
is now read as a formula: identifiers with subscripts and powers, roots,
functions, quotients, a group raised to a power, and a note set beside
the equation. A block that is not an equation, such as a matrix or a
worked example in prose, stays as it was written.

The interface catalog named fifteen crates in the generator, which was
written when only the foundation, physics and orbit crates existed. The
workspace has since grown to forty-one, and every network, radio and
operations crate was missing from the documentation without anything
saying so. The crates now come from the workspace manifest, so a crate
cannot be left out by omission again. The count went from 424 items to
1202, and the index now shows all seven layers.

A crate with no layer is the facade, which re-exports every layer. It is
left without one rather than filed under the foundation, and it is left
out of the crate structure diagram, where it would belong to no layer.
2026-10-01 23:24:56 +08:00
dev 50d6ecc9fd web: scroll to the top on navigation, organize the interface by layer, and set the formulas in the tool output
A page change did not move the scroll position, so following a link from
the bottom of a long document landed in the middle of the next one. The
scroll now follows the path rather than the fragment, so a filter applied
to the page a reader is on leaves them where they are. The browser fires
`popstate` for a fragment navigation as well as for a traversal, so
there is no event that means "this was a traversal" on its own, and going
back starts at the top rather than where the reader left.

The interface index listed fifteen crates in one grid. It is now grouped
by layer, with the title and order the concept documentation already
declares, and a card shows the opening sentence of the crate summary
rather than the whole of it, which is what made the cards four hundred
pixels tall.

A tool result showed its column headings, scalar labels and notes in the
reader's language, but left the table body in English. Text cells are now
looked up as well. Units are written with a power, and are set as one.

A tool description is Markdown, so it is now rendered as such, as are the
parameter help and the notes. A formula written between dollar signs is
set the same way in both places.
2026-10-01 22:46:56 +08:00
dev 17e99428de web: split the physics layer, render formulas, and fix the graph load
The graph took over three seconds to draw. The force layout ran four
hundred and twenty steps of repulsion between every pair of nodes, which
is quadratic in the node count and was doing that work through a reactive
proxy inside an effect that read the state it wrote, so it rescheduled
itself on every run. The repulsion now runs on a grid over neighbouring
cells, and the effect no longer tracks its own writes.

The physics layer covered two unrelated subjects, so it is presented as
two. Formulas are written between dollar signs and translated to MathML,
which the browser sets itself, so no script has to run. The palette is
now two sets of the same variables with a toggle between them, and below
a thousand pixels the sidebar is a drawer over the page.
2026-10-01 17:28:43 +08:00
dev 68eaaf6b20 web: restore the layout variables, align the sidebar rows, and name the layers 2026-10-01 15:55:19 +08:00
dev c726dd1ecc web: describe the field by layer and separate it from the implementation 2026-10-01 13:57:23 +08:00
dev 3a3caf5496 web: give a tool its own page instead of sharing the index 2026-10-01 11:31:26 +08:00
dev b4ed846f73 web: fix the outline anchors, level the outline, and fold the sidebar chapters 2026-10-01 11:00:09 +08:00
dev bc662bc492 web: cancel the sibling margin inside a card grid 2026-10-01 10:28:20 +08:00
dev 829825ac73 web: give every card on a page one size 2026-10-01 09:38:16 +08:00
dev 87dd6b78d0 web: name assets with a content hash so a stale bundle cannot be served 2026-10-01 09:06:10 +08:00
dev 839162dc3e web: align the interface layout and translate the remaining captions 2026-10-01 08:46:46 +08:00
dev 3d716469d1 web: correct the asset cache policy, serve the flow index, and drop a stray artifact 2026-10-01 07:35:56 +08:00
dev 65b0b690f7 web: fix the tool form, the search box, the toc anchors, and translate the interface 2026-09-27 10:29:10 +08:00
dev b4c9627cbb web: fix the interface layout and add Chinese search 2026-09-27 09:45:26 +08:00
dev b7e5222bcd web: add a bilingual interface with Chinese as the default 2026-09-27 09:13:52 +08:00
dev 0415a5fe6f web: bind with --host and --port 2026-09-27 09:13:47 +08:00
dev 25f9b28e9e web: register the hohmann transfer tool and check graph references 2026-09-27 00:54:28 +08:00
dev 2a14c59264 web: add the apsis-web service, content, and frontend 2026-09-27 00:53:20 +08:00