Commit graph

369 commits

Author SHA1 Message Date
26dbc84865
chore(release): v0.16.0
All checks were successful
CI / deploy (push) Successful in 28s
CI / check (push) Successful in 17m3s
v0.16.0
Build 19.

Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 15:07:53 +02:00
0a52a3947a
test(publish): publishing end to end against a running server
All checks were successful
CI / check (push) Successful in 16m58s
CI / deploy (push) Successful in 1m28s
A sync test of the public tier as a whole: the author's device syncs a
studyset (a topic, tagged cards, a note, an exam plan, an exam of their
own), the instance builds the first release from what it holds, another
person finds it in Explore and adds it to their library (the topic, the
tags, the note and the plan's topic come along, the exam stays the
author's), a second release is offered and taken while the learner's own
edit stays, and a third is offered no more once the author withdraws.
Until now each half had tests of its own only: the server's publish from
rows written into its database, the app's copy and update from releases
through fakes.

A test can't sign in to GitHub, so it gives the author the identified
scope by SQL, on the person and their sessions, as linking GitHub does.
The devices' clients are exposed for what lives on the instance rather
than in the synced data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 14:35:36 +02:00
dd6c567ad0
feat(studyset): prepare for an exam, with plans, a date and the pace to it
A studyset plans for exams: each plan names what an exam covers, the
topics it takes whole (with their flashcards, learning paths and PDFs)
and the tags whose flashcards it takes from anywhere in the studyset.
The author's plans are published with the studyset (payload schema 7,
`examPlans`); copying a release and taking a newer one point their
topics at the copy's own studysets. Plans are also offered from level
tags: JLPT N5 to N1, HSK 1 to 7-9 and KRDict's levels, each taking the
levels below it, as the exams do.

The person chooses a plan and the exam's day (`exam`), which stays
theirs: the server leaves it out of a release, as a PDF's deadline.
The study plan then shows the days left, how many of the plan's
flashcards were started, its paths' steps done and its PDFs' pages
read, and the pace that gets everything new learnt a week before the
exam (by the exam when two weeks or fewer are left): new flashcards a
day, steps a week, pages a day. Study for the exam studies only what
the plan covers, its new cards at that pace (counted as the day began)
and every review due.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 14:35:36 +02:00
31e4d7d64b
feat(studyset): an empty tab says its material is in the topics, and leads there
People testing the app looked for the material of a studyset that groups
topics in its own empty tabs, which said "No flashcards yet" and how to
add some. A tab with none of its own while the topics hold some now says
so: "The flashcards are in its topics", each topic holding some with how
much (everything inside it counted), opening it; for flashcards Browse
all of them in one list; and, quieter, how to add some here after all.
Opened for the first time, such a studyset shows the first tab its topics
fill. The tabs' counts stay the studyset's own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 14:35:36 +02:00
8dde0fa42a
feat(assistant): a question opens in the AI assistant chosen, Proton's Lumo by default
Questions asked from the app went to Claude alone. Settings › Assistant
now chooses where they open (decided 2026-10-14): Lumo, the default, by
its guest chat with the question in the address's fragment (Lumo's
`prefill`, which needs no account and reaches no server; the address for
one's own account loses the question when nobody is signed in), ChatGPT,
Claude, Mistral, Perplexity, Duck.ai, Grok, Kagi Assistant, HuggingChat,
or an address of one's own with %s. Each link fills the question in for
the person to send (Perplexity answers at once, Grok asks first); Gemini,
which has no such link, and Copilot, whose stopped working, are left out.
The field names the assistant ("Ask Lumo…"). A device that sent questions
to Claude goes on asking an assistant, now Lumo. Connecting Claude to read
studysets stays as it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 14:35:35 +02:00
6541af937a
docs: ADR 0009, feature parity for the other languages
What applies of Japanese's features to Chinese, Korean, Greek, German and
English, with the developer's choices and each decision's reason: Unihan's
characters, KRDict's words and levels, Make Me a Hanzi's and the curator's
own strokes, typed pinyin, radicals and readings by language; what doesn't
apply or has no open data. AGENTS.md, the server's README and the
instance's example environment follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 14:35:35 +02:00
e7f3abdb8d
fix(dictionary): a Hanja reads 물 수, and each radical's form shows once
Korean names a Hanja by its meaning, then its sound; simplified Chinese
shares some radicals' forms with Japanese (麦), which showed twice, and
writes 黑 as itself. A test can read a language's strokes beforehand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 14:35:35 +02:00
93c33ce079
feat(dictionary): a German verb is found with its particle at the clause's end
"Ich rufe dich morgen an": where a clause ends in a separable particle,
each word before it is also asked for with the particle joined on, as a
subordinate clause writes the verb (anrufe), which Wiktionary lists among
anrufen's forms. The verb is offered beside its stem and its particle,
as the longer words running over a Japanese word are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 14:35:35 +02:00
87bebd1953
feat(card): the card browser finds and sorts words of any language as typed
Search folded case and katakana only, for Japanese. It now also leaves out
accents, tone marks and umlauts (strasse finds Straße, maedchen and
madchen Mädchen, σπιτι σπίτι, xue xi xué xí), and A–Z sorts by the same
bare letters, so Ärger stands among the a's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 14:35:34 +02:00
e93f37ce68
fix(studyset): a studyset's title shows in its language's shapes outside it too
The library's cards and Explore's drew Chinese and Korean titles with
Japanese shapes first (骨, 直): each card now sets its studyset's
languages, as every screen inside a studyset does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 14:35:34 +02:00
5479b973fc
feat(dictionary): characters of Chinese, Korean and Greek, written by hand; pinyin typed
Parity with Japanese's kanji and kana:

