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.
This commit is contained in:
r-zakarya
2026-08-22 16:14:04 +01:00
parent 61c52e0edd
commit ba4ab18d0f
5 changed files with 225 additions and 5 deletions
+55 -2
View File
@@ -312,7 +312,7 @@
.sheet { position: fixed; inset: 0; z-index: 80; display: none;
flex-direction: column;
background: linear-gradient(var(--bg-top), var(--bg-bot)); }
body.on-settings .sheet { display: flex; }
body.on-settings #settings, body.on-history #history { display: flex; }
.sheet h2 { margin: 18px 4px 6px; font-size: 13px; font-weight: 600;
color: var(--muted); text-transform: uppercase;
letter-spacing: .06em; }
@@ -550,6 +550,18 @@
</div>
</div>
<!-- ANCIENS MOTS DE PASSE. Trois au plus, et seulement pour l'entree ouverte.
Un voile plutot qu'une rangee du detail : on ne les consulte pas en
passant, et les laisser a l'ecran les exposerait par-dessus l'epaule bien
apres qu'on a fini de les lire. -->
<div class="sheet" id="history">
<div class="head">
<button class="btn" id="histBack">&#8592;</button>
<h1>Previous passwords</h1>
</div>
<div class="scroll"><div id="histBody"></div></div>
</div>
<div class="busy" id="busy"><div></div></div>
<div class="toast" id="toast"></div>
@@ -1260,7 +1272,8 @@ function closeMenu() {
/// un menu qui apparait loin du doigt ne se rattache a rien.
function openMenu() {
var m = document.getElementById('menu');
m.innerHTML = '<button data-icon="fetch">Fetch icon</button>'
m.innerHTML = '<button data-hist>Previous passwords</button>'
+ '<button data-icon="fetch">Fetch icon</button>'
+ '<button data-icon="clear">Remove icon</button>'
+ '<button data-dup>Duplicate entry</button>'
+ '<button class="danger" data-del>Delete entry</button>';
@@ -1276,6 +1289,11 @@ document.getElementById('kebab').addEventListener('click', function (ev) {
function onMenuClick(ev) {
var b = ev.target.closest('button');
if (!b) return;
if (b.getAttribute('data-hist') !== null) {
closeMenu();
send('history');
return;
}
var ic = b.getAttribute('data-icon');
if (ic !== null) {
closeMenu();
@@ -1429,6 +1447,23 @@ function closeSettings() {
}
document.getElementById('setBack').addEventListener('click', closeSettings);
function closeHistory() {
// Le clair part avec la vue, pas seulement de l'ecran.
document.getElementById('histBody').innerHTML = '';
document.body.classList.remove('on-history');
send('state?view=' + view);
}
document.getElementById('histBack').addEventListener('click', closeHistory);
document.getElementById('histBody').addEventListener('click', function (ev) {
var b = ev.target.closest('[data-hcopy]');
if (!b) return;
// UN RANG, JAMAIS UNE VALEUR. C'est la regle du pont partout ailleurs -
// depuis la liste, un indice de coffre ; depuis le detail, un rang de ligne -
// et il n'y a pas de raison qu'un ancien mot de passe fasse exception parce
// qu'il se trouve deja affiche.
send('copyhist?k=' + b.getAttribute('data-hcopy'));
});
document.getElementById('sLog').addEventListener('click', function () {
closeSettings();
if (document.getElementById('log').hidden) togglePanel('log');
@@ -1636,6 +1671,21 @@ var PM = {
document.getElementById('lock').classList.toggle('ahead', !!on);
document.getElementById('btnSync').classList.toggle('ahead', !!on);
},
/// Les anciennes versions descendent ICI et nulle part ailleurs : une seule
/// entree, sur demande. PM.wipe les efface avec le reste du clair.
showHistory: function (json) {
var rows = JSON.parse(json), html = '', i;
for (i = 0; i < rows.length; i++)
html += '<div class="field"><div class="cap">'
+ esc(rows[i].at.replace('T', ' ').slice(0, 16)) + '</div>'
+ '<div class="line"><div class="val" data-h="' + i + '">'
+ esc(rows[i].pwd) + '</div>'
+ '<button class="btn ico-btn" data-hcopy="' + i + '">'
+ ico('copy', 'Copy') + '</button></div></div>';
document.getElementById('histBody').innerHTML = html;
document.body.classList.add('on-history');
send('state?view=history');
},
setVersion: function (v) {
document.getElementById('sVersion').textContent = v;
},
@@ -1649,6 +1699,7 @@ var PM = {
/// Sans cela, un geste d'annulation fermait le coffre - le contraire de ce
/// qu'il veut dire.
clearSelection: function () { clearSel(); },
closeHistory: function () { closeHistory(); },
/// La touche retour referme les reglages avant de toucher au coffre.
closeSettings: function () { closeSettings(); },
// Efface tout ce qui est en clair. Appele au passage en arriere-plan et au
@@ -1658,6 +1709,8 @@ var PM = {
document.getElementById('rows').innerHTML = '';
document.getElementById('fields').innerHTML = '';
document.getElementById('folders').innerHTML = '';
document.getElementById('histBody').innerHTML = '';
document.body.classList.remove('on-history', 'on-settings');
q.value = '';
document.body.classList.remove('on-list', 'on-detail', 'searching');
view = 'unlock';