Commit Graph

64 Commits

Author SHA1 Message Date
r-zakarya cc51540283 fix(android): le clavier se replie une fois l'appui passe
Retenir le focus au mousedown fait atteindre la cible du premier coup,
mais laissait le clavier ouvert : apres "Open vault" il restait dresse
par-dessus la liste. Les deux moities vont ensemble - on retient le focus
le temps de l'appui, on le lache des que l'action est partie.

Sauf si le bouton agit sur le champ lui-meme : un oeil qui devoile, une
fleche qui deroule. Ceux-la vivent dans la meme rangee que le champ, et
replier le clavier apres eux obligerait a le rouvrir pour continuer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 17:47:59 +01:00
r-zakarya 0ec32a7c19 fix(android): le message ne se cache plus derriere le clavier
Le toast vit a bottom: 14px, donc sous le clavier des que celui-ci est
leve : il ne se decouvrait qu'en quittant l'ecran, quand le clavier se
repliait. Et depuis qu'un appui sur un bouton ne vole plus le focus, le
clavier RESTE ouvert - ce qui a rendu systematique un defaut jusque-la
occasionnel.

Il remonte au-dessus de la reserve que body.typing menage deja pour le
clavier : meme mesure, meme raison.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 17:43:26 +01:00
r-zakarya 0930a866c2 fix(android): le premier appui n'est plus mange par le clavier
Un bouton touche pendant qu'un champ a le focus recevait d'abord le
blur : le clavier se repliait, la reserve de body.typing disparaissait,
la page remontait - et le bouton n'etait plus sous le doigt quand le clic
partait. Il fallait appuyer deux fois, la premiere ne servant qu'a fermer
le clavier.

Empecher le defaut du mousedown empeche le TRANSFERT DE FOCUS, pas le
clic : le champ garde le curseur, rien ne bouge, et l'appui atteint sa
cible du premier coup. Pose sur toute la page - le piege n'a aucune
raison d'etre propre aux reglages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 17:40:19 +01:00
r-zakarya b3f725137e fix(android): le message se voit dans les reglages, et le code s'y cache coffre ferme
Le toast n'avait pas de z-index : il se peignait DERRIERE l'ecran des
reglages, qui est a 80. "Unlock code set" partait bien, mais sous la
page, et ne se decouvrait qu'en sortant - quand il ne s'etait pas deja
eteint. Un message qui ne se voit pas la ou on l'a provoque ne sert a
rien.

La section du code de deverrouillage ne parait plus que coffre ouvert.
Poser comme retirer exigent le clair en main : coffre ferme, les deux ne
pouvaient que refuser, et proposer une action qui refuse toujours vaut
moins que ne rien proposer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 17:36:37 +01:00
r-zakarya a41c61405c fix(android): retirer le code de deverrouillage demande de le connaitre
Le coffre ouvert ne prouvait rien : il peut l'etre depuis une heure et
rester a l'ecran. Quiconque trouvait le telephone dans cet etat levait la
protection en deux appuis, sans que rien ne le lui demande.

Le code courant voyage maintenant avec la demande, et Delphi le compare a
FPinSealed - ce sous quoi le cache est REELLEMENT scelle, pose seulement
par une ouverture reussie, jamais par un champ.

Fermee au passage : SetUnlockCode('') retirait le code en silence, ce qui
contournait le controle qu'on vient de poser.

Le FBusy de ClearUnlockCode ne sort plus sans un mot - une sortie muette
se lit comme une reussite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 16:30:30 +01:00
r-zakarya 203f45eb7c fix(android): les refus se voient, et la croix se recale sur le champ
Un refus s'affichait du meme gris qu'un etat : "Master password: 8
characters at least" se lisait comme "Vault on this device", et rien ne
disait que l'application venait de refuser. StatusErr le met en rouge -
deux procedures plutot qu'un drapeau devine du texte, parce que trier des
messages sur leur contenu a deja detruit un cache dans ce projet. C'est
l'appelant qui sait s'il refuse.

La croix des champs etait calee a top: 8px, en compensation du margin-top
du champ. Les piles annulent ce margin depuis qu'on y a unifie
l'espacement, et la croix restait donc 8 px trop bas. Elle s'ancre
desormais au bord INFERIEUR, le seul que le champ touche dans les deux
cas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 16:25:07 +01:00
r-zakarya ed7ccdd2d6 feat(android): deux secrets separes, comme sur le poste
Le mot de passe de creation scellait le conteneur local ET pretendait
sceller l'instantane du depot. Un coffre cree ici publiait donc sous son
propre mot de passe, que le poste ne savait plus ouvrir - et le
rafraichissement echouait avec "wrong password" sur un depot pourtant
intact.

FVaultPwd ouvre le conteneur local et s'arrete la. Le secret du depot vit
dans les reglages, doit etre le meme sur tous les appareils, et c'est lui
seul qui ouvre et rescelle l'instantane.

Les deux ne se rencontrent qu'a UN endroit, la reconciliation : le
conteneur local s'ouvre sous le sien, puis la cle est jetee avant de
toucher au depot - la garder rescellerait le depot sous le mauvais
secret. Ailleurs ils ne se croisent jamais : un coffre ne ici ne publie
pas, un coffre publie n'a plus de conteneur local.