- The characters' tab is named by its language (Hanzi, Hangul & Hanja,
  Alphabet), its groups too: HSK levels and the characters in common use
  (Hong Kong's grades for traditional ones); Hangul and the Hanja taught
  in schools; Greek's letters. Radicals for Chinese and Korean in their
  own forms (讠 for 言 in simplified Chinese), none for Greek.
- A character's readings are named as its language names them: On and
  Kun, Pinyin, Eum and Hun (음, 훈); a letter means its sound and name.
- How characters are written comes by language: Chinese from Make Me a
  Hanzi (its medians in KanjiVG's square, Arphic Public License, a data
  file beside the app, never in cards), the Hangul letters drawn by the
  curator; a Korean word is written letter by letter (닭: ㄷ ㅏ ㄺ). Words
  of alphabets (German, Greek) aren't written by hand.
- A Chinese word's pinyin is typed in a session, tone numbers turning
  into marks (ni3 → nǐ); KRDict's levels (초급, 중급, 고급) tag Korean
  words; a Korean word's pronunciation shows in brackets ([가치]).
- Smaller: Look up in the composer keeps a noun's article; the Watch
  filter names its level list; hints and placeholders by language.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 14:35:34 +02:00
7070cd6bd2
fix(server): a dictionary whose file is gone is unloaded
Korean's words move from Wiktionary to KRDict, and an operator may keep
fewer languages: the curator removes those files, and the loader now
unloads every source no file holds, so a language never has two word
dictionaries. Only once every file in the folder could be read, as an
unreadable one may be the dictionary that seems gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 14:35:34 +02:00
8820cefde3
feat(curator): characters of Chinese and Korean, the Greek alphabet, Korean words from KRDict
For parity with Japanese's KANJIDIC2 and kana:

- Unihan (Unicode License v3) makes characters' dictionaries: the 8,105
  standard simplified characters (unihan-hans) and the traditional ones in
  use (unihan-hant), told apart and ranked by CC-CEDICT's words (Unihan
  calls 这 its own traditional variant; 後 ranks as the 后 of 以后), with
  pinyin, meanings, strokes, Kangxi radical, HSK 2025 character levels,
  the 3,500 common ones and Hong Kong's grades; and Korean Hanja
  (unihan-ko) with their sounds, the 1,800 of education, and their native
  readings from Wiktionary (물 수), with the 40 Hangul letters as kana are.
- The Greek alphabet, the curator's own (CC0): letters with names,
  transliteration and sound.
- KRDict, the Basic Korean Dictionary for learners (CC BY-SA 2.0 KR,
  downloaded without an account), replaces Wiktionary for Korean's words:
  learners' levels, pronunciation where it differs from the spelling
  (같이 [가치]), Hanja, English senses; Wiktionary's update keeps the
  words' forms (가다's 99) and the Hanja's readings beside it for both.
- The update converts KRDict when its page offers another export and the
  characters weekly, and removes the files of languages no longer kept
  or of sources replaced.

