fix(privacy): what the app shows and adds keeps private studysets private (ADR 0019)

The reading of 22 Oct, on the device's side. Private cards never move
into a readable studyset: the card browser asks to make it private first.
Locked studysets are left out of the move sheets and the destinations
words are added to. A studyset whose key isn't on this device says to
sign in with the password here, not that it is lost, and can still be
archived; creating one there says why it can't. The marker shows the lock
for a private studyset published before, and a shield with a warning
while something in it is still readable or unsealed, with Finish in its
dialog. The library's offer covers every top-level studyset, Recently
deleted and orphaned ones too, says what couldn't be sealed, and its
answer follows the person in their settings (schema 11).

The full-window editors carry the studyset they were opened from, and an
account with keys adds no file where none is known (a new studyset's
description takes pictures once it is made). A file no live row names is
never uploaded, and deleting a row clears what it owed. Sealed rows are
indexed by key and hash. A suggestion from a private copy uploads
readable copies of its files for the author; accepting swaps whole
`asset:` values only.

Docs: ADR 0019's "The reading (22 Oct 2026)" with the decisions, what
changed and the residual risks; "As built" and Learn's AGENTS.md brought
up to date.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-22 09:29:56 +02:00
commit 46a47d0975
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
5 changed files with 625 additions and 73 deletions

View file

@ -1180,6 +1180,7 @@
"fileNotAnImage": "Diese Datei ist kein Bild, das die App anzeigen kann.",
"fileNotASound": "Diese Datei ist keine Audiodatei, die die App abspielen kann.",
"fileTooLarge": "Diese Datei ist größer als {megabytes} MB.",
"fileAfterCreate": "Bilder und Töne lassen sich hinzufügen, sobald das Lernset erstellt ist.",
"fileNotKept": "Die Datei konnte nicht auf diesem Gerät gespeichert werden.",
"fileRemoveImage": "Bild entfernen",
"fileRemoveSound": "Audio entfernen",
@ -2072,9 +2073,18 @@
"studysetReadableBody": "Alles darin wird entschlüsselt und neu synchronisiert, sodass die Instanz es lesen kann: Du kannst es veröffentlichen, und Claude kann es über den Connector lesen.",
"studysetPrivateNoKeys": "Melde dich auf diesem Gerät mit deinem Passwort an, um Lernsets privat zu machen.",
"studysetPrivateSomeLocked": "Manches darin ließ sich auf diesem Gerät nicht öffnen und blieb, wie es war.",
"studysetPrivateLabel": "Privat",
"studysetLockedTitle": "Gesperrt",
"studysetLockedBody": "Dieses Lernset ist privat, und sein Schlüssel öffnet sich auf diesem Gerät nicht: Es wurde mit Schlüsseln verschlüsselt, die dein Konto nicht mehr hat. Es lässt sich nur noch löschen.",
"studysetVisibilityPartly": "Privat, teils lesbar",
"studysetPrivacyPartlyBody": "Manches darin kann die Instanz noch lesen: was ein Gerät schrieb, bevor es davon wusste, oder Dateien, die noch nicht umgezogen sind. Es wird versiegelt, sobald ein Gerät mit deinen Schlüsseln es kann, oder jetzt mit Abschließen.",
"studysetPrivacyFinish": "Abschließen",
"studysetLockedNoKeysBody": "Dieses Lernset ist privat, und sein Schlüssel ist nicht auf diesem Gerät. Melde dich auf diesem Gerät mit deinem Passwort an, um es zu öffnen.",
"cardMovePrivateTitle": "„{title}“ privat machen?",
"@cardMovePrivateTitle": {"placeholders": {"title": {"type": "String"}}},
"cardMovePrivateBody": "Diese Karteikarten sind privat. „{title}“ kann die Instanz lesen, also können sie erst dorthin, wenn es auch privat ist.",
"@cardMovePrivateBody": {"placeholders": {"title": {"type": "String"}}},
"cardMovePrivateAction": "Privat machen und verschieben",
"privacyKeysMissing": "Melde dich auf diesem Gerät mit deinem Passwort an: Was du hier hinzufügst oder änderst, wäre privat.",
"libraryOfferPrivateTitle": "Deine Lernsets privat machen?",
"libraryOfferPrivateBody": "Neue Lernsets sind privat: Ende-zu-Ende-verschlüsselt, sodass nur deine Geräte sie lesen können. Die, die du vorher gemacht hast, bleiben für die Instanz lesbar, bis du sie auch privat machst. Kopien in den Sicherungen der Instanz bleiben lesbar, bis diese ablaufen.",
"libraryOfferPrivateAction": "Alle privat machen",

View file

