From ff661ba4a415181c25c73762b28e45713616ebf6 Mon Sep 17 00:00:00 2001 From: NiloCK Date: Mon, 7 Jul 2025 15:17:36 -0300 Subject: [PATCH 1/9] add title, logo props --- packages/standalone-ui/src/components/CourseHeader.vue | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/standalone-ui/src/components/CourseHeader.vue b/packages/standalone-ui/src/components/CourseHeader.vue index 5becf5626..237f32e0b 100644 --- a/packages/standalone-ui/src/components/CourseHeader.vue +++ b/packages/standalone-ui/src/components/CourseHeader.vue @@ -36,6 +36,12 @@ import { ref, computed } from 'vue'; import { useDisplay } from 'vuetify'; import { UserLoginAndRegistrationContainer } from '@vue-skuilder/common-ui'; +// Define props +defineProps<{ + title?: string; + logo?: string; +}>(); + const { mobile } = useDisplay(); const isMobile = computed(() => mobile.value); const drawer = ref(false); From 4e962afa5ee4a613ddb588c0ff08d54a89d3b7bf Mon Sep 17 00:00:00 2001 From: NiloCK Date: Mon, 7 Jul 2025 15:38:07 -0300 Subject: [PATCH 2/9] markRaw on dynamic component... perf, but mostly suppressing noise --- .../common-ui/src/components/cardRendering/CardLoader.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/common-ui/src/components/cardRendering/CardLoader.vue b/packages/common-ui/src/components/cardRendering/CardLoader.vue index fbd441b30..42c4b068d 100644 --- a/packages/common-ui/src/components/cardRendering/CardLoader.vue +++ b/packages/common-ui/src/components/cardRendering/CardLoader.vue @@ -13,7 +13,7 @@ diff --git a/packages/standalone-ui/src/router/index.ts b/packages/standalone-ui/src/router/index.ts index 5d21bdfee..76b516bbf 100644 --- a/packages/standalone-ui/src/router/index.ts +++ b/packages/standalone-ui/src/router/index.ts @@ -3,6 +3,8 @@ import HomeView from '../views/HomeView.vue'; import StudyView from '../views/StudyView.vue'; import ProgressView from '../views/ProgressView.vue'; import BrowseView from '../views/BrowseView.vue'; +import UserStatsView from '../views/UserStatsView.vue'; +import UserSettingsView from '../views/UserSettingsView.vue'; import { UserLogin, UserRegistration } from '@vue-skuilder/common-ui'; const routes: Array = [ @@ -37,6 +39,16 @@ const routes: Array = [ name: 'Register', component: UserRegistration, }, + { + path: '/u/:username', + name: 'UserSettings', + component: UserSettingsView, + }, + { + path: '/u/:username/stats', + name: 'UserStats', + component: UserStatsView, + }, ]; const router = createRouter({ diff --git a/packages/standalone-ui/src/views/UserSettingsView.vue b/packages/standalone-ui/src/views/UserSettingsView.vue new file mode 100644 index 000000000..7169cda87 --- /dev/null +++ b/packages/standalone-ui/src/views/UserSettingsView.vue @@ -0,0 +1,57 @@ + + + \ No newline at end of file diff --git a/packages/standalone-ui/src/views/UserStatsView.vue b/packages/standalone-ui/src/views/UserStatsView.vue new file mode 100644 index 000000000..f0d216cea --- /dev/null +++ b/packages/standalone-ui/src/views/UserStatsView.vue @@ -0,0 +1,76 @@ + + + \ No newline at end of file From 94d87ce678c16c3c2bd25952ab47fb39821bfb8e Mon Sep 17 00:00:00 2001 From: NiloCK Date: Tue, 8 Jul 2025 09:50:16 -0300 Subject: [PATCH 8/9] perf & logging cleanliness --- packages/common-ui/src/components/StudySession.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/common-ui/src/components/StudySession.vue b/packages/common-ui/src/components/StudySession.vue index 8cc694dd8..8ebec2184 100644 --- a/packages/common-ui/src/components/StudySession.vue +++ b/packages/common-ui/src/components/StudySession.vue @@ -595,7 +595,7 @@ export default defineComponent({ this.cardCount++; this.data = tmpData; - this.view = tmpView; + this.view = markRaw(tmpView); this.cardID = _cardID; this.courseID = _courseID; this.card_elo = tmpCardData.elo.global.score; From b11f589f140dff11a8adeb5af81d6f76c9233ad1 Mon Sep 17 00:00:00 2001 From: NiloCK Date: Tue, 8 Jul 2025 09:53:18 -0300 Subject: [PATCH 9/9] add read / write db mini abstractions... clarifies intent. allows SyncStrategy to configure the targets for reads and writes. --- packages/db/src/impl/common/BaseUserDB.ts | 23 +++++++++++++------ packages/db/src/impl/common/SyncStrategy.ts | 7 ++++++ .../db/src/impl/couch/CouchDBSyncStrategy.ts | 10 ++++++++ packages/db/src/impl/couch/updateQueue.ts | 20 +++++++++------- .../db/src/impl/static/NoOpSyncStrategy.ts | 5 ++++ 5 files changed, 50 insertions(+), 15 deletions(-) diff --git a/packages/db/src/impl/common/BaseUserDB.ts b/packages/db/src/impl/common/BaseUserDB.ts index 630f0da9b..133361fcf 100644 --- a/packages/db/src/impl/common/BaseUserDB.ts +++ b/packages/db/src/impl/common/BaseUserDB.ts @@ -79,11 +79,14 @@ export class BaseUser implements UserDBInterface, DocumentUpdater { return !this._username.startsWith(GuestUsername); } - private remoteDB!: PouchDB.Database; public remote(): PouchDB.Database { return this.remoteDB; } + private localDB!: PouchDB.Database; + private remoteDB!: PouchDB.Database; + private writeDB!: PouchDB.Database; // Database to use for write operations (local-first approach) + private updateQueue!: UpdateQueue; public async createAccount( @@ -597,7 +600,11 @@ Currently logged-in as ${this._username}.` private setDBandQ() { this.localDB = getLocalUserDB(this._username); this.remoteDB = this.syncStrategy.setupRemoteDB(this._username); - this.updateQueue = new UpdateQueue(this.localDB); + // writeDB follows local-first pattern: static mode writes to local, CouchDB writes to remote/local as appropriate + this.writeDB = this.syncStrategy.getWriteDB + ? this.syncStrategy.getWriteDB(this._username) + : this.localDB; + this.updateQueue = new UpdateQueue(this.localDB, this.writeDB); } private async init() { @@ -697,7 +704,9 @@ Currently logged-in as ${this._username}.` * @returns The updated state of the card's CardHistory data */ - public async putCardRecord(record: T): Promise> { + public async putCardRecord( + record: T + ): Promise & PouchDB.Core.RevisionIdMeta> { const cardHistoryID = getCardHistoryID(record.courseID, record.cardID); // stringify the current record to make it writable to couchdb record.timeStamp = moment.utc(record.timeStamp).toString() as unknown as Moment; @@ -735,8 +744,8 @@ Currently logged-in as ${this._username}.` streak: 0, bestInterval: 0, }; - void this.remoteDB.put>(initCardHistory); - return initCardHistory; + const putResult = await this.writeDB.put>(initCardHistory); + return { ...initCardHistory, _rev: putResult.rev }; } else { throw new Error(`putCardRecord failed because of: name:${reason.name} @@ -793,7 +802,7 @@ Currently logged-in as ${this._username}.` const deletePromises = duplicateDocIds.map(async (docId) => { try { const doc = await this.remoteDB.get(docId); - await this.remoteDB.remove(doc); + await this.writeDB.remove(doc); log(`Successfully removed duplicate review: ${docId}`); } catch (error) { log(`Failed to remove duplicate review ${docId}: ${error}`); @@ -891,7 +900,7 @@ Currently logged-in as ${this._username}.` if (err.status === 404) { // doc does not exist. Create it and then run this fcn again. - await this.remoteDB.put({ + await this.writeDB.put({ _id: BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS, registrations: [], }); diff --git a/packages/db/src/impl/common/SyncStrategy.ts b/packages/db/src/impl/common/SyncStrategy.ts index 50fb5a926..36c3ff99e 100644 --- a/packages/db/src/impl/common/SyncStrategy.ts +++ b/packages/db/src/impl/common/SyncStrategy.ts @@ -14,6 +14,13 @@ export interface SyncStrategy { */ setupRemoteDB(username: string): PouchDB.Database; + /** + * Get the database to use for write operations (local-first approach) + * @param username The username to get write DB for + * @returns PouchDB database instance for write operations + */ + getWriteDB?(username: string): PouchDB.Database; + /** * Start synchronization between local and remote databases * @param localDB The local PouchDB instance diff --git a/packages/db/src/impl/couch/CouchDBSyncStrategy.ts b/packages/db/src/impl/couch/CouchDBSyncStrategy.ts index 08e8d1ca5..826644284 100644 --- a/packages/db/src/impl/couch/CouchDBSyncStrategy.ts +++ b/packages/db/src/impl/couch/CouchDBSyncStrategy.ts @@ -32,6 +32,16 @@ export class CouchDBSyncStrategy implements SyncStrategy { } } + getWriteDB(username: string): PouchDB.Database { + if (username === GuestUsername || username.startsWith(GuestUsername)) { + // Guest users write to local database + return getLocalUserDB(username); + } else { + // Authenticated users write to remote (which will sync to local) + return this.getUserDB(username); + } + } + startSync(localDB: PouchDB.Database, remoteDB: PouchDB.Database): void { // Only sync if local and remote are different instances if (localDB !== remoteDB) { diff --git a/packages/db/src/impl/couch/updateQueue.ts b/packages/db/src/impl/couch/updateQueue.ts index a0ef59899..0c02862fe 100644 --- a/packages/db/src/impl/couch/updateQueue.ts +++ b/packages/db/src/impl/couch/updateQueue.ts @@ -12,7 +12,8 @@ export default class UpdateQueue extends Loggable { [index: string]: boolean; } = {}; - private db: PouchDB.Database; + private readDB: PouchDB.Database; // Database for read operations + private writeDB: PouchDB.Database; // Database for write operations (local-first) public update>( id: PouchDB.Core.DocumentId, @@ -27,21 +28,24 @@ export default class UpdateQueue extends Loggable { return this.applyUpdates(id); } - constructor(db: PouchDB.Database) { + constructor(readDB: PouchDB.Database, writeDB?: PouchDB.Database) { super(); // PouchDB.debug.enable('*'); - this.db = db; + this.readDB = readDB; + this.writeDB = writeDB || readDB; // Default to readDB if writeDB not provided logger.debug(`UpdateQ initialized...`); - void this.db.info().then((i) => { + void this.readDB.info().then((i) => { logger.debug(`db info: ${JSON.stringify(i)}`); }); } - private async applyUpdates>(id: string): Promise { + private async applyUpdates>( + id: string + ): Promise { logger.debug(`Applying updates on doc: ${id}`); if (this.inprogressUpdates[id]) { // console.log(`Updates in progress...`); - await this.db.info(); // stall for a round trip + await this.readDB.info(); // stall for a round trip // console.log(`Retrying...`); return this.applyUpdates(id); } else { @@ -49,7 +53,7 @@ export default class UpdateQueue extends Loggable { this.inprogressUpdates[id] = true; try { - let doc = await this.db.get(id); + let doc = await this.readDB.get(id); logger.debug(`Retrieved doc: ${id}`); while (this.pendingUpdates[id].length !== 0) { const update = this.pendingUpdates[id].splice(0, 1)[0]; @@ -66,7 +70,7 @@ export default class UpdateQueue extends Loggable { // console.log(`${k}: ${typeof k}`); // } // console.log(`Applied updates to doc: ${JSON.stringify(doc)}`); - await this.db.put(doc); + await this.writeDB.put(doc); logger.debug(`Put doc: ${id}`); if (this.pendingUpdates[id].length === 0) { diff --git a/packages/db/src/impl/static/NoOpSyncStrategy.ts b/packages/db/src/impl/static/NoOpSyncStrategy.ts index c6a2d3464..bf1e2b170 100644 --- a/packages/db/src/impl/static/NoOpSyncStrategy.ts +++ b/packages/db/src/impl/static/NoOpSyncStrategy.ts @@ -17,6 +17,11 @@ export class NoOpSyncStrategy implements SyncStrategy { return getLocalUserDB(username); } + getWriteDB(username: string): PouchDB.Database { + // In static mode, always write to local database + return getLocalUserDB(username); + } + startSync(_localDB: PouchDB.Database, _remoteDB: PouchDB.Database): void { // No-op - in static mode, local and remote are the same database instance // PouchDB sync with itself is harmless and efficient