Migration : rien a rechiffrer. Les coffres existants ont simplement les
deux valeurs egales, et cessent d'etre forces de le rester.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 15:58:17 +01:00
r-zakarya ea2450abfd feat(android): dire pourquoi le cache est illisible, et accepter un mot de passe comme code
"Local vault unreadable" laissait croire a une corruption, donc a une
perte, alors que le coffre est intact dans le depot. Quand Android a
lui-meme detruit la cle - verrouillage d'ecran retire ou reinitialise -
l'application le dit maintenant, et dit aussi que rien n'est perdu.

Le tri passe par un drapeau pose la ou l'invalidation se constate, pas
par un test sur le texte du message : c'est ce tri-la, fait sur des
chaines, qui detruisait autrefois le cache en se trompant.

Le code de deverrouillage accepte un mot de passe, et l'a toujours
accepte - aucune validation n'a jamais impose des chiffres. Seul
inputmode="numeric" le laissait croire, et le clavier numerique
decourageait d'essayer. Clavier complet, libelles et note mis a jour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 15:24:29 +01:00
r-zakarya 1a19c838cd fix(android): un seul espacement entre les elements d'une pile
Un champ porte son propre margin-top de 8 px, dont il a besoin dans les
reglages ou les champs se suivent en flux normal, sans gap. Dans une
pile, ce margin s'ajoutait au gap : deux champs se retrouvaient a 16 px
l'un de l'autre, un champ et un bouton a 8. Dans une pile, c'est le gap
qui commande.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 14:51:45 +01:00
r-zakarya ab5eeb60f5 fix(android): l'accueil se lit de haut en bas, et ses blocs respirent
L'espace de 8 px vit A L'INTERIEUR de chaque pile. "Create vault" et
"Download vault" etaient dans deux piles voisines et se touchaient donc,
tandis que "Download vault" et "Settings", dans la meme pile,
respiraient. Un margin entre piles adjacentes suffit.

L'ordre suit maintenant les frequences et non l'ordre du code : aller
chercher le coffre, regler, creer en dernier. La creation etait en tete
parce que son bloc etait ecrit en premier dans la page.

L'accent suit le geste attendu - il va a "Download vault" des qu'un depot
est configure, et ne reste sur la creation que lorsqu'il n'y a rien a
aller chercher.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 14:42:54 +01:00
r-zakarya 900f5ba740 fix(android): poser, changer et retirer le code exigent un coffre ouvert
Deux branches de l'ouverture posaient le code a partir de ce qui trainait
dans les champs.

La premiere appliquait un "nouveau code" depose par les reglages. Elle
n'exigeait rien de celui qui le deposait : n'importe qui tenant le
telephone deverrouille pouvait en poser un, et c'est le proprietaire qui,
en ouvrant son coffre, l'appliquait contre lui-meme.

La seconde rescellait sous le code tape des que le cache ne l'etait plus,
ce qui DEFAISAIT LE RETRAIT : on retirait le code, le champ le portait
encore, et l'ouverture suivante le reposait sans qu'un mot le dise. Le
premier octet du cache le disait - VER_PIN apres un retrait annonce
reussi.

Poser et changer passent maintenant par SetUnlockCode, jumeau de
ClearUnlockCode : geste explicite, coffre deja ouvert - donc code courant
connu - et effet immediat. Le champ de l'accueil ne veut plus dire qu'une
chose, "ouvre avec ceci", et ne parait que lorsqu'un code est pose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 14:17:24 +01:00
r-zakarya 45564e7266 fix(android): un seul bouton sur l'accueil, et il ne ment pas sur sa destination
Il y en avait deux, "Settings" et "Log", et le second a fini par ouvrir
le premier depuis que le journal a demenage dans les reglages. Un bouton
nomme "Log" qui ouvre "Settings" se ment sur son etiquette.

Settings redevient VISIBLE EN PERMANENCE. Le cacher des qu'un coffre
existe laissait l'accueil sans aucune porte vers les reglages - et c'est
precisement quand le coffre refuse de s'ouvrir, code faux ou adresse
fausse, qu'on en a besoin avec le journal. Ce n'est pas un doublon de la
roue dentee : celle-ci est DANS le coffre, ce bouton est sur l'accueil,
jamais cote a cote.

En release, c'est desormais le bouton des reglages qui s'efface, l'appui
long sur la ligne d'etat le ramenant comme avant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 14:09:17 +01:00
r-zakarya fe00fe8484 fix(android): le journal se deplie dans les reglages, la ou on le demande
Il n'affichait rien, et pas parce qu'il etait vide. togglePanel
parcourait ['cfg','log'] et cherchait pour chacun le bouton qui le
commande - or btnCfg a perdu son data-panel le jour ou les reglages sont
devenus un ecran. La premiere iteration levait sur un null, et le journal
n'etait jamais deplie, quel que soit le chemin.

