1- import Dexie , { Table } from 'dexie' ;
1+ import Dexie , { Table } from 'dexie' ;
22
33export interface SampleRecord {
44 id ?: number ;
@@ -79,6 +79,11 @@ export class LiPyDatabase extends Dexie {
7979 contributors : '++id,contributorId,sessionId,[contributorId+sessionId]' ,
8080 uploadQueue : 'clientSampleId,contributorId,sessionId,characterId,status,nextAttemptAt,updatedAt' ,
8181 } ) ;
82+ this . version ( 5 ) . stores ( {
83+ samples : '++id,clientSampleId,characterId,contributorId,sessionId,filename,timestamp,syncStatus,uploadedAt,[characterId+contributorId+sessionId],[contributorId+syncStatus]' ,
84+ contributors : '++id,contributorId,sessionId,[contributorId+sessionId]' ,
85+ uploadQueue : 'clientSampleId,contributorId,sessionId,characterId,status,nextAttemptAt,updatedAt' ,
86+ } ) ;
8287 }
8388}
8489
@@ -102,13 +107,11 @@ export async function saveSample(sample: Omit<SampleRecord, 'id'>) {
102107 uploadedAt : sample ?. uploadedAt || null ,
103108 } ;
104109 const id = await db . samples . add ( record ) ;
110+ // Session statistics reset per session; lifetime statistics are incremented
111+ // only after a sample has been successfully synchronized (see
112+ // datasetSyncService.markUploaded -> incrementLifetimeCount).
105113 try {
106- const devKey = `lipy_device_sample_count_${ record . contributorId } ` ;
107114 const sessKey = `lipy_session_sample_count_${ record . contributorId } _${ record . sessionId } ` ;
108- try {
109- const prevDev = Number ( localStorage . getItem ( devKey ) || 0 ) || 0 ;
110- localStorage . setItem ( devKey , String ( prevDev + 1 ) ) ;
111- } catch ( e ) { }
112115 try {
113116 const prevSess = Number ( localStorage . getItem ( sessKey ) || 0 ) || 0 ;
114117 localStorage . setItem ( sessKey , String ( prevSess + 1 ) ) ;
@@ -163,4 +166,124 @@ export async function getPendingUploadCount() {
163166 return db . uploadQueue . count ( ) ;
164167}
165168
169+ /**
170+ * Lifetime contribution count helpers.
171+ *
172+ * The lifetime count represents the total number of samples a contributor has
173+ * *synchronized* (uploaded) across all sessions. It is stored in localStorage
174+ * under `lipy_device_sample_count_{contributorId}` and is incremented only
175+ * after a successful upload (see datasetSyncService.markUploaded). This
176+ * ensures the count persists across sessions, only ever increases on sync,
177+ * and is cleared by Reset Profile or by clearing browser storage.
178+ */
179+
180+ export function getLifetimeCount ( contributorId : string ) : number {
181+ if ( ! contributorId ) return 0 ;
182+ try {
183+ const key = `lipy_device_sample_count_${ String ( contributorId ) . trim ( ) } ` ;
184+ return Number ( localStorage . getItem ( key ) || 0 ) || 0 ;
185+ } catch ( e ) {
186+ return 0 ;
187+ }
188+ }
189+
190+ export function incrementLifetimeCount ( contributorId : string ) : number {
191+ if ( ! contributorId ) return 0 ;
192+ try {
193+ const key = `lipy_device_sample_count_${ String ( contributorId ) . trim ( ) } ` ;
194+ const prev = Number ( localStorage . getItem ( key ) || 0 ) || 0 ;
195+ const next = prev + 1 ;
196+ localStorage . setItem ( key , String ( next ) ) ;
197+ return next ;
198+ } catch ( e ) {
199+ return 0 ;
200+ }
201+ }
202+
203+ export function clearLifetimeCount ( contributorId : string ) {
204+ if ( ! contributorId ) return ;
205+ try {
206+ const key = `lipy_device_sample_count_${ String ( contributorId ) . trim ( ) } ` ;
207+ localStorage . removeItem ( key ) ;
208+ } catch ( e ) { }
209+ }
210+
211+ /**
212+ * Counts ALL samples in the local IndexedDB for a given contributor (across
213+ * all sessions, regardless of sync status).
214+ */
215+ export async function getContributorSampleCount ( contributorId : string ) : Promise < number > {
216+ if ( ! contributorId ) return 0 ;
217+ try {
218+ const cid = String ( contributorId ) . trim ( ) ;
219+ return await db . samples . where ( 'contributorId' ) . equals ( cid ) . count ( ) ;
220+ } catch ( e ) {
221+ return 0 ;
222+ }
223+ }
224+
225+ /**
226+ * Counts only *uploaded* samples in the local IndexedDB for a given
227+ * contributor (across all sessions). Used to reconcile the displayed
228+ * lifetime contribution count so it reflects only successfully synchronized
229+ * samples.
230+ */
231+ export async function getUploadedSampleCount ( contributorId : string ) : Promise < number > {
232+ if ( ! contributorId ) return 0 ;
233+ try {
234+ const cid = String ( contributorId ) . trim ( ) ;
235+ return await db . samples . where ( { contributorId : cid , syncStatus : 'uploaded' } ) . count ( ) ;
236+ } catch ( e ) {
237+ return 0 ;
238+ }
239+ }
240+
241+ /**
242+ * Clears ALL local data associated with a contributor profile:
243+ * - Lifetime contribution count (localStorage)
244+ * - Session config (localStorage)
245+ * - Session sample counts (localStorage)
246+ * - Last-sample timestamps (localStorage)
247+ * - Scheduler state (localStorage)
248+ * - Sample counter keys (localStorage)
249+ * - IndexedDB samples, contributors, and uploadQueue tables
250+ *
251+ * After calling this the contributor profile is fully reset.
252+ */
253+ export async function clearContributorData ( contributorId : string ) {
254+ if ( ! contributorId ) return ;
255+ const cid = String ( contributorId ) . trim ( ) ;
256+
257+ // 1. Remove known localStorage keys for this contributor
258+ try {
259+ const keysToRemove : string [ ] = [ ] ;
260+ for ( let i = 0 ; i < localStorage . length ; i ++ ) {
261+ const k = localStorage . key ( i ) ;
262+ if ( k && (
263+ k . startsWith ( `lipy_device_sample_count_${ cid } ` ) ||
264+ k . startsWith ( `lipy_session_sample_count_${ cid } ` ) ||
265+ k . startsWith ( `lipy_sample_counter_${ cid } ` ) ||
266+ k . startsWith ( `lipy_last_sample_ts_${ cid } ` ) ||
267+ k . startsWith ( `lipy_last_export_ts_${ cid } ` ) ||
268+ k . startsWith ( `lipy_mixed_scheduler_state_v1_${ cid } ` )
269+ ) ) {
270+ keysToRemove . push ( k ) ;
271+ }
272+ }
273+ keysToRemove . forEach ( ( k ) => localStorage . removeItem ( k ) ) ;
274+ } catch ( e ) { }
275+
276+ // 2. Remove session config (global, not contributor-scoped)
277+ try { localStorage . removeItem ( 'lipy_session_config' ) ; } catch ( e ) { }
278+
279+ // 3. Clear IndexedDB tables for this contributor
280+ try {
281+ await db . transaction ( 'rw' , db . samples , db . contributors , db . uploadQueue , async ( ) => {
282+ await db . samples . where ( 'contributorId' ) . equals ( cid ) . delete ( ) ;
283+ await db . contributors . where ( 'contributorId' ) . equals ( cid ) . delete ( ) ;
284+ await db . uploadQueue . where ( 'contributorId' ) . equals ( cid ) . delete ( ) ;
285+ } ) ;
286+ } catch ( e ) { }
287+ }
288+
166289export default db ;
0 commit comments