feat(api): let the console ask which export formats this build can run - #152
Open
TheMeinerLP wants to merge 2 commits into
Open
feat(api): let the console ask which export formats this build can run#152TheMeinerLP wants to merge 2 commits into
TheMeinerLP wants to merge 2 commits into
Conversation
`GET /api/export-formats` answers with every format the registry knows of, each carrying its name, whether this build can actually run it, and the sink family that carries its bytes. Until now the only way to learn any of this was to configure a destination and read the `supported` list off the 400 -- which the console throws away, because `sanitiseFetchError` keeps nothing of a failed response but its status. `pdf` and `confluence` move from a paragraph in the module docstring into `UNBUILT`, beside `FORMATS`, and `catalogue()` reads both halves out. They were documented as unbuilt in three places that had no way to correct each other; building either would have falsified two of them with nothing failing. Moving a name between the two structures is now the single edit in the single file that `export_formats` always promised it would be. Three fields on the wire and no fourth. `media_type` is read inside this process, a label is a word in a language with no catalogue here, and a `target_pattern` is a Python regular expression -- handing one to a caller to re-compile hands over a dialect rather than a rule.
…lt ones included The destinations page asks `GET /api/export-formats` and hands the answer to the destination form. `EXPORT_FORMATS` -- the hard-coded array shipped in #150, with its own copy of every format's name, note, target kind and readability -- is gone, and so is `TargetKind`, which was this console's private word for what the API already calls a sink family. What stays here is words: a name-to-translation-key map, because the API has no message catalogue and cannot serve "Outline-Dokument" to one reader and "Outline document" to another. A format this build cannot run is now a disabled row saying so, where #150 made it absent. That PR's decisive argument was that the console could not see the registry, so a "PDF, not built" row would have been a claim about a build it could not inspect -- one it would have gone on making after `pdf` was built. Neither holds now: the row is the deployment's own answer and stops being unavailable the moment the deployment says so. Its other argument, that such a row is a trap under the cursor, is an argument about a row that looks choosable; this one is stepped over by the keyboard, rendered unchoosable, and refused by `draftProblems` with the reason under the field. What is left is that PDF exists and this build has none, which is what somebody who came here looking for PDF needs told. The two properties #150 tested are unchanged. A stored format the catalogue does not report keeps its row with its raw name and stays choosable while that destination is edited, and `primaryTarget` filters against nothing -- which matters more now, not less: the reported list can arrive empty because the request failed, and a page that filtered against it would rewrite every destination in a guild the first time it did.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Gives the API a way to say which export formats exist and which of them this build can actually run, and makes the destinations page ask instead of believe.
What it does
GET /api/export-formatsanswers with every formatsturnus.application.export_formatsknows of —outline,markdown,html, and alsopdfandconfluence, which are specified in §3.4 and deliberately not built. Each row carries three things: thenamethatguild_export_target.formatstores, anavailableflag, and thesinkfamily that carries the bytes (outline,object_store, ornullwhere nothing has decided).The console's destinations page reads it once per visit and hands it to the destination form.
EXPORT_FORMATS— the hard-coded array #150 shipped, with its own copy of every format's name, note, target kind and readability — is gone. So isTargetKind, the console's private word for what the API already calls a sink family. What is left on the console side is words: a name-to-translation-key map, because the API has no message catalogue and cannot serve "Outline-Dokument" to one reader and "Outline document" to another. A format the deployment reports and this console has no word for renders as its own raw name.Two properties #150 argued for and tested are unchanged, and one of them is stronger now. A stored format the catalogue does not report keeps its row with its raw name and stays choosable while that destination is being edited;
primaryTargetis still enabled-and-oldest with no filtering against any list. There is now a way for the reported list to arrive empty that did not exist before — the request failing — so a page that filtered stored values against it would rewrite every destination in a guild the first time the endpoint timed out.Why it is shaped that way
The registry gained a second half rather than a second home.
pdfandconfluencewere documented as unbuilt in theexport_formatsmodule docstring, restated as an absence in the console's format array, and restated a third time as an English sentence under the picker. Three copies of one fact, and buildingpdfwould have falsified the second and third with nothing failing anywhere. They are now named once, inUNBUILT, besideFORMATSin the module that already promised adding a format would be one edit in one file.catalogue()reads both halves out; a test asserts a name cannot be in both.Three fields on the wire, and each of the candidates for a fourth was rejected for a reason.
media_typeandfile_extensionare read by the object-store sink and by the route that serves an artefact back, both inside this process. A label is a word in a language this process has no catalogue for. Andtarget_patternis a compiled Python regular expression: handing one to a browser to re-compile hands over a dialect rather than a rule — Python and JavaScript disagree about\dunder Unicode, about what a bare$matches, about inline flags — so the sink family goes over instead and the console keeps one pattern per family. That copy was always a courtesy rather than a control, and_requested_targetstill refuses a bad target whatever the console believes.The route lives in
routes_exportsand behind a session. It is the only one there that names no guild, and it is there because it reads the registry those five routes enforce: a caller cannot be told what a create will accept by any list except the one the create accepts from. A test asserts the catalogue's available set equals thesupportedlist a 400 carries, so the two answers cannot drift into disagreement. Behindrequire_sessionfor the reasonroutes_setupgives about the invite link — public is not the same as unauthenticated, and an endpoint of this API that answered without a session would be the only one. It answers somebody who administers no guild, unlike every other route in the file, because it is a fact about the binary and refusing it would only mean the console could not draw a format picker until it had first found a guild. No audit line: there is nothing an access record could establish about a compiled-in constant, and no guild id to scope one to.An unavailable format is now a disabled row that says why, and this reverses #150. That PR chose absence, and its argument had two halves. The decisive half was that the console could not see the registry: a "PDF — not built" row would have been the console asserting a fact about a build it cannot inspect, and it would have gone on asserting it after
pdfwas built, until somebody remembered to edit a second list. Neither is true any more. The row's text is the deployment's own answer, and it stops being unavailable the moment the deployment says so, with nothing in the console to change.The other half — a dropdown row that a save would refuse is "a trap laid under the cursor of somebody deciding" — is an argument against a row that looks choosable and is not. This one does not look choosable.
UiOption.disabledis a first-class state in this console:stepEnabledwalks past it, the control renders it unchoosable, anddraftProblemsrefuses a draft naming an unavailable format so that Save is off and the reason sits under the field rather than arriving later as a bare 400 thatapiErrorhas stripped of its explanation. What is left is the fact that PDF exists and this deployment does not build it — which is what somebody who came to this page looking for PDF needs to be told, and what an absent row leaves them to conclude wrongly. "Sturnus has no PDF" and "this build has no PDF" are different things to walk away believing, and only the second is true.The precedent that cuts the other way,
video_consent_offered, does not reach this case. That is a guild's policy choice about its own server, and an absent option there is a correct statement about that guild. This is a property of the binary, which is not something an administrator can go and change — so telling them it exists and is not here is the only version of the sentence that helps.One exception, and it is about the control rather than about honesty: where the unavailable format is the one the destination already stores, its row is left choosable. A disabled row that the picker's own value sits on is a control pointing at something it may not select, which is how a picker silently reports the wrong value.
The sink family is what decides the address field, so a format built tomorrow needs no console change.
formatMarkdownNoteandformatHtmlNoteheld the same sentence twice, which is what a per-name table decays into; they are onesinkObjectStoreNotenow. Label, hint, note and whether the console can serve the bytes back all follow the family. A format with no reported family gets a neutral "Where it writes" label and a plain box — the old code fell back to the object-store wording, which stated a rule ("letters, digits, dot, dash…") that nothing had said applied.What it deliberately refuses to do
available: false. "This console has not been told about it" is evidence of nothing, and an unreadable catalogue must not make every stored destination look like a broken configuration.sessionDocumentsasks for the word for a format and nothing else. That page renders what was already published — the document exists and its link works — so whether this build still produces that format is not a fact about the row, and a request per recording view would buy nothing.routes_exportsreads or writes one guild's configuration; this one reads a constant identical for every administrator on the deployment.pdforconfluence. The reasons inexport_formatsstand: a PDF renderer is a large native dependency in an image that today holds Python and a Whisper model, and Confluence is a second wiki API with its own auth and its own document model. This change only makes their absence something the deployment says rather than something the console assumes.Checks
Measured on
eed19d7(v0.16.0) before and on this branch after.pytest -q -m "not slow"2742 → 2754 passed, 4 deselected (12 new) ·vitest run1719 → 1735 passed (16 new) ·ruff check .clean ·mypy srcclean, 140 files ·eslint .clean ·nuxt typecheckclean ·nuxt buildclean.