@ -1337,6 +1337,7 @@
"megabytes": {"type": "int"}
}
},
"fileAfterCreate": "Pictures and sounds can be added once the studyset is made.",
"fileNotKept": "The file couldn't be kept on this device.",
"fileRemoveImage": "Remove image",
"fileRemoveSound": "Remove sound",
@ -2438,9 +2439,18 @@
"studysetReadableBody": "Everything in it is decrypted and synced again, so the instance can read it: you can publish it, and Claude can read it through the connector.",
"studysetPrivateNoKeys": "Sign in with your password on this device to make studysets private.",
"studysetPrivateSomeLocked": "Some of it couldn't be opened on this device and stayed as it was.",
"studysetPrivateLabel": "Private",
"studysetLockedTitle": "Locked",
"studysetLockedBody": "This studyset is private, and its key doesn't open on this device: it was encrypted with keys your account no longer has. It can only be deleted.",
"studysetVisibilityPartly": "Private, partly readable",
"studysetPrivacyPartlyBody": "Some of it is still readable by the instance: what a device wrote before it heard, or files not moved yet. It is sealed as soon as a device with your keys can, or now with Finish.",
"studysetPrivacyFinish": "Finish",
"studysetLockedNoKeysBody": "This studyset is private, and its key isn't on this device. Sign in with your password on this device to open it.",
"cardMovePrivateTitle": "Make “{title}” private?",
"@cardMovePrivateTitle": {"placeholders": {"title": {"type": "String"}}},
"cardMovePrivateBody": "These flashcards are private. “{title}” is readable by the instance, so they can only move there once it is private too.",
"@cardMovePrivateBody": {"placeholders": {"title": {"type": "String"}}},
"cardMovePrivateAction": "Make private and move",
"privacyKeysMissing": "Sign in with your password on this device: what you add or change here would be private.",
"libraryOfferPrivateTitle": "Make your studysets private?",
"libraryOfferPrivateBody": "New studysets are private: end-to-end encrypted, so only your devices can read them. Those you made before stay readable by the instance until you make them private too. Copies in the instance's backups stay readable until those expire.",
"libraryOfferPrivateAction": "Make all private",

View file