Il vivait de plus sur l'accueil : "Show the log" refermait les reglages
pour le deplier derriere, c'est-a-dire sur un ecran qu'on ne regarde plus
des que le coffre est ouvert. Il est maintenant DANS les reglages, deplie
sur place - le seul ecran atteignable depuis l'accueil comme depuis le
coffre.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 12:55:06 +01:00
r-zakarya 6c5fb2f4ea fix(android): Sync ouvre puis synchronise, et l'accueil ne demande plus le nouveau code
Le garde pose juste avant fermait la seule porte : le bouton Sync ne vit
que sur l'accueil, et une fois le coffre ouvert on est sur la liste, ou
il n'existe pas. Synchroniser devenait impossible des qu'un code etait
pose. Sync ouvre donc le coffre - ce qui verifie le code, seul LoadVault
le peut - et OpenLocalNow reprend la synchronisation ou elle s'est
arretee. Le drapeau est consomme des sa lecture, pour qu'un code faux ne
laisse pas une synchronisation en embuscade.

Le champ "new unlock code" quitte l'accueil : les reglages posent et
changent le code, il y faisait doublon sur l'ecran ou l'on veut seulement
ouvrir. Il reste dans la page, cache - c'est lui que "Set this code"
remplit avant pushConfig.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 12:50:15 +01:00
r-zakarya ac3e9db4d9 feat(android): l'accueil ne repete plus Settings quand le coffre est la
La roue dentee de la liste mene au meme ecran : le repeter sur l'accueil
n'encombrait que l'accueil.

Mais pas de suppression pure et simple. Sans coffre local, la liste
n'existe pas - donc la roue dentee non plus - et il faut bien pouvoir
saisir l'adresse du depot pour aller le chercher. Le bouton parait donc
exactement quand "Open vault" ne parait pas, sur le meme signal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 12:36:49 +01:00
r-zakarya ba4ab18d0f feat: the last three passwords travel, and the phone can show them
The desktop keeps twenty previous versions per entry and never sent any. It
now sends three.

Three, not twenty, because the trade is not symmetrical. On the desktop they
sit encrypted column by column and never move. In the snapshot everything
travels in the clear inside the container, and comes down into a phone cache
sealed only by the device key - not by the master password. An old password
stays sensitive: it is often still valid somewhere else. The real use of the
list is short - "I changed it recently, what was it?" - and three cover it.
The other seventeen would be copies with no need facing them.

The phone reads them ON DEMAND from the raw snapshot, never into TVault:
loading them with the vault would put every entry's old passwords in the clear
in memory from the moment it opens, for something one looks at now and then.

Copying goes by RANK, not by value. The page displays the text but never sends
it back - that is the bridge's rule everywhere else, and an old password has
no reason to be the exception just because it happens to be on screen.

They leave with the plaintext at lock, on both sides.
2026-08-22 16:14:04 +01:00
r-zakarya 2010dec22a feat(android): an entry can get its icon, through the same relay as the desktop
Fetching https://<site>/favicon.ico directly would resolve a DNS name - and
open a connection - to EVERY domain kept in the vault. The list of sites where
someone holds an account is precisely what this vault protects; handing it to
the network for a sixteen-pixel drawing would be a poor trade. The relay sees
only that someone is interested in one domain, which is why the call is asked
for entry by entry and never happens on its own.

Two tries: the full host, then the second-level domain - chat.qwen.ai has no
icon at the relay, qwen.ai does. The type comes from the first bytes, never
from the extension: the relay answers PNG under a .ico address.

A dedicated patch rather than PatchEntryField, because icon_custom is a NUMBER
in the snapshot and writing it as a string would give "0" - which the desktop
reads with GetValue<Boolean> and would choke on. It is set to zero: the icon
comes from an automatic lookup, not from a choice, and that flag is what stops
the desktop from ever replacing a chosen icon during a refresh.

Base64 without line breaks. Delphi's encoder wraps at 76 characters by
default, a wrapped data: URI does not display, and that trap has already been
paid once on the containers.
2026-08-22 15:39:19 +01:00
r-zakarya 73e3faa24c feat(android): a settings screen, and the unlock code can finally be removed
Three things of different natures lived in collapsible panels on the home
screen - repository credentials, the unlock code, the log - on the one screen
where one mostly wants to open one's vault. They move to a settings sheet,
reachable from the list too.

A sheet, not a fourth view of the rail: each view costs its transform rules
and a place in back-key navigation, and settings is not somewhere one moves
about - it is a parenthesis one opens and closes.

The unlock code can now be REMOVED. It could not before: clearing the field
meant "nothing typed", not "drop the protection", so one could set a code and
never get out. It requires an open vault, and that is the proof being asked
for - the cache only let itself be read under the right code. Without that,
anyone holding the unlocked phone could lift the protection without knowing
it.

The refusal that guards every other path stays: it is what stopped a sync
from silently undoing a code. Removing is the one deliberate way out, and it
says so at the call site.

The old fields stay in the document as the source Delphi knows; the sheet is
only a view of them. Emptying them would lose the configuration at the first
sync.
2026-08-22 15:34:40 +01:00
r-zakarya 4ae845bc13 feat(android): tell when the repository has moved, without adopting it
A probe every ten minutes compares the remote fingerprint to ours and lights
an indicator. It does not pull: adopting on its own while someone is reading
an entry would change it under their eyes. The user decides when.

The body is downloaded anyway - Gitea's contents API cannot answer without it
- but it is thrown away undecrypted: no derivation, no write, just a string
comparison. The cost is network, not time or battery.

It rides the idle timer rather than a second one. That timer already beats
every 15 seconds for auto-lock; counting its beats costs an integer, where
another timer costs an object, a lifecycle, and one more chance to forget it
on shutdown.

Two indicators, two colours, two gestures. Orange: things wait HERE and the
repository does not know. Blue: the repository holds things we do not have.
One dot for both would say "something is wrong" without saying what - and one
is resolved by publishing, the other by fetching.
2026-08-22 15:28:41 +01:00
r-zakarya 92ceb1e8c9 fix(android): the merge lost folders, and the pending dot was on one screen
Two defects found by re-reading before adding more, along with a test plan.

MergeSnapshots merged entries and tombstones but not folders. A folder created
on the phone - offline, or just before another device published - vanished
from the list while the entries kept its name: filed under a folder nothing
displays, unfindable without knowing it. That is exactly what EnsureFolder was
written against, come back through another door.

The pending dot sat on the padlock, which only exists in the list. After
locking one lands on the home screen - precisely where one decides to sync -
and it said nothing there. It is on Sync as well now.

TESTS.md records what the reading checked and what it left alone, with reasons
for the two defects deliberately not fixed, and a scenario that says what each
step must PRODUCE rather than only what to do.
2026-08-22 15:24:01 +01:00
r-zakarya a24b18e90f feat(android): saving works without a connection
The save used to start from the network: without it, nothing was kept. But
what someone has just typed is already theirs - publishing is a consequence,
not a condition. The cache is written first, always, and the repository
follows when it can.

A failed publish no longer loses anything. It raises a flag, and the toast
says both things: that it is kept, and that the repository does not know yet.
A bare "failed" would have people retype what is already there.

On conflict the two snapshots are now MERGED rather than the change replayed.
Replaying one edit onto the fresh snapshot was right while writing required
being online; with offline saving several changes can be waiting here, and
replaying one would drop the others.

Sync publishes what is pending BEFORE fetching. Fetching first would overwrite
our cache with the repository's version and the offline change would vanish
without a word - exactly what offline saving promises to prevent.

A dot on the padlock says changes are waiting: discreet until looked for,
present at every glance.
2026-08-22 14:56:04 +01:00
r-zakarya 248cdafa49 fix(android): a note can be written on several lines, and Enter means Enter
The creation form still put a note's body behind a single-line input, so one
could read and edit multi-line text but never create it - the note had to be
created, then reopened to be laid out.

And the newline could not be typed at all: the handler that turns the
validate key into "fold the keyboard" applied to every field. That rule
exists because in a one-line field the key can only jump to the next one, and
the next one sat on an off-screen view - which is how the unclosable keyboard
happened. In a text area, Enter means Enter. It is the third time that
handler has misfired by treating "any field" as one thing; it now carries its
three exceptions explicitly.

Two things fixed in the same code, since they lived there:

Rows were recognised BY THEIR LABEL - prose. Renaming a caption would have
been enough for this code to stop acting on it, silently. They go by id now.

And what is already typed survives a change of kind. Replacing the control
without carrying the value over would have wiped the entry for a mere change
of mind between Login and Note.
2026-08-22 14:33:58 +01:00
r-zakarya 4672ba8fd8 fix(android): a note's line breaks show in view, not only in the editor
white-space defaults to normal, which folds newlines into spaces: the body of
a note read as one run-on paragraph, while the editor showed it correctly.
The formatting looked lost until you opened the pencil - the worst kind of
mismatch, since it invites re-typing what was already there.

The same rows also drop break-all, which cuts mid-word. That suits a password
and not prose; they break only where they must.
2026-08-22 11:48:54 +01:00
r-zakarya 8315ce9230 fix(android): editing a note was about to strip its line breaks
Making every field editable put a note's body behind an <input>, and HTML's
value sanitization removes CR and LF without a word. The formatting would
have been lost on the phone and published to the repository at the next
save - silent, and irreversible once the desktop pulled it.

Multi-line is marked on the row in Delphi, like the field name, rather than
guessed from the caption or from whether the value happens to contain a
newline today: a single-line note one wants to make multi-line deserves the
same control.

The row wraps through a class set when the editor opens, not through :has() -
that selector does not exist before Chrome 105, and an older engine would
ignore the rule without saying so.
2026-08-22 11:33:10 +01:00
r-zakarya 6b3001666e feat(android): attachments can be saved, section 3 item 1 is done
Two questions, and the code was only the second one.

WHERE THE FILE LANDS. The private directory is useless - no file manager
opens it and it leaves with the uninstall - and direct external storage has
been closed since Android 10. Of the three remaining doors, MediaStore ->
Downloads is the only one needing neither a permission nor an extra screen:
since API 29 an app may write what it inserted itself. The destination picker
would need an activity result, which FMX makes painful; sharing would need a
FileProvider in the manifest. Below API 29 it would take
WRITE_EXTERNAL_STORAGE - one of the ten permissions just removed - so it
refuses and says so.