Measured on the real data: Unihan 3 s; KRDict 54,870 entries in 24 s
and under 600 MB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 14:35:28 +02:00
4e6581b9a5
chore(release): v0.15.0
All checks were successful
CI / deploy (push) Successful in 28s
CI / check (push) Successful in 16m38s
v0.15.0
Build 18.

Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 10:58:00 +02:00
ca21421723
docs: ADR 0008 with the sizes and times measured on the real data
All checks were successful
CI / check (push) Successful in 16m15s
CI / deploy (push) Successful in 1m11s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 09:39:31 +02:00
9945bc2ff3
fix(curator): a syllable's homographs don't all become common
Korean's 이 has thirteen entries in Wiktionary (tooth, this, two, a name
…), each of which took the syllable's frequency, so all thirteen were
common and adding Korean's common words would add thirteen cards of
이. Only a word's first three entries, in Wiktionary's order, which
puts the usual meanings first, take its frequency now; the rest rank
after the common words.

The quick look before reading a line of the dump as JSON accepts it
with or without a space after the colon, so it still finds the
languages should kaikki.org write its JSON otherwise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 09:39:31 +02:00
8f4131a138
fix(curator): contractions are words of their own
Wiktionary tags a contraction as an abbreviation and an alternative of
its first word (im: in + dem), so German's im, zum and ins, which every
subtitle has, were neither entries nor found as forms. A contraction is
now an entry of its own, as text writes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 09:39:31 +02:00
2c4d760eac
feat(dictionary): a Chinese word's row says its HSK level
As a Japanese word's says its JLPT level: HSK 1 … HSK 6, HSK 7–9. The
levels' scheme (`WordLevels`, `levelOf`) moves from the Watch screen's
filter into the dictionary's model, which both name levels with.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 09:39:31 +02:00
222175584b
feat(curator): Tatoeba's sentences as examples in every new language
JMdict brings Tatoeba's sentences for Japanese; the other languages'
dictionaries had none (CC-CEDICT) or Wiktionary's own, often
quotations. Each file now takes Tatoeba's sentences of the week (its
per-language exports, CC BY 2.0 FR) as its entries' first examples,
which become a card's hints: each sentence with a translation into the
dictionary's meaning language goes to the entries its words find, cut
as the app cuts a line of subtitles (at spaces, Korean's particles off,
Chinese by its longest words), up to three for each, those using the
word as its headword writes it first (Haus rather than zu Hause; 学习
rather than 學習 in the simplified dictionary), then those nearest seven
words long. German's common words nearly all get some (8,849 of
9,292), in ten seconds and under 300 MB; a dictionary whose sentences
can't be had keeps its words. `curate tatoeba` does the same for a file
made by hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 09:39:31 +02:00
a2441ecdf2
feat(dictionary): German, Greek, English, Chinese and Korean words
A studyset in German, Greek, English, Chinese or Korean finds its
dictionary as one in Japanese does (ADR 0008), and its subtitles are cut
into words by its language (`WordCutting`): Chinese by its longest
words as they stand, the longer words over each cut offered as for
Japanese; Korean at its spaces, a word the dictionary lacks as it stands
taken without the particle it ends with (집에서 is 집, 아이 stays a
child); every other language at its spaces, a word written exactly so
first (the noun Essen before the verb essen). A studyset finds the
dictionary of its language tag, else of its language alone (zh for
zh-Hans).

A Wiktionary word shows its genders, principal forms, IPA, romanisation
and Hanja; a German or Greek noun of one gender goes onto its card with
its article (das Haus, το σπίτι); its tag is its word class, named in
full (Noun, Adjective, Preposition), as Wiktionary's adj and adv would
read as JMdict's codes. Articles, pronouns, prepositions and
conjunctions are left out of a video's words. Chinese words show their
pinyin over each character on the Watch screen, carry their HSK level
as a tag (HSK-1 … HSK-7-9), and the dictionary adds a level whole; the
Watch screen filters by HSK levels for Chinese, JLPT's for Japanese, and
by none for a language without. A reading is typed in a session only
when it is kana, so pinyin isn't. Words are looked up on the web on
Jisho in Japanese, on Wiktionary at the language's section otherwise,
and a search's hint speaks of kana and romaji only in Japanese.