@ -6946,6 +6946,12 @@ abstract class AppLocalizations {
/// **'This file is larger than {megabytes} MB.'**
String fileTooLarge(int megabytes);
/// No description provided for @fileAfterCreate.
///
/// In en, this message translates to:
/// **'Pictures and sounds can be added once the studyset is made.'**
String get fileAfterCreate;
/// No description provided for @fileNotKept.
///
/// In en, this message translates to:
@ -12219,12 +12225,6 @@ abstract class AppLocalizations {
/// **'Some of it couldn\'t be opened on this device and stayed as it was.'**
String get studysetPrivateSomeLocked;
/// No description provided for @studysetPrivateLabel.
///
/// In en, this message translates to:
/// **'Private'**
String get studysetPrivateLabel;
/// No description provided for @studysetLockedTitle.
///
/// In en, this message translates to:
@ -12237,6 +12237,54 @@ abstract class AppLocalizations {
/// **'This studyset is private, and its key doesn\'t open on this device: it was encrypted with keys your account no longer has. It can only be deleted.'**
String get studysetLockedBody;
/// No description provided for @studysetVisibilityPartly.
///
/// In en, this message translates to:
/// **'Private, partly readable'**
String get studysetVisibilityPartly;
/// No description provided for @studysetPrivacyPartlyBody.
///
/// In en, this message translates to:
/// **'Some of it is still readable by the instance: what a device wrote before it heard, or files not moved yet. It is sealed as soon as a device with your keys can, or now with Finish.'**
String get studysetPrivacyPartlyBody;
/// No description provided for @studysetPrivacyFinish.
///
/// In en, this message translates to:
/// **'Finish'**
String get studysetPrivacyFinish;
/// No description provided for @studysetLockedNoKeysBody.
///
/// In en, this message translates to:
/// **'This studyset is private, and its key isn\'t on this device. Sign in with your password on this device to open it.'**
String get studysetLockedNoKeysBody;
/// No description provided for @cardMovePrivateTitle.
///
/// In en, this message translates to:
/// **'Make “{title}” private?'**
String cardMovePrivateTitle(String title);
/// No description provided for @cardMovePrivateBody.
///
/// In en, this message translates to:
/// **'These flashcards are private. “{title}” is readable by the instance, so they can only move there once it is private too.'**
String cardMovePrivateBody(String title);
/// No description provided for @cardMovePrivateAction.
///
/// In en, this message translates to:
/// **'Make private and move'**
String get cardMovePrivateAction;
/// No description provided for @privacyKeysMissing.
///
/// In en, this message translates to:
/// **'Sign in with your password on this device: what you add or change here would be private.'**
String get privacyKeysMissing;
/// No description provided for @libraryOfferPrivateTitle.
///
/// In en, this message translates to:

View file

@ -4315,6 +4315,10 @@ class AppLocalizationsDe extends AppLocalizations {
return 'Diese Datei ist größer als $megabytes MB.';
}
@override
String get fileAfterCreate =>
'Bilder und Töne lassen sich hinzufügen, sobald das Lernset erstellt ist.';
@override
String get fileNotKept =>
'Die Datei konnte nicht auf diesem Gerät gespeichert werden.';
@ -7747,9 +7751,6 @@ class AppLocalizationsDe extends AppLocalizations {
String get studysetPrivateSomeLocked =>
'Manches darin ließ sich auf diesem Gerät nicht öffnen und blieb, wie es war.';
@override
String get studysetPrivateLabel => 'Privat';
@override
String get studysetLockedTitle => 'Gesperrt';
@ -7757,6 +7758,37 @@ class AppLocalizationsDe extends AppLocalizations {
String get studysetLockedBody =>
'Dieses Lernset ist privat, und sein Schlüssel öffnet sich auf diesem Gerät nicht: Es wurde mit Schlüsseln verschlüsselt, die dein Konto nicht mehr hat. Es lässt sich nur noch löschen.';
@override
String get studysetVisibilityPartly => 'Privat, teils lesbar';
@override
String get studysetPrivacyPartlyBody =>
'Manches darin kann die Instanz noch lesen: was ein Gerät schrieb, bevor es davon wusste, oder Dateien, die noch nicht umgezogen sind. Es wird versiegelt, sobald ein Gerät mit deinen Schlüsseln es kann, oder jetzt mit Abschließen.';
@override
String get studysetPrivacyFinish => 'Abschließen';
@override
String get studysetLockedNoKeysBody =>
'Dieses Lernset ist privat, und sein Schlüssel ist nicht auf diesem Gerät. Melde dich auf diesem Gerät mit deinem Passwort an, um es zu öffnen.';
@override
String cardMovePrivateTitle(String title) {
return '„$title“ privat machen?';
}
@override
String cardMovePrivateBody(String title) {
return 'Diese Karteikarten sind privat. „$title“ kann die Instanz lesen, also können sie erst dorthin, wenn es auch privat ist.';
}
@override
String get cardMovePrivateAction => 'Privat machen und verschieben';
@override
String get privacyKeysMissing =>
'Melde dich auf diesem Gerät mit deinem Passwort an: Was du hier hinzufügst oder änderst, wäre privat.';
@override
String get libraryOfferPrivateTitle => 'Deine Lernsets privat machen?';

View file

@ -4277,6 +4277,10 @@ class AppLocalizationsEn extends AppLocalizations {
return 'This file is larger than $megabytes MB.';
}
@override
String get fileAfterCreate =>
'Pictures and sounds can be added once the studyset is made.';
@override
String get fileNotKept => 'The file couldn\'t be kept on this device.';
@ -7674,9 +7678,6 @@ class AppLocalizationsEn extends AppLocalizations {
String get studysetPrivateSomeLocked =>
'Some of it couldn\'t be opened on this device and stayed as it was.';
@override
String get studysetPrivateLabel => 'Private';
@override
String get studysetLockedTitle => 'Locked';
@ -7684,6 +7685,37 @@ class AppLocalizationsEn extends AppLocalizations {
String get studysetLockedBody =>
'This studyset is private, and its key doesn\'t open on this device: it was encrypted with keys your account no longer has. It can only be deleted.';
@override
String get studysetVisibilityPartly => 'Private, partly readable';
@override
String get studysetPrivacyPartlyBody =>
'Some of it is still readable by the instance: what a device wrote before it heard, or files not moved yet. It is sealed as soon as a device with your keys can, or now with Finish.';
@override
String get studysetPrivacyFinish => 'Finish';
@override
String get studysetLockedNoKeysBody =>
'This studyset is private, and its key isn\'t on this device. Sign in with your password on this device to open it.';
@override
String cardMovePrivateTitle(String title) {
return 'Make “$title” private?';
}
@override
String cardMovePrivateBody(String title) {
return 'These flashcards are private. “$title” is readable by the instance, so they can only move there once it is private too.';
}
@override
String get cardMovePrivateAction => 'Make private and move';
@override
String get privacyKeysMissing =>
'Sign in with your password on this device: what you add or change here would be private.';
@override
String get libraryOfferPrivateTitle => 'Make your studysets private?';