15 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
28877ba0c4 |
feat: My Resources takes Details — what to cover, at length, folded away until needed
Topic is one line and Instructions say how; neither is the place for a list of twelve topics or a case to build around. Details is: optional, collapsed by default, up to 12,000 characters, quoted into the prompt as the author's own account of what the resource must cover — material, not commands, in both the deck and the markdown shapes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU |
||
|
|
5f738fbe30 |
feat: a theme is shown by a sample deck you download, not a picture
A picture of one slide answers a narrower question than the one a theme picker is asked. What a person wants to know is what a deck will look like in this theme — all of it, at the size it will be shown, with the fonts substituted the way they will be. One rendered slide showed one layout, in content someone then read instead of looking at. So: a sample deck per theme. Every layout the renderer can draw — title, bullets with a sub-point, both two-column forms, table, callout, figure, full-slide figure, section divider, a custom slide with shapes, an arrow and a chart, and a references slide — with filler text throughout. Download it, open it, see the theme. The text is deliberately meaningless. Clinical content in a specimen invites you to read it, and then you are judging the teaching rather than the type; that is what the old croup slide got wrong. No engine. The previous preview needed a Gotenberg round trip and a pdftoppm to produce a PNG, cached on disk because of what it cost, and could fail in ways a missing picture cannot explain. python-pptx builds the file in ~330ms and PowerPoint draws it. The link is a plain anchor, so it is there whether or not anything on the server is well. The figure placeholder rides the same path a generated figure does — attachFigures downgrades a figure slide with no picture to bullets and an image slide to a section, so without it the sample would silently stop showing those two layouts. Verified: all five themes build, 11 slides, the chart is a real chart and the placeholder embeds as real media. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU |
||
|
|
59226f2109 |
feat: deck themes — a palette you pick, previewed by the renderer itself
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 46s
Forgejo Docker Build / Root app tests (push) Successful in 46s
Forgejo Android APK / Build signed APK (push) Successful in 2m13s
Forgejo Docker Build / Build Docker image (push) Successful in 9s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
A deck had exactly one look. The slide vocabulary is structural — bullets, compare, table, callout, figure — and none of it carries a colour, so "make it yellow" had nowhere to land but the image prompts, and produced yellow figures on a blue deck. A theme is a palette and a typeface in assets/deck-themes.json. render_pptx.py rebinds INK, MUTED, ACCENT, RULE and PAPER from it in one place, so every slide builder follows without a line changing in any of them — five themes restyle ten slide types for free. An unusable theme leaves the default standing, because a deck in the wrong colours beats a deck that will not render. The theme rides on the deck, which is already the renderer's spec, so nothing has to thread it through. It is validated against the same catalogue the renderer reads: an id the renderer would ignore is never stored, so a deck cannot claim a look it does not have. PUT /my-resources/:id/theme re-skins a stored deck — a column write, no model call, nothing that can reword a slide — and the next download is in the new colours. Offered in the library only on rows that have a deck; flat markdown has no palette. Previews are rendered by the renderer, one representative compare slide per theme, cached because each costs a pptx render, a Gotenberg round trip and a rasterise. Drawn rather than mocked up: a hand-made swatch drifts the moment a palette or a layout changes, and a preview that is not true is worse than none. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU |
||
|
|
bd8e413bc7 |
fix: an assistant attachment must be the image type it claims to be
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 59s
Forgejo Docker Build / Root app tests (push) Successful in 50s
Forgejo Android APK / Build signed APK (push) Successful in 1m56s
Forgejo Docker Build / Build Docker image (push) Successful in 9s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
The MIME type was taken on trust here. Anything at all could be posted as image/png: it passed the size and base64 checks, was stored in the saved chat, and was handed to a provider as a data URI. Documents and S3 uploads have always been sniffed by fileType.js; this was the one upload path that was not. Now sniffed with the same helper, so there is one idea of what a PNG looks like. A PHP payload, a shell script, an ELF or PE binary, a zip, or a real PDF labelled image/png are all refused with a message that says what is wrong. What this does not claim: bytes hidden after a valid PNG header still make a valid PNG, and no sniffer can promise otherwise. The protection is that the file is never executed and never served as anything but an image. Existing fixtures used buffers of 0x07 as stand-in images, which are correctly refused now. They carry real file headers instead — a fixture should be the thing it claims to be, exactly like a real upload. Also adds the deck theme system: five palettes in assets/deck-themes.json, render_pptx.py rebinding its palette from the theme rather than hardcoding it, the theme carried on the deck and validated against the same catalogue the renderer reads, a picker on the generate form, and PUT /my-resources/:id/theme to re-skin a stored deck with no model call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU |
||
|
|
fce05a2749 |
fix: the active library view is unmistakable, and a leading question cannot mislead the vision model
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 49s
Forgejo Docker Build / Root app tests (push) Successful in 57s
Forgejo Android APK / Build signed APK (push) Successful in 1m58s
Forgejo Docker Build / Build Docker image (push) Successful in 18s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
Documents and Images were two buttons of the same weight, which reads as two buttons rather than as a current view and an alternative. The active one now carries the surface, the weight and a hairline — keyed off aria-selected, so the visible highlight and what a screen reader announces cannot disagree. The look_at_image question is written by a model that has not seen the image, from what the user said, so it can presume something that is not there — "describe this rash" about a photograph of a drug chart. Answering the presumption would send it back as fact. The seeing model is now told the question may assume something absent, and to say so first and describe what is actually there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU |
||
|
|
03621752e8 |
feat: image fallback chains for every workflow, and a library worth looking at
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 55s
Forgejo Docker Build / Root app tests (push) Successful in 45s
Forgejo Android APK / Build signed APK (push) Successful in 2m5s
Forgejo Docker Build / Build Docker image (push) Successful in 19s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
**Fallbacks.** One image model meant a refusal, a rate limit or a model the gateway had since dropped ended as a missing picture. Every workflow now tries its model, then each fallback in order, stopping at the first that produces an image. Primary plus two, capped: each hop is a paid request, and a chain long enough to need a cap is long enough to surprise someone. My Resources previously had no fallback at all — only the Clinical Assistant did, and only one. That is backwards: a missing figure is most visible in a deck, where it leaves a hole in a slide. The retry rule is now a classifier that says *why*, rather than a boolean. Transient faults, a 404 for a model the gateway does not have, and a content refusal all move to the next model — a refusal because policy is a vendor decision, not a fact about the request. 401/403 stop immediately (one gateway, one set of credentials, the next model fails identically), as do 413 and any other 4xx, which are malformed everywhere. Refusals are recognised from the message: no provider sends a machine-readable reason and the status varies. Each hop re-leases the job, so a chain cannot outlive its claim and let a second worker repeat the same paid work, and the row records the model actually being paid for so a picture made by the third model is not attributed to the first. The old singular `fallback_image_model` is still read, so an existing configuration keeps working without anyone re-entering it. **Library.** Documents/Images tabs in My Resources, with a real grid: fixed aspect tiles so the rows line up whatever shape the pictures are, a source badge on the picture, two-line prompt, hover lift, shimmer skeletons while thumbnails land, and a lightbox that closes on Escape or the backdrop and restores focus. Actions are hidden on hover only behind `@media (hover:hover)` — hiding delete behind :hover would put it out of reach on touch and keyboard. Downloads go through privateImageBlob rather than a bare `<a download href>`: a mobile client's session is a bearer token an anchor cannot send, and these assets are served no-store on purpose. The gallery lives in My Resources only. Assistant images appear in it, which was the point; the assistant page does not grow a gallery of its own, and a test asserts no assistant module lists the endpoint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU |
||
|
|
259b4858be |
feat: an image library — every picture this account has made
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 47s
Forgejo Docker Build / Root app tests (push) Successful in 46s
Forgejo Android APK / Build signed APK (push) Successful in 2m6s
Forgejo Docker Build / Build Docker image (push) Successful in 19s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
A figure outlives the deck it was drawn for: the deck gets replaced, the diagram is still good. Until now a generated image could only be seen in the resource it was made for, and there was no way to find one again or to delete it. Library → Images is a grid of every finished image the account owns, across all three workflows, newest first. GET /api/generated-images is scoped by owner_id in the statement rather than filtered after, returns only finished jobs — an unfinished one is a broken frame in a gallery — and pages by keyset, because a gallery that grows while you scroll repeats or skips rows under OFFSET. Most of this already existed. Thumbnails were already rendered at 256 and 640 by sharp and already served by ?w=, with their own checksum so the client's tamper check passes on a derived copy; hydrateImage already handles auth, the account boundary and caching. The tiles ask for the 256px preview, so thirty of them cost a few kB each rather than thirty full-size downloads, and the prompt is decrypted for the caption because it is the only human-readable label an image has. Deleting needed new work. The storage interface had no remove at all, so a delete that dropped the row would have left the object and both previews in the bucket — paid for, and still readable by anything with credentials. Storage now removes all three keys, and the bytes go before the row: a row pointing at a missing object is a broken image in a gallery, while an object without its row is only wasted space, and unreachable storage refuses the delete outright rather than reporting a success that left the picture behind. THUMB_WIDTHS now has one definition, in generatedImageStorage. Two copies drift, and the drift that matters is a width that gets written and never deleted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU |
||
|
|
1ad72b134b |
feat: a resource can have several illustrations, on its own path
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 54s
Forgejo Docker Build / Root app tests (push) Successful in 48s
Forgejo Android APK / Build signed APK (push) Successful in 2m22s
Forgejo Docker Build / Build Docker image (push) Successful in 12s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
My Resources used imageTool.dispatch, which permits exactly one image per request — "Only one image tool invocation is permitted per request". That is the right rule for a chat reply and the wrong one for a twelve-slide deck where the author asked for three figures. Rather than relax a limit the clinical assistant and the Learning Hub also depend on, this adds a separate dispatcher for this feature. Same queue, same storage, same my_resources workflow, same asset endpoint — only the number of figures differs, bounded at six because each one is a paid request. "Use 3 diagrams" in the instructions is read as the number it is, and the illustration option now says several are possible rather than promising one. Three things had to be got right, each found by measuring rather than assuming: The illustration guidance has to be the last thing in the prompt. Placed before the output rules it lost — with the tool offered and the paragraph present, the model returned 3297 characters of markdown and zero tool calls, while the same tool and wording in a shorter prompt produced three calls. Even last, it loses to a prompt carrying thirty library excerpts: deterministically, with the library off "use 3 diagrams" made three calls and with the library on it made none and wrote a longer deck instead. So when the author names a number the call is required rather than merely offered. With no number named the choice stays the model's. And a model that has just made three tool calls tends to sign off instead of writing: "I'll create the presentation and the three teaching diagrams." was returned as the resource — 61 characters — because only a completely empty body counted as missing. A body with no title block and no heading is not a Pandoc document whatever its length, and if the continuation is no better than the first attempt, whichever actually reads like a resource is kept. Verified end to end with the library on: generate produced three figures and an eight-slide deck; modify added two more figures and a ninth slide. The figures were fetched and looked at — labelled airway anatomy, and a croup/epiglottitis/ bacterial tracheitis comparison. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU |
||
|
|
055a86bbb1 |
feat: My Resources says what it is, offers its sources in one place, and Modify gets them too
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 55s
Forgejo Docker Build / Root app tests (push) Successful in 47s
Forgejo Android APK / Build signed APK (push) Successful in 1m59s
Forgejo Docker Build / Build Docker image (push) Successful in 18s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
The screen had been trimmed to controls with no explanation of what it was for; "Private to you" said who could see it, not what it did. It now opens with a sentence that says what you get and how you get it out, and the header reads "Only you can see these". The four choices — clinical library, PubMed, web, illustration — are one "Draw on" group instead of four separate rows, with the library ticked by default. Each option hides itself when an administrator has not enabled it, so nothing appears that a person could tick and then be refused. Modify offers exactly the same choices. It had none, so "add what the 2024 trial showed" was answered from the model's memory rather than by looking anything up. Generate and Modify now go through one gatherSources(), so they cannot drift into offering different things or searching them differently. Writing "include a diagram of the airway" in the instructions now switches the illustration option on and says why, rather than the request being dropped in silence. Switching it off by hand sticks — the hint then reminds instead of fighting — and when no image model is configured it says so rather than pretending. Both the generate and modify boxes behave this way. Two things found by testing this rather than assuming it. PubMed ANDs every mapped term, so one unrecognised word takes the query to zero. "febrile seizures" returns six results and "febrile seizures in under-fives" returns none; "the anatomy of croup: subglottic narrowing and the steeple sign" returned none until it was narrowed to "anatomy croup", which returns six. A query that finds nothing is now retried against progressively shorter versions of itself, longest first, and the response says which query actually worked so the screen cannot report one that found nothing. Those extra calls tripped NCBI's three-a-second limit and produced a 429, so retries are spaced and the first attempt waits for nothing. Separately, the searches run on the topic while the library retrieval also gets the instruction: retrieval is semantic and benefits from the context, but a keyword engine handed a whole sentence returns nothing. And when a search was asked for and came back empty, the prompt now says not to invent a citation, a PMID or a URL to fill the gap. Without that the model supplies them from memory, and a fabricated PMID looks exactly like a real one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU |
||
|
|
5577ec301c |
refactor: cut the explanatory copy from the sign-in screen and My Resources
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 55s
Forgejo Docker Build / Root app tests (push) Successful in 55s
Forgejo Android APK / Build signed APK (push) Successful in 2m3s
Forgejo Docker Build / Build Docker image (push) Successful in 19s
Forgejo Docker Build / Deploy to the host (push) Failing after 2s
The HIPAA notice is gone from the auth screen, along with the CSS rule that had no other user. The My Resources intro paragraph and the helper paragraphs under each checkbox are gone too; where a paragraph carried something the reader actually needed, it moved onto the control itself — the illustration checkbox now says what it does and where to steer it. Two sentences were kept rather than deleted, both about data leaving the building: that a web search query leaves this network and must not carry anything identifying, and that a PubMed query goes to NCBI. Trimmed to one line each. Say the word and they go too, but a clinical tool should not quietly send a query off-network with nothing on screen about it. The three tests that pinned the removed sentences now assert the behaviour instead: that the header still claims privacy, and that the illustration checkbox is genuinely unchecked in the markup rather than merely described as off by default. Verified as rendered: neither the sign-in nor the sign-up screen mentions HIPAA, the invite-code field is visible, and the generator card is down to its controls plus the NCBI line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU |
||
|
|
8cca5a4796 |
feat: instructions can ask for the illustration; library scrolls and searches; Modify
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 50s
Forgejo Docker Build / Root app tests (push) Successful in 48s
Forgejo Android APK / Build signed APK (push) Successful in 2m4s
Forgejo Docker Build / Build Docker image (push) Successful in 12s
Forgejo Docker Build / Deploy to the host (push) Failing after 2s
Three things to My Resources. Illustration was entirely the model's call: someone who wanted a figure of something particular had no way to say so, because the instructions steered the prose and nothing else. The illustration guidance now tells the model to follow the author's instructions when they ask for a figure or name what it should show, and to treat that as the decision already made. Verified live: "include a diagram showing the age distribution and the simple-versus-complex distinction" produced exactly that, both halves in one figure. Exactly one image per generation is a real limit, not a wording choice — the shared imageTool dispatcher rejects more than one tool call per request, and it is used by the assistant and Learning Hub too. So the prompt says to draw the single most useful one if several are asked for, and the screen says the same. The library was an unbounded list that pushed everything below it off the page. It is now a 360px scrolling box with a search over title and topic, filtered locally because the rows are already in hand. "Nothing yet" and "nothing matches" are different messages, because telling someone whose search missed that they have never generated anything is wrong. Measured in a real render: 360px visible of 642px of content, and searching narrows 10 rows to 3. Modify is new UI over the refine endpoint, which existed with no way to reach it. Pick a resource, say what to change, and it is rewritten in place keeping its id, its downloads and its References section. The picker is built from the same library array, so it cannot drift, and a selection survives the refresh that follows a generation. Verified live: "add a Key Takeaways slide before References" inserted exactly that and left the other four slides alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU |
||
|
|
1270899dcb |
feat: PubMed search for My Resources, and an image tool that actually fires
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 46s
Forgejo Docker Build / Root app tests (push) Successful in 47s
Forgejo Android APK / Build signed APK (push) Successful in 1m56s
Forgejo Docker Build / Build Docker image (push) Successful in 18s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
PubMed joins web search as an optional source for a generated resource: a
literature search on the topic, with abstracts, cited by PMID in References.
Off by default, admin-enabled, with its own optional API key (NCBI raises the
rate limit from 3/sec to 10/sec; it works without one).
Neither search is a tool any more, and that is the point. Offering them as
function calls meant the model decided whether to search, and with a prompt
ending "Output ONLY Pandoc markdown" it decided not to — every time, with and
without corpus grounding, no matter how the tool description was worded.
Calling callAI with the tool directly produced a correct pubmed_search call, so
the plumbing was never the problem. The search only ever needed the topic, and
the route knows the topic before it calls the model, so both searches now run up
front and their results go into the prompt as findings, exactly the way corpus
excerpts do. Ticking the box now means the search happened.
Verified live against deepseek-v4-flash: 30 corpus excerpts and 6 PubMed
results, and a References slide carrying both the library sources and four real
PMIDs (29562151, 38506440, 35721052, 28814254).
Three fixes to illustration, which had never once fired:
- The dispatch call had been lost in a refactor. The tool was still offered, the
model still called it, and the call was dropped, so no job was ever enqueued.
- imageContext was passed as a bare topic string where dispatch expects
{ request, history }, which made the bound request undefined.
- The prompt never mentioned the tool existed while explicitly demanding only
markdown — the same suppression that killed the searches. It now says an
illustration is available and that calling it is not a violation of that rule.
my_resources is its own image workflow rather than a reuse of learning_hub,
because generated_image_links only accepts learning_hub assets, and that is
exactly the barrier that keeps a private illustration out of published content.
The illustration renders in the panel, rather than a toast pointing at an image
history this feature does not have.
Verified end to end: job queued, rendered, and the asset served to its owner as
a correctly labelled subglottic-anatomy teaching diagram.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
|
||
|
|
571a013d29 |
feat: optional web search, admin-enabled and off by default
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 47s
Forgejo Docker Build / Root app tests (push) Successful in 47s
Forgejo Android APK / Build signed APK (push) Successful in 2m9s
Forgejo Docker Build / Build Docker image (push) Successful in 11s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
The one feature here that sends text outside the building, so the defaults are the careful ones: disabled unless an administrator turns it on, opt-in per generation even then, and the option is hidden entirely rather than shown as something a user can tick and be refused. Only the search query leaves. Library excerpts, the generated resource and anything about the user never do. Both screens say so plainly, because a topic typed while drafting clinical material can carry clinical detail and the provider keeps its own logs. Four providers behind one shape, so swapping changes nothing downstream: Tavily, Serper over Google, Brave, and SearXNG — the only one where the query does not reach a commercial third party at all, which is why it is worth supporting even though it needs somewhere to run. The tool description says when NOT to search, because a model handed a search tool will reach for it constantly: not for settled clinical knowledge, which is what the indexed library is for, and one search per resource. That last one is enforced in the route with toolChoice: 'none' on the continuation rather than trusted to the model. A failed search never fails a generation — same contract as corpus retrieval. The resource is written without it and the response says what was searched for and what came back, so a query that left the network is visible rather than silent. The API key is masked on read and preserved when the field is left blank, the handling the OIDC client secret already gets, so changing provider cannot silently wipe a working key. Verified on the running instance: with nothing configured, webSearchAvailable is false, and a request asking for it anyway is ignored rather than honoured. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU |
||
|
|
7eca509b02 |
fix: slides shrink to fit, and an article is never offered as slides
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 49s
Forgejo Docker Build / Root app tests (push) Successful in 47s
Forgejo Android APK / Build signed APK (push) Successful in 2m0s
Forgejo Docker Build / Build Docker image (push) Successful in 15s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
A generated deck was losing content. pandoc writes a bare <a:bodyPr/> on every shape, which leaves the body with no autofit even though the slide master has one, so a slide with too much on it is cut off mid-sentence and the rest is not rendered at all. Reproduced and counted: eight bullets went in, three came out, the third ending mid-word. Every generated deck now carries <a:normAutofit/> on its body placeholders. No fontScale, deliberately — the renderer works out the reduction, so a slide that already fits is untouched, where a fixed scale would shrink all of them. The same eight bullets now fit with nothing in the bottom 6% of the slide. This is a floor, not a licence to overcrowd. The prompt still asks for one idea per slide; this stops a long one becoming unreadable. Also: an article is no longer offered as PowerPoint. A deck of paragraphs is not a presentation. Word and PDF suit either kind, and the route refuses the combination rather than relying on the button being absent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU |
||
|
|
fac8757ce8 |
feat: My Resources has a menu, a library and three downloads
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 56s
Forgejo Docker Build / Root app tests (push) Successful in 48s
Forgejo Android APK / Build signed APK (push) Successful in 2m13s
Forgejo Docker Build / Build Docker image (push) Successful in 14s
Forgejo Docker Build / Deploy to the host (push) Failing after 1s
The pathway existed but was reachable only by API. It now has a tab of its own next to the Learning Hub — related, not the same thing, and sitting together is how someone discovers the difference — visible to every signed-in user with no role gate in the markup. Generate a deck or an article, see everything you have made, download each as PowerPoint, Word or PDF, delete what you no longer want. The screen says "Private to you" and "Nobody else sees these", because the distinction from published Learning content is the thing a person needs to understand before typing a patient's condition into it. Downloads are fetched rather than linked: an <a href> cannot carry the Authorization header. The blob is saved under the filename the server chose and the object URL is revoked afterwards. Resource titles come from a model, so rows are built as elements and a title is only ever assigned to textContent. The e2e stack now joins danvics_convert too. It could previously reach only Postgres and Redis, so a PDF download failed there in a way production would not — which did at least prove the degradation path works: with Gotenberg unreachable the response is "PDF conversion is unavailable right now. PowerPoint and Word still work", and the other two formats download unaffected. Verified in a browser as an ordinary user: the tab appears and opens, the form swaps slide count for word count when the format changes, the library lists their own work, and pptx, docx and pdf all download with sensible filenames (36360, 13285 and 68310 bytes). Also documents retrieval sizing in docs/retrieval-tuning.md — the per-feature budgets, and RERANKER_TOP_K, which caps all of them and had until now appeared in no configuration file at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU |