fix: restore recording playback and default to 30-day retention

This commit is contained in:
Codex
2026-10-06 22:20:00 +08:00
parent 91915b9d90
commit 59bd83403f
17 changed files with 160 additions and 20 deletions
+22
View File
@@ -4,6 +4,15 @@ test('ranges clip to selected day and merge overlaps without filling gaps',()=>{
const items=[{start:date(-10),duration:40},{start:date(20),duration:30},{start:date(60),duration:100},{start:'invalid',duration:20},{start:date(3),duration:-4},{start:date(3),duration:Infinity}];
assert.deepEqual(Ranges.normalize(items,date(0),date(100)),[{start:+date(0),end:+date(50)},{start:+date(60),end:+date(100)}]);
});
test('microsecond recording starts round inward and never seek into the preceding gap',()=>{
const raw='2026-10-04T00:00:00.340240Z';
const ranges=Ranges.normalize([{start:raw,duration:41.1355}],date(0),date(100));
assert.equal(new Date(ranges[0].start).toISOString(),'2026-10-04T00:00:00.341Z');
assert.equal(new Date(ranges[0].end).toISOString(),'2026-10-04T00:00:41.475Z');
const exact=Ranges.normalize([{start:'2026-10-04T00:00:00.340000Z',duration:10}],date(0),date(100));
assert.equal(exact[0].start,Date.parse(raw));
assert.deepEqual(Ranges.normalize([{start:raw,duration:0.0001}],date(0),date(1)),[]);
});
test('long recording continues after each one-hour session',()=>{
const ranges=[{start:+date(0),end:+date(8000)}];assert.deepEqual(Ranges.at(ranges,date(0)),{start:+date(0),end:+date(3600)});
assert.deepEqual(Ranges.next(ranges,date(3600)),{start:+date(3600),end:+date(7200)});assert.equal(Ranges.next(ranges,date(8000)),null);
@@ -20,6 +29,19 @@ test('clip requires valid, positive, fully recorded interval up to one hour',()=
function section(file,a,b){const s=fs.readFileSync('web/'+file,'utf8'),i=s.indexOf(a),j=s.indexOf(b,i+a.length);assert.ok(i>=0&&j>i);return s.slice(i,j);}
function deferred(){let resolve;const promise=new Promise(r=>resolve=r);return{promise,resolve};}
function ui(extra={}){const elements=new Map();const $=s=>{if(!elements.has(s))elements.set(s,{replaceChildren(){},reset(){},value:'',hidden:false});return elements.get(s);};return vm.createContext({$,Number,Date,URLSearchParams,loggedIn:true,activePage:'playback',RecordingRanges:Ranges,...extra});}
test('recording health distinguishes configured from writing and never changes the player',()=>{
const c=ui({state:{cameras:[{id:'camera',recordingState:'off'}],storage:{retentionDays:30,maxGB:40,reserveGB:8}},isAdmin:()=>true});
c.$('#play-camera').value='camera';c.$('#archive-source').value='local';c.$('#recording-configure').dataset={};
c.$('#play-video').src='keep-playing';
vm.runInContext(section('recording-ui.js','function renderRecordingStatus(','function localDateTime('),c);
for(const [status,text] of [['off','未开启'],['waiting','等待文件写入'],['recording','正在录像'],['paused','磁盘保护'],['offline','等待摄像头'],['unavailable','服务未连接'],['disabled','设备连接已停用']]){
c.state.cameras[0].recordingState=status;c.renderRecordingStatus();assert.ok(c.$('#recording-health-text').textContent.includes(text));
}
assert.match(c.$('#recording-policy-note').textContent,/30 天/);assert.equal(c.$('#recording-configure').dataset.editCamera,'camera');
assert.equal(c.$('#play-video').src,'keep-playing');
c.isAdmin=()=>false;c.state.storage={};c.renderRecordingStatus();assert.equal(c.$('#recording-configure').hidden,true);assert.doesNotMatch(c.$('#recording-policy-note').textContent,/undefined|40 GB/);
c.$('#archive-source').value='recorder';c.renderRecordingStatus();assert.equal(c.$('#recording-health').hidden,true);
});
test('auto-continue preserves user choice, stops short clips and ends at last segment',()=>{
const c=ui({playbackRange:{start:0,end:10000},spans:[{start:0,end:10000},{start:20000,end:30000}],playAt:d=>c.played=+d});
vm.runInContext(section('recording-ui.js','function continuePlayback(','function makeClip('),c);