`DICTIONARY_LANGUAGES` in deploy/.env names the languages an instance
keeps (unset: de,el,en,ko,zh; empty: none).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 09:39:30 +02:00
292e369ed3
feat(server): an entry's text keys alone find it in running text
A dictionary file may name an entry's `textKeys`, its own forms: those
alone fill `dictionary_key`, so a German word is found in a subtitle by
its forms, never by its English meanings, which would match English
words in German text; a search still finds it by them. Files without
them (JMdict's) fill it with every key without a space, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 09:39:30 +02:00
ee9cd3dd3f
feat(curator): Korean from Wiktionary, with its Hanja
Korean comes from the same dump as German and Greek, with English
meanings (decided 2026-10-14: Korean last, from Wiktionary for now): a
verb's conjugations are among its forms, so text finds them; a word's
Hanja and romanisation find it in a search and show beside it. The
counter words Wiktionary's tables name and its notes on what it couldn't
read are no forms of a word.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 09:39:30 +02:00
7b94eda592
fix(curator): HSK levels by the syllabus's tones; variants find their word
The syllabus's reading is matched with its tones first, so 和 hé gets
HSK 1 and 和 hè doesn't; only where no entry of a word reads it so (the
syllabus marks the tone changes of 一 and 不, 一下 yíxià, CC-CEDICT
doesn't) are the letters alone compared. CC-CEDICT's 2,885 entries that
are only "variant of …" (咊, an old variant of 和) are no entries of
their own any more, which repeated their word in the simplified file,
but written forms of that word, finding it in text and searches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 09:39:30 +02:00
9d21f7f20e
feat(curator): Chinese from CC-CEDICT, with the HSK levels of 2025
Chinese comes from CC-CEDICT (CC BY-SA 4.0), made for learners of
Chinese: `curate cedict` writes two dictionaries of its entries,
`cc-cedict-hans-en` (zh-Hans) and `cc-cedict-hant-en` (zh-Hant), so a
studyset finds its words as it writes them, each finding a word by
either script, by pinyin with or without tones and by its meanings.
Readings are pinyin with tone marks, a syllable to a character; measure
words are a remark beside the meanings.

The HSK levels are those of the 2025 examination syllabus, which HSK
3.0's examination follows since July 2026 (decided 2026-10-14: level
tags, credited), as hsk30 extracted them (MIT): `hsk:1` … `hsk:6`, and
`hsk:7` for 7 to 9, each given to the entry read as the syllabus reads
the word (的 de, not 的 dì; 书 shū, not the Book of Documents, Shū),
ranking the words, HSK 1 first.

`curate update --languages de,el,en,zh` takes over from `--wiktionary`:
Chinese from MDBG's file of the day as often as EDRDG's files, the rest
from Wiktionary's dump.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 09:39:30 +02:00
6d57f50b16
feat(curator): dictionaries of German, Greek and English from Wiktionary
The languages besides Japanese come from the English Wiktionary as
Wiktextract reads it (decided 2026-10-14: German, Greek and English
first, then Chinese, then Korean): kaikki.org's raw dump, every language
in one file, as its per-language files are being withdrawn.
`curate wiktionary --languages de,el,en` reads it once, streamed, and
writes German and Greek with Wiktionary's English meanings and English
with what its translation tables give in German, the app's other
language (`wiktionary-de-en`, `-el-en`, `-en-de`; sources
`wiktionary-<language>`, as the server holds one source per meaning
language). Inflected forms, and Wiktionary's entries for forms alone
("Häusern: dative plural of Haus"), find their word instead of being
entries; names and affixes are left out, as are articles that tables
give cells of their own, abbreviations and rare spellings, which would
match common words. Entries keep their part of speech, genders, IPA,
principal forms and examples keyed by language as JMdict's.

wordfreq's small lists (CC BY-SA 4.0, crediting SUBTLEX) in data/ rank
the words by how often they are used and label the 15,000 most used
`common`. Typing without accents or umlauts finds a word (`strasse`,
`haeuser`, `spiti`).