WHERE THE CONTENT IS. TVault drops it on purpose: a single attachment took
the cache from 13 to 174 Ko. It is re-read from the raw snapshot, which has
been in memory ever since saving stopped fetching before publishing. The same
data now serves two needs that had nothing to do with each other.

Decoding and writing happen off the main thread, under the waiting veil: a
few megabytes would freeze the screen, and the veil would not even paint.

What leaves the application leaves its protection: the file lands in the
clear in Downloads. The confirmation names the place rather than saying
"saved".

Release configuration compiles too - checked, since that is what section 5
caught out.
2026-08-22 00:41:05 +01:00
r-zakarya 2e84c8a7f8 fix(android): the busy veil was switching the whole page off
An empty dark blue screen, which is the window's own background with nothing
drawn on it. The cause is one name used twice: the veil element carries class
`busy`, and the state was put on the body under the same name - so
`.busy { display: none }` applied to the BODY as well, and the page vanished
the moment any operation started.

The state is now called `is-busy`, which nothing else carries. Checked the
other body states - typing, selecting, searching, on-list, on-detail - none
of them names an element, so none has the same trap.
2026-08-22 00:23:34 +01:00
r-zakarya 5df1c7e8e8 feat(android): say that work is happening, on every screen
The home screen's buttons already dimmed during an operation, but from the
list nothing moved: deleting or duplicating takes a second or two - the
container's round trip - and the screen stayed identical, as if the tap had
not registered. A veil with a turning arc now covers any screen, and it also
prevents the second tap, which would not wait any better.

Its safety net is the existing 25 s deadline: even if an operation never
hands back, the veil lifts.

The plan records where deleted entries actually are: every publish is a Gitea
commit, so the snapshot from before the deletion is still in the repository's
history, readable in its web interface and recoverable by import. Not a
screen, but a real net - and one that catches what a trash would not: an
unlucky edit, a bad import.
2026-08-22 00:18:07 +01:00
r-zakarya ebd4752ec9 feat(android): a selection bar in the list, and Duplicate everywhere
The floating menu was wrong for a list. It closed on the next tap, so opening
an entry afterwards took two gestures and felt like a button had got stuck.

Android's contextual bar instead: a long press selects, the top bar takes the
place of search, says how many rows are held, and offers duplicate and
delete. It goes away when nothing is held. One does not search while choosing
what to delete, and two rows of controls would fight over the same thumb.

Selection is multiple, because the cost is in the bar, not in the work: the
patch closure loops, and ten deletions become ONE publish. The container goes
up whole on every write - deleting ten entries one by one would pay that trip
ten times, and give nine chances to fail halfway.

A tap during selection picks rather than opens, which is also the way out:
unpicking the last row closes the bar. The hardware back cancels the
selection before locking the vault - locking because someone wanted to cancel
a choice is the opposite of what the gesture means.

Both menus stay, with distinct jobs: the bar acts on rows chosen in the list,
the detail's three dots act on the entry in front of you. Duplicate copies
everything, attachments included, as the desktop does - a copy that silently
lost the attached document would be a surprise discovered too late.
2026-08-22 00:10:38 +01:00
r-zakarya 0619740da1 fix(android): delete from the list, and stop reopening what was deleted
Two things, and the second was me misreading the question: the affordance was
asked for in the LIST, not in the opened entry.

A long press on a row opens the same menu. Not a cross per row - an
irreversible action a thumb's width away on a list one scrolls - and not a
third button per row, when those were just cleared away. A long press is
invisible until used, it is Android's convention for acting on a list item,
and it is where multi-select would go if it ever comes.

And the deleted entry reopened itself. editDone hands control back after a
refusal, but it redrew the LAST detail - after a delete the view has already
returned to the list, so the entry one had just erased came back on screen.
It now hands back to what is on screen, not to what was.

Delphi deletes by rank when the list asks, by the open entry otherwise -
exactly like open and copy. The page never sends a uuid: it has one, but
letting it name a row Delphi never displayed is a door with no reason to be
open.
2026-08-21 23:51:09 +01:00
r-zakarya 00d5b582cc feat(android): delete moves into the entry's menu
It was a full-width red button at the foot of every detail, permanently in
view for a gesture one makes three times a year. It now lives behind the
three dots in the header: open, then confirm - two deliberate gestures, and
nothing lingering in sight the rest of the time.

A single item in a menu is not an oversight. The action has no way back, and
an action with no way back should not sit where a stray tap reaches it. A
cross in the corner would be worse still: that corner means "close"
everywhere else.

The plan records what the server code says about the trash: receiving a
tombstone runs DELETE FROM vault_entries, a hard delete. A deletion from the
phone removes the row on both sides and bypasses the desktop's trash. The
only net is the desktop's backups, and there is no trash to show here - the
phone never receives its contents.
2026-08-21 23:40:35 +01:00
r-zakarya a3a6d8b0f8 fix(android): a dropdown raises no keyboard, so nothing should move
Focusing the Folder field made the screen jump: every .field-in was treated
as if a keyboard were coming, reserving 60% of the height and lifting the row
to the top of the panel. A <select> opens a native list over the page - it
occupies nothing and vanishes on choice.

