diff --git a/PROGRESS.md b/PROGRESS.md
index 6fc97bf..c5b5e8c 100644
--- a/PROGRESS.md
+++ b/PROGRESS.md
@@ -56,6 +56,35 @@ and until now nothing set them.
---
+## 2026-09-10 — Fixed: pressing play made an episode vanish from the list
+
+Reported as "where did Session Zero go, and why do parts share a number?".
+
+Two separate things, one of them a real bug.
+
+**The bug.** `play()` marked an episode read the moment playback started. The default view is the
+Unread tab, so pressing play removed the episode from the list you were looking at — indistinguishable
+from it going missing. Confirmed from the data: all four read entries had `position = 0`, meaning
+playback never passed the 10-second save threshold. They had been started, never listened to.
+
+Marking read now happens when an episode is actually consumed — on `ended`, or once playback passes
+90% — never on play. Two earlier PROGRESS entries described mark-on-play as a deliberate feature;
+it was a design error, and this supersedes them.
+
+Also restored the four affected rows to unread, since their read flag was purely an artifact of the
+bug (`UPDATE entries SET read=0 WHERE read=1 AND position=0` — no genuinely-played episode could
+match, since playing one writes a position).
+
+**Not a bug: repeated episode numbers.** Each story part ships as two items — the main episode and a
+shorter `Junk in the Trunk` companion — with distinct GUIDs, durations and files, and the publisher
+gives both the same `itunes:episode`. Zero duplicated titles across all 131 entries. `Session Zero`
+additionally carries a season with no episode number and drops the "Part N" naming, so it does not
+match the pattern its siblings follow.
+
+Verified: the arc shows 11/11 items under the default Unread view again.
+
+---
+
## 2026-09-10 — Titles: RSS `
` is the only source
Confirmed against the live feed rather than assumed: 131 feed items, 131 stored, **0 mismatches** —
diff --git a/web/index.html b/web/index.html
index 49f1168..5c1d875 100644
--- a/web/index.html
+++ b/web/index.html
@@ -495,7 +495,20 @@ async function epAction(a,e,el){
/* ---------------- player ---------------- */
const audio=$('#audio');
-const player={guid:null,feed:null,entry:null,saveAt:0};
+const player={guid:null,feed:null,entry:null,saveAt:0,marked:false};
+
+// Marked read when an episode has actually been listened to -- at the end, or past 90%.
+// NOT on play: doing that made the episode vanish from the default Unread list the instant
+// you pressed play, which looks exactly like it went missing.
+function markPlayed(){
+ if(!player.guid||player.marked) return;
+ player.marked=true;
+ const e=player.entry;
+ if(!e||e.read) return;
+ e.read=true;
+ api(`/api/entries/${encodeURIComponent(e.feed_id)}/${encodeURIComponent(e.guid)}/flags`,
+ {method:'POST',body:JSON.stringify({read:true})}).then(()=>loadFeeds(true)).catch(()=>{});
+}
function play(e){
const enc=e.enclosures.find(x=>x.path);
@@ -512,10 +525,7 @@ function play(e){
$('#pfeed').textContent=f?(f.title||f.id):'';
$('#player').classList.add('on');
mediaSession(e,f);
- // Playing it is the clearest signal it has been listened to; retention deletes
- // read episodes before unread ones.
- if(!e.read){ e.read=true; api(`/api/entries/${encodeURIComponent(e.feed_id)}/${encodeURIComponent(e.guid)}/flags`,
- {method:'POST',body:JSON.stringify({read:true})}).then(()=>loadFeeds(true)); }
+ player.marked=false;
}
audio.play().catch(err=>toast('Playback failed: '+err.message,true));
renderEntries();
@@ -537,6 +547,7 @@ audio.addEventListener('timeupdate',()=>{
if(d) $('#seek').value=String(Math.round(audio.currentTime/d*1000));
// Persist roughly every 10s so a reload resumes where you were.
if(player.guid && audio.currentTime-player.saveAt>10){ savePos(); }
+ if(d && audio.currentTime/d >= 0.9) markPlayed();
});
function savePos(){
if(!player.guid) return;
@@ -547,7 +558,7 @@ function savePos(){
new Blob([JSON.stringify({secs:Math.floor(audio.currentTime)})],{type:'application/json'}));
}
audio.addEventListener('pause',savePos);
-audio.addEventListener('ended',()=>{savePos();$('#pplay').textContent='▶'});
+audio.addEventListener('ended',()=>{savePos();markPlayed();$('#pplay').textContent='▶'});
audio.addEventListener('play',()=>$('#pplay').textContent='❚❚');
audio.addEventListener('pause',()=>$('#pplay').textContent='▶');
window.addEventListener('beforeunload',savePos);