The format gains `textKeys`: what finds an entry in running text, its
own forms, never its meanings, which would match words of the meaning
language in a subtitle. `curate update --wiktionary de,el,en` keeps the
languages current: it asks kaikki.org when the dump last changed and
converts it again only when it is newer, or the languages or the
conversion changed. The whole dump converts in about ten minutes in
under 300 MB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 09:39:29 +02:00
5683a1f33e
test(tag): the tag filter has room beside four material tabs
The test font draws every letter as a full square, so at 1,200 wide the
four tabs left the flashcards' filter folded into the menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 09:39:29 +02:00
f8336ada90
test(studyset): every empty material tab is opened, as each has one
Since paths moved out of the Study plan, every empty section the test
looks at is a tab of Material, so the check for one without a tab is
always true, which the analyser refuses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 09:39:29 +02:00
fe6b89e773
docs: AGENTS.md says the paths are material, the plan refers to them
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 09:39:29 +02:00
ed34503cda
feat(studyset): an empty material tab shows no count
With four tabs, Paths 5, Flashcards 12, Notes 0 and Files 0 don't fit a
phone, and Files scrolls out of sight. An empty tab says so itself once
opened, so its count goes (decided 2026-10-14, after the trial's
screenshots); the four tabs then fit as long as a few are empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 09:39:28 +02:00
c3f7de9607
feat(studyset): learning paths are material, the study plan refers to them
A learning path is material too, as its flashcards, notes and files are,
and the study plan only refers to it (decided 2026-10-13), as it will to
an exam's date. Material's tabs are now Paths, Flashcards, Notes and
Files, the paths first as the way through the rest, with Reorder beside
them; opened here for the first time, Material shows the first tab with
something in it. Study plan keeps how many paths are finished and Next
up, which names the next step of each path under way ("Step 3 of 6: が"),
or offers to start the first path not finished when none is, and lists
how far every path has come, its steps done in a bar, each opening the
path. The path list's state now knows each path's steps done and next
step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 09:39:23 +02:00
744a068c97
chore(release): v0.14.0
All checks were successful
CI / deploy (push) Successful in 14s
CI / check (push) Successful in 15m45s
v0.14.0
Build 17.

Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 06:02:07 +02:00
073475001c
docs(deploy): the Jellyfin demo's account never locks
All checks were successful
CI / check (push) Successful in 15m54s
CI / deploy (push) Successful in 15s
With Jellyfin's default of three failed tries, anyone could switch the
demo off with three wrong passwords; an account without a password has
nothing for the lockout to guard. The runbook also says how people sign
in to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 01:55:12 +02:00
107160cac4
fix(jellyfin): a tap on someone a server shows names them, the password asked
Jellyfin v10.11 and later no longer say which accounts have a password
(HasPassword is always true, jellyfin/jellyfin#14950), so the demo's
account was listed as "With a password" and only named in the form.
Every listed account is now named in the form with the password field
focused, which says to leave it empty for an account without one, as
Jellyfin's own apps do; the line under each account goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 01:55:12 +02:00
33584e428e
docs(deploy): media for the Jellyfin sidecar go over tar and SSH
rsync has to be installed on both ends, and learn.halkyon.eu's server
has none; tar through SSH needs nothing the server lacks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 01:55:11 +02:00
8c57bdf4d1
docs(deploy): Jellyfin's group gets the ID its compose file runs it with
The sidecar's steps made Jellyfin's user with its own group, to which
useradd gave a system group ID (987 on learn.halkyon.eu's server), while
compose.jellyfin.yaml runs it as 61096:61096. The group is now made
first with the same ID, and the steps say why useradd's warning about an
ID above the system range is meant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 01:55:11 +02:00
9d2a93a648
fix(deploy): a deploy failing after the switch brings the containers back
All checks were successful
CI / check (push) Successful in 15m42s
CI / deploy (push) Successful in 15s
A failed deploy put the checkout back at the deployed commit but left
the containers as the new one had made them: when Caddy couldn't start
on the new network (2026-10-14), learn.halkyon.eu stayed down until they
were brought back by hand. Failing after the switch, the deploy now also
starts the containers as the deployed commit has them, on the new
images, which fit the database the migrations have changed. Before the
switch, the running containers are never touched, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 01:01:35 +02:00
b834bdbb85
fix(deploy): automatic addresses on the edge network from its upper half
The deploy that brought the edge network failed and left learn.halkyon.eu
down: the server, started before Caddy, got the first free address on
the network, 172.30.80.2, which is Caddy's fixed one, so Caddy couldn't
start ("Address already in use"). Containers without an address of their
own now get one from 172.30.80.128/25, below which the fixed ones (Caddy
.2, sidecars from .10) are never taken.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 01:01:34 +02:00
28d22f3e94
chore: the SSH aliases and CI's runner label keep the servers' names
Some checks failed
CI / check (push) Successful in 15m47s
CI / deploy (push) Failing after 1m9s
The servers' names are no secret, so the settings that carry them stay
as they were, and nothing on a server or a laptop needs changing: CI
runs on `sahaquiel` again, the release uploads to `ramiel-fdroid`, and
deploys by hand go through `sahaquiel-deploy`. Prose still names a
server by the domain it serves, which says what a reader needs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 00:33:51 +02:00
48ac84d92c
feat(jellyfin): any stretch of a line's characters chosen by hand
The cut takes the longest word and knows no grammar, so 今日は reads as
こんにちは where it is 今日 and は. Any stretch of a line's characters can
now be chosen by hand: dragged across with a mouse, Shift+clicked from
the word or stretch chosen, or on a touch screen held on a character and
dragged on. It is looked up as one word, as it stands or as a form of
one, in one small request (the instance has the line's text already);
while it is chosen the line reads it as that word (きょう over 今日), and
what the dictionary lacks as one word is offered as a search on Jisho.

The line's words move into line_words.dart, out of the Watch screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 00:33:51 +02:00
9b5ea88a8a
fix(deploy): the server starts once more after a deploy with a migration
After v0.13.0's deploy every request to learn.halkyon.eu took a minute or
two and syncs failed, until the server was restarted by hand. The
migration changed a synced table, so the new server, as it first used
the database, found the sync engine's registry of synced tables changed,
and then rebuilt every space's projections in every session until it
stopped (serverpod_offline_sync issue #150). The maintenance run that
applies the migrations doesn't spare it, as AGENTS.md wrongly said.

redeploy.sh now restarts the server once it answers whenever the deploy
brought a migration, and waits for it to answer again: started a second
time, it finds the registry as the first start left it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 00:33:51 +02:00
4b45cacfa4
feat(deploy): sidecars beside the instance, Jellyfin the first, behind an edge network
An instance may now run services beside itself, each a compose file of
its own switched on in deploy/.env (COMPOSE_FILE=compose.yaml:compose.
jellyfin.yaml), so the deploy runs them with the instance. The first is
Jellyfin, which people watch and listen to learn from: the instance then
suggests it (JELLYFIN_URL from JELLYFIN_DOMAIN), and Caddy serves its
site, refusing every change to a user from outside, so a passwordless
demo account stays everyone's; its admin signs in through an SSH tunnel.

The instance's compose gets a second network, `edge`, for Caddy (at a
fixed address, which sidecars trust as their proxy) and the server;
Postgres stays on the internal one. Caddy loses its needless way to
Postgres, and a sidecar never gains one. A sidecar publishes no port,
as the CI runner may reach 127.0.0.1, and runs as a user ID of its own.
Caddy imports the sidecars' sites from sidecars/, mounted by their
compose files (none without), and redeploy.sh recreates it when one
changes. sidecars/README.md has the steps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 00:33:51 +02:00
85506ac7eb
feat(jellyfin): the instance suggests a Jellyfin server, signed in to with a tap
An instance now names a Jellyfin server for its people to learn from
(instance.info().jellyfinUrl): its operator's JELLYFIN_URL (`none`: none),
else https://jellyfin. on the domain registered for its public host, as
learn.halkyon.eu suggests jellyfin.halkyon.eu, once that answers as a
Jellyfin server. The registered domain comes from Mozilla's Public Suffix
List, kept in the server's data folder, so an instance at example.co.uk
never names someone else's jellyfin.co.uk. The instance looks as it
starts and again hourly when asked; the app never reaches for a guessed
address.

In the app, Add material offers From Jellyfin as soon as the instance
suggests a server, before anyone signed in to one. The sign-in fills the
server in and finds it by itself, and lists the people the server shows
on its login screen: one without a password, as a demo's, signs in with
a tap; one with a password is named in the form, which asks for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 00:33:50 +02:00
93b0c0a6b0
chore: the servers are named by the domain they serve
Files called the servers by their own host names, which are the
operator's and say nothing a reader needs. They now say which domain a
server serves: learn.halkyon.eu's server (the instance and its CI
runner), the halkyon.app server (Forgejo and the F-Droid repository).

Two names are settings rather than words. CI's runner label becomes
`instance` (`runs-on: instance`, `labels: ["instance:host"]`): the
runner reads its installed copy of deploy/runner/config.yml, so jobs
wait in the queue until that copy is updated and the runner restarted.
The SSH aliases the scripts and docs use become `learn-deploy` and
`fdroid-upload`. AGENTS.md says to keep it so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 00:33:50 +02:00
63c2f86b12
chore(release): v0.13.0
All checks were successful
CI / deploy (push) Successful in 14s
CI / check (push) Successful in 15m59s
v0.13.0
Build 16.

Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:48:52 +02:00
d552f8bea0
feat(jellyfin): From Jellyfin once signed in, the panel in the glow's colour, adding the words listed
All checks were successful
CI / check (push) Successful in 16m0s
CI / deploy (push) Successful in 1m13s
Add material's entry is now From Jellyfin (Aus Jellyfin), with a play
icon rather than a TV, as it plays music too, and shows only once the
device is signed in to a server in Settings, as the assistant shows only
once switched on; Settings says where it then is. The screen it opens is
called Jellyfin, and its search finds music too.

The words' panel takes the glow's colour as the scene changes: each copy
the glow takes (every 3 s) gives its average, no lighter than 35 % so the
text stays readable, as strong as the glow and fading into the next as
it does. In the light theme, which has no glow, the panel is tinted from
it while the pointer is over it. What lights up in the panel takes a
scheme made from the same colour; the episode's thumbnail is no longer
used.

While the studyset holds none of the video's words, the study button
adds the words listed (Add 11 words, as the filter shows them), tagged
as a word added alone is; afterwards it studies them, and its menu adds
the new ones listed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:50 +02:00
c6e7a53dd6
feat(tag): one switch hides the tags from Jellyfin while studying
Every tag the app makes for where words were met on Jellyfin (the show
and the episode, the artist and the song) now says so (`origin` in tag
schema 2, written only for such a tag), in place of a tag hidden one by
one. Manage tags, where there are such tags, has one switch, Hide tags
from Jellyfin while studying (`hideJellyfinTags`, settings schema 9,
following the person): with it on, cards being studied leave them out of
their row at once, and Tags and Manage tags list them faint. They still
filter and narrow a session. The per-tag eye button is gone.

A word added while watching no longer takes the line it was met in as
its first hint: its hints are the dictionary's examples alone. Tatoeba's
sentences are checked, a subtitle line may be wrong or mistyped, a word
met again in a better line wouldn't get it, and every episode's line
would pile up on a common word.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:50 +02:00
cc777d6b06
fix(study): cramming again after turning front and back
A session begun anew (turned to the other side, its settings changed, or
tried again) kept the cards it had crammed before, so a cram of a few
cards turned round said at once there was nothing to study. It now
crams every card again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:50 +02:00
22f5a4fcd1
feat(jellyfin): songs and their lyrics, read as subtitles are
A music library's artists and albums open as shows and seasons do, and
can be tied to a studyset. A song plays as sound alone (`/Audio/{id}/
stream`, as stored) with its lyrics as Jellyfin keeps them (`/Audio/{id}/
Lyrics`: an LRC file beside it, or its tags), cut into the dictionary's
words as a video's subtitles are, with no translation. Timed lyrics
follow the song: the line sung large and bright among the others faded,
scrolled to as it comes, a line tapped played from its start; untimed
ones show whole. A song without lyrics shows greyed and doesn't open
(`HasLyrics`), as there is nothing in it to read. Its cover stands where
the picture would, glowing as a video does, and its words are tagged
with the artist, shown, and "artist · title", hidden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:50 +02:00
436dc884b6
feat(jellyfin): an episode's own tag, and studying an episode's words
A word added from an episode gets the show's tag and the episode's own ("あかね噺 S1 E1"), hidden on cards. The words panel's Study button studies the studyset's cards of the video's words, on their schedules or crammed (each once): it pauses the video and first gives the episode's tag to the cards of its words the studyset held before, then opens the session on that tag. A study focus can now ask for cramming.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:49 +02:00
68945b2224
feat(tag): a tag can be hidden on cards being studied
A tag's eye in Manage tags hides it on cards being studied, or shows it again: for the many tags naming where a word was met (an episode, a song), which would crowd a common word's card. A hidden tag still filters, narrows a study session and shows where tags are edited. Tag schema 2 adds 'hidden', written only for a hidden tag, so older apps go on reading every other.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-13 21:21:49 +02:00