And the lift threshold now says what the keyboard actually covers, a little
under the bottom half. At 40% it raised fields that stayed perfectly visible,
moving under the eye what was already fine.
2026-08-21 23:29:06 +01:00
r-zakarya b1f84cb977 style(android): the inline editor's buttons become icons too
Show, Cancel and Save shared their row with the input field, and three words
left barely enough room to read what one types - the very crowding that made
the row overflow in the first place. A cross and a tick read without
translation.

The tick gives way to a turning arc while saving. The word "Saving..." said
the wait was real; a motionless tick would say nothing, and it is exactly
during that second or two that one presses again.

The form's Save and Delete entry keep their words: they are not inline
controls sharing a row, and the second is the only action with no way back.
2026-08-21 22:41:55 +01:00
r-zakarya 68722e301c style(android): the icon buttons lose their chip, keeping their target
Three translucent pills per list row weighed more than what they pointed at.
The drawing alone carries it.

The target stays 44 px: that is what a finger expects, and it does not need to
be visible to exist. The press feedback moves from the background to the
stroke, since there is no background left to dim.
2026-08-21 22:21:26 +01:00
r-zakarya 1dba5d392f fix(android): the tap lands on the drawing, not on the button
Copying a password or a username opened the entry instead - about half the
time, which is the worst kind of failure to diagnose. Since those buttons
carry an icon, the tap target is the <svg> or one of its strokes, so reading
the attribute off the target returned null and the handler fell through to
"open". A finger landing sometimes on the glyph and sometimes on the margin
made it look random. Both handlers now resolve to the button first.

The new-entry form gets a folder, like the editor: an entry filed from the
start saves having to move it.

And a folder can now be created, not only chosen. Filing an entry under a
name is not enough - it is `folders` that fills the desktop's column, and a
name missing from it leaves the entry in a folder nothing displays, looking
lost. The two now go together, in one place, for both editing and creation.
'All' is never registered: it is the default, not a folder.
2026-08-21 22:11:14 +01:00
r-zakarya 0badc5fcfc feat(android): folder and 2FA secret can be changed, and the buttons wear icons
The folder had no row of its own - it shared "Folder / tags" with the tags,
so neither could be touched. It is now CHOSEN from the known folders rather
than typed: a typo would file the entry under a folder nothing displays,
here or on the desktop, and it would look lost. Tags stay read-only because
they are an array in the snapshot, and writing one as a string would break
its shape for the desktop.

The 2FA secret gets its own masked row. The code has had a panel for a long
time, but it is computed and renewed every thirty seconds; the secret is what
one sets, changes and removes, and nothing allowed that from the phone.

User, Pass, Copy, Show and Edit become drawings. Four words on a list row left
little room for the entry's name and had to be re-read every time. Every icon
carries an aria-label: replacing a word with a drawing must not take the
information away from someone listening to a screen reader.
2026-08-21 21:51:42 +01:00
r-zakarya 0b0f1e3fcd chore(android): section 5 - ten permissions down to three, no secrets in release
The manifest asked for fine and coarse location, camera, phone, phone state
and external storage - Delphi's defaults, none of them used. On a password
vault those are exactly the wrong signals: whoever reads the app's listing saw
a request for position and camera with no possible explanation. The list is
written by hand now.

A trap on the way: naming the substitution marker inside the comment that
explains its removal is enough to bring it back. The substitution is textual
and does not care that it lands in a comment.

The bench credentials are no longer compiled into a release - the local
include is debug-only. Verified in the binary in UTF-8 AND UTF-16: Delphi
stores strings as UTF-16, so an ASCII grep returned zero on both sides and
proved nothing.

The log leaves the home screen in release without leaving the application: a
1.5 s press on the status line brings it back. Nothing secret is in it, but a
permanent Log button is development noise, and diagnostics must stay reachable
for the day something goes wrong remotely.

And the Release configuration did not compile at all, which nobody knew: the
two self-checks had their declarations under IFDEF DEBUG and their bodies
outside. Invisible as long as one only ever builds Debug.

Signing is what remains, and the keystore password is the user's to hold.
2026-08-21 17:38:46 +01:00
r-zakarya 795f07fd8a fix(android): Cancel works, because lastDetail is now written
It was introduced with two readers and no writer. Cancel called
renderDetail(null), which threw on d.rows and did nothing at all - and
editDone, which uses it to hand control back after a refusal, was dead the
same way. That is why the successive fixes for "Cancel does not respond"
changed nothing: they all ended in the same silent throw.

Assigned where a detail is drawn, which is the one place that knows it.
2026-08-21 16:38:26 +01:00
r-zakarya 5522089939 fix(android): the unlock code no longer changes by itself
It went from 0000 to 00000000 with nobody deciding it. The typed code and the
code the cache is sealed under were the same variable, so every write of the
cache re-sealed under whatever sat in the field - a sync, which asks for no
code at all, was enough to change it, silently.

They are two things now. Only opening the vault touches the seal, because
that is the one moment we know the current code is known: the decryption just
proved it. Everything else - sync, background refresh, saving an entry -
rewrites the cache under the code already in place.

Hence a second field, shown only once a code exists: the current one must open
the vault for the new one to take. Without it this fix would have locked the
device under the code set by accident, which is exactly the harm it is meant
to prevent.
2026-08-21 16:30:57 +01:00
r-zakarya 5107ebf7b2 feat(android): an entry can be deleted from the phone
Both, or nothing: the row leaves the snapshot AND a tombstone takes its
place. Removing the row alone does not delete - the desktop still holds its
own copy, and the next pull would bring it back as an entry we never had.
The time matters too: the desktop's arbitration keeps an entry edited AFTER
the deletion rather than killing it a second time.

No trash on the phone, deliberately. The desktop's is a `deleted` flag that
does not travel in sync, so a deletion stopping there would propagate
nowhere. Hence a button at the very bottom, in two taps, that disarms itself
after four seconds - a button left armed gets touched by accident much
later, and this is the one action with no way back.

The confirmation is in the page because the engine cannot offer one: a
JavaScript prompt needs a WebChromeClient FMX does not provide.

Not exercised on the device: it would have destroyed a real entry.
2026-08-21 16:21:45 +01:00
r-zakarya 732c52a64e feat(android): every field of an entry can be changed, not just the password
PatchEntryField already took a field name; what was missing was carrying it
from the row. It travels in the detail row itself rather than being guessed
from the label - a caption is prose, and the day it changes the page would
send a field name that does not exist, or quietly stop offering the edit.

Delphi does not take it on trust either: four names accepted, the rest
refused. The snapshot also carries tombstones, attachments and timestamps
that PatchEntryField would overwrite without understanding them.

An empty row now shows IF it can be filled - a missing username you cannot
add is a dead end. Emptying is allowed, except on a password, where it would
almost always be a slip; the refusal says so rather than doing nothing.

Show only appears on a masked field: on a site in clear it has nothing to
reveal, and would in fact hide it. A note keeps only Name and Content - a
site and a username mean nothing there.
2026-08-21 16:13:35 +01:00
r-zakarya 131bf44b36 chore(android): drop the keyboard diagnostic, and the field only jumps if it must
The trace has served: it named the cause where two rounds of reasoning had
missed it. Out with it, and out with the viewport measurement it proved
worthless here - a value that is always zero reads as a mechanism, and the
next person would trust it.

adjustResize goes too. Measured to do nothing: Android ignores the mode for a
fullscreen window, which FMX's is. Leaving it in the manifest would suggest it
carries something.

A field already near the top of the panel - the settings ones - stays where it
is. It was visible as it was, and lifting it only moved under the eye what was
already fine.

The plan keeps what the measurement cost: on this screen, instrument first.
2026-08-21 15:46:06 +01:00
r-zakarya ad8a037e29 fix(android): room under the form while typing, measured by nobody
adjustResize changed nothing either: FMX's window is fullscreen, and Android
ignores the mode there. So three sources were asked what the keyboard covers
- visualViewport, innerHeight, the window itself - and all three answer as if
it were not there.

Stop asking. While a field has focus the panel reserves 60% of the height
below its content; that is overflow, so it scrolls, and the focused field is
lifted to the top of the panel where no keyboard can reach it whatever its
size. The reserve goes at the first field left, so nothing floats when one is
not typing.

Crude on purpose. Every attempt at measuring this correctly has been wrong,
and the honest version of "we cannot know" is to make the answer not matter.
2026-08-21 15:40:58 +01:00
r-zakarya 79a6625613 chore(android): make the page say what it measures about the keyboard
Two fixes placed by reasoning changed nothing observable, which means the
reasoning was wrong somewhere I cannot see from here. This writes the
numbers into the log rather than a fleeting message, so they can be read
after the fact: the window height, the visible height, and - the one that
settles it - what the scroll box shows against what it contains. Equal, and
there is nothing to scroll, which would be the whole problem.

Temporary. It comes out once the cause is known.
2026-08-21 15:21:10 +01:00
r-zakarya bde22d2288 fix(android): the keyboard no longer buries the bottom of a form
The frame is nailed to the whole screen, so when the keyboard rises the
content overflows nothing and the bottom third becomes unreachable: the
form's password field could only be seen by folding the keyboard away.

Reserving below the scroll area exactly the height the keyboard covers
gives the panel something to scroll. The measure answers correctly whether
or not the engine actually resizes the window - if it does, innerHeight
shrinks too, the reserve is zero, and the natural overflow already does the
work. No need to know which case applies.

A first attempt shrank the body to the visible height; it changed nothing
observable, and it depended on which of three over-constrained box
properties the engine drops. This does not.

The plan records where a save's time actually goes: 46 ms of crypto, 2745 ms
of PUT for 267 Ko. That is the floor for a monolithic container.
2026-08-21 15:16:12 +01:00
r-zakarya d65fe909b4 feat(android): entries can be created on the phone
A vault you cannot add to is not a vault, so an account created here was
useless without this. A plus button next to the padlock, a form, and a
kind - login or note, which the vault distinguishes and whose body is the
password field either way.

One save path now, two transports, and the rule that decides between them
is: if a local container exists it is the authority. A vault born here has
no repository; a vault pulled from one has no local container. The case
where both exist - a local vault that later gains a repository - needs a
merge, and that is section 7 step 4.

The patch arrives as a function rather than an already-patched snapshot:
the remote path must modify WHAT IT HAS JUST PULLED, or it would overwrite
whatever another device published in between. That constraint is why the
two transports could not simply share a finished payload.

Two traps closed while passing: the form left the typed password sitting
in the document after the save, and a refusal left its Save button dead
with the text behind it.

Verified on the device that the button and the form appear and read
correctly. Saving was not exercised - the vault on this phone syncs to a
real repository, and a test entry would have been published to it with no
way to delete it from here yet.
2026-08-21 14:22:08 +01:00
r-zakarya f32cde81fc feat(android): a local account, so the app serves without a repository
Until now the phone could only read a vault that came from elsewhere:
without a URL, a token and a sync password it showed nothing. It now
signs in like the desktop does - username and master password - and can
create its own vault, with no network at all.

The local container carries exactly what the repository would carry: same
format, same encryption under the password. Two reasons, not one. It is
the desktop's model, where the master password protects the vault at rest
rather than the hardware alone; and the format staying the repository's
means sync keeps working with nothing to convert. No Keystore on that
file - it is already encrypted under the password, and binding it to the
device would make it unrecoverable after a reinstall. vault.bin keeps
that role: it is the fast-open cache, and only it needs sealing.

The password opens the container - the decryption itself is the check, no
secret is ever compared - and the username, read from inside once open,
says whose vault it is.

The home screen now knows which of three situations it is in: nothing
here yet, a container without a cache, or a cache ready to open.

Verified on the device: with the cache set aside, the screen offers
username, master password and Create vault, alongside the download path.
Creating an account could not be exercised to the end - the device locked
its screen mid-run - and the vault that was set aside is back in place,
same size, still sealed under its code.
2026-08-21 14:07:39 +01:00
r-zakarya afc6119cf8 fix(android): the unlock code is asked for, and never undone in silence
Two defects, one feature made useless. The cache did seal under the code,
but nothing on the home screen asked for it: opening without one failed,
the failure was read as corruption, and the device offered to download a
vault it held perfectly well. And the next refresh rewrote the cache in
the old shape, so the code was undone seconds after being set, silently.

The code now lives on the home screen, not among the server settings: it
seals this device's cache, while the other three fields describe the
shared remote. Always visible - hiding it when none is set would remove
the only place to set one - the hint saying which case you are in. An
absent code is no longer attempted, a wrong one is no longer corruption,
and a sealed cache is never rewritten without its code.

The two panels each got an arrow, and only one opens at a time.

Measured end to end on the device: version byte 1, code typed, open, byte
4; cold start says "Unlock code required" and open without the code
leaves the byte at 4; correct code opens the vault, and the refresh that
follows leaves it at 4.
2026-08-21 13:04:48 +01:00
r-zakarya 90ea09a718 fix(android): the keyboard closes, and the write test is gone
Same origin as the sideways slide. The keyboard's validate key asks for
the next field, and the next one in the document sits on a view parked
outside the frame. Focus landed there unseen: the keyboard changed shape
- numeric for the code, alphabetic for the search - and would not close,
because the field holding it open was off screen.

No field of a resting view keeps the focus now, and the validate key
closes the keyboard instead of jumping. In the editor it saves, which is
what the key is for after typing a password.

The write test goes with it: it proved the write path before the first
real PUT, that path now carries the vault itself, and the button, its
handler, the probe and the debug hook that revealed it were only ever
scaffolding.
2026-08-21 12:39:22 +01:00
r-zakarya 7a9fe4ae77 fix(android): nothing slides sideways any more
The previous attempt clamped the scrolling panes, and the panel still
slid. It was not the panes: overflow:hidden forbids the GESTURE, not the
engine, which keeps the right to move a box so the newly focused field
comes into view - and the rail's off-screen views, parked at
translateX(100%), leave it room to the right. Confirmed by measurement:
the CSS was in the shipped library and the behaviour was unchanged.

Caught in the capture phase now, whichever box moves and whatever moves
it. No view here has content to the right, except the folder strip.
2026-08-21 12:34:09 +01:00
r-zakarya 75fde64c20 fix(android): the detail panel can no longer be dragged sideways
The editing row kept the full-width field and set three buttons beside
it, so the line was wider than the frame. A pane scrollable on one axis
is scrollable on both - the specification turns the untouched axis into
`auto` - so the overflow was draggable, and the keyboard's Enter key
brought the field back into view, taking the whole panel with it.

The field now shares its line, and no pane scrolls sideways. Enter
validates, which is what the key was pressed for.
2026-08-21 12:30:42 +01:00