A Deno-powered backend service for Plants vs. Zombies: MODDED. [Read-only GitHub mirror] docs.pvzm.net
express typescript expressjs plant deno jspvz pvzm game online backend plants-vs-zombies zombie javascript plants modded vs plantsvszombies openapi pvz noads
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

0.5.2: unify logging data in database, post features on bluesky

Clay dbb46641 101af44c

+284 -96
+1 -1
README.md
··· 1 - # PVZM Backend ![v0.5.1](https://img.shields.io/badge/version-v0.5.1-darklime) 1 + # PVZM Backend ![v0.5.2](https://img.shields.io/badge/version-v0.5.2-darklime) 2 2 3 3 > A Deno-powered backend service for [Plants vs. Zombies: MODDED](https://github.com/roblnet13/pvz). This service provides APIs for uploading, downloading, listing, favoriting, and reporting user-created _I, Zombie_ levels. 4 4
+4 -4
TODO.md
··· 7 7 - Keep `USE_PUBLIC_FOLDER` for admin UI but add conditional routing for test interface 8 8 - This would allow production deployments to disable testing while keeping admin functionality 9 9 10 - - [x] _(Removed in favor of NGINX)_ ~~**Fix SSL/HTTPS Implementation**: The current SSL implementation is incomplete and non-functional~~ 11 - - The SSL certificate and key are read but not actually used to create an HTTPS server 12 - - Need to implement proper HTTPS server with Express.js or migrate to native Deno HTTPS 13 - - Add proper SSL error handling and validation 10 + - [x] _(Removed in favor of NGINX)_ ~~**Fix SSL/HTTPS Implementation**: The previous SSL implementation was incomplete and non-functional~~ 11 + - The SSL certificate and key were read but not actually used to create an HTTPS server 12 + - Needed to implement proper HTTPS server with Express.js or migrate to native Deno HTTPS 13 + - Needed to add proper SSL error handling and validation 14 14 15 15 ## Medium Priority 16 16
+1 -1
deno.json
··· 1 1 { 2 - "version": "0.5.1", 2 + "version": "0.5.2", 3 3 "tasks": { 4 4 "dev": "deno run --watch -P=dev --env-file=.env main.ts", 5 5 "start": "deno run -P --env-file=.env main.ts",
+62 -3
modules/db.ts
··· 24 24 featured: number; 25 25 featured_at: number | null; 26 26 logging_data: string | null; 27 - admin_logging_data: string | null; 28 27 }; 29 28 30 29 function tableHasColumn(db: Database, tableName: string, columnName: string) { ··· 147 146 if (!tableHasColumn(db, "levels", "logging_data")) { 148 147 db.prepare("ALTER TABLE levels ADD COLUMN logging_data TEXT").run(); 149 148 } 150 - if (!tableHasColumn(db, "levels", "admin_logging_data")) { 151 - db.prepare("ALTER TABLE levels ADD COLUMN admin_logging_data TEXT").run(); 149 + 150 + // migrate old flat logging_data format to nested structure 151 + // old: { "discord": "msg_id" } -> new: { "discord": { "public": "msg_id" } } 152 + const levelsWithOldFormat = db 153 + .prepare("SELECT id, logging_data FROM levels WHERE logging_data IS NOT NULL") 154 + .all() as { id: number; logging_data: string }[]; 155 + 156 + let migratedCount = 0; 157 + for (const level of levelsWithOldFormat) { 158 + try { 159 + const data = JSON.parse(level.logging_data); 160 + let needsMigration = false; 161 + 162 + // check if any provider has flat string value instead of object 163 + for (const [provider, value] of Object.entries(data)) { 164 + if (typeof value === "string") { 165 + needsMigration = true; 166 + data[provider] = { public: value }; 167 + } 168 + } 169 + 170 + if (needsMigration) { 171 + db.prepare("UPDATE levels SET logging_data = ? WHERE id = ?").run(JSON.stringify(data), level.id); 172 + migratedCount++; 173 + } 174 + } catch { 175 + // skip levels with invalid JSON 176 + } 177 + } 178 + if (migratedCount > 0) { 179 + console.log(`Migrated ${migratedCount} levels from flat to nested logging_data format`); 180 + } 181 + 182 + // migrate old admin_logging_data into unified logging_data structure 183 + if (tableHasColumn(db, "levels", "admin_logging_data")) { 184 + const levelsToMigrate = db 185 + .prepare("SELECT id, logging_data, admin_logging_data FROM levels WHERE admin_logging_data IS NOT NULL") 186 + .all() as { id: number; logging_data: string | null; admin_logging_data: string | null }[]; 187 + 188 + for (const level of levelsToMigrate) { 189 + try { 190 + const existingData = level.logging_data ? JSON.parse(level.logging_data) : {}; 191 + const adminData = JSON.parse(level.admin_logging_data!); 192 + 193 + // merge admin message IDs into unified structure 194 + // old format: { "discord": "msg_id" } 195 + // new format: { "discord": { "public": "...", "admin": "msg_id" } } 196 + for (const [provider, messageId] of Object.entries(adminData)) { 197 + if (!existingData[provider]) existingData[provider] = {}; 198 + existingData[provider].admin = messageId; 199 + } 200 + 201 + db.prepare("UPDATE levels SET logging_data = ? WHERE id = ?").run(JSON.stringify(existingData), level.id); 202 + } catch { 203 + // skip levels with invalid JSON 204 + } 205 + } 206 + console.log(`Migrated ${levelsToMigrate.length} levels from admin_logging_data to unified logging_data`); 207 + 208 + // drop the old column 209 + db.prepare("ALTER TABLE levels DROP COLUMN admin_logging_data").run(); 210 + console.log("Dropped admin_logging_data column"); 152 211 } 153 212 } catch (migrationError) { 154 213 console.error("Logging data migration error:", migrationError);
+37 -3
modules/logging/bluesky.ts
··· 1 1 import { AtpAgent, RichText } from "@atproto/api"; 2 - import type { LevelInfo, LoggingProvider } from "./types.ts"; 2 + import type { FeaturedLevelInfo, LevelInfo, LoggingProvider } from "./types.ts"; 3 3 4 4 export type BlueskyProviderConfig = { 5 5 enabled?: boolean; ··· 62 62 facets: rt.facets, 63 63 }); 64 64 65 - // return the post URI as the message ID 65 + // return the post uri as the message id 66 66 return response.uri; 67 67 } catch (error) { 68 68 console.error("Bluesky logging provider: sendLevelMessage failed:", error); ··· 88 88 } 89 89 90 90 async sendReportMessage(): Promise<boolean> { 91 - // reports not supported on Bluesky 91 + // reports not supported on bluesky 92 92 return false; 93 + } 94 + 95 + async sendFeaturedMessage(level: FeaturedLevelInfo): Promise<string | null> { 96 + if (!this.agent) return null; 97 + 98 + try { 99 + const playUrl = `${level.gameUrl}/?izl_id=${level.id}`; 100 + const message = `Newly featured level!\n\n"${level.name}" by ${level.author}\n\n${playUrl}`; 101 + 102 + const rt = new RichText({ text: message }); 103 + await rt.detectFacets(this.agent); 104 + 105 + const response = await this.agent.post({ 106 + text: rt.text, 107 + facets: rt.facets, 108 + }); 109 + 110 + return response.uri; 111 + } catch (error) { 112 + console.error("Bluesky logging provider: sendFeaturedMessage failed:", error); 113 + return null; 114 + } 115 + } 116 + 117 + async deleteFeaturedMessage(messageId: string): Promise<boolean> { 118 + if (!this.agent || !messageId) return false; 119 + 120 + try { 121 + await this.agent.deletePost(messageId); 122 + return true; 123 + } catch (error) { 124 + console.error("Bluesky logging provider: deleteFeaturedMessage failed:", error); 125 + return false; 126 + } 93 127 } 94 128 }
+134 -61
modules/logging/manager.ts
··· 1 - import type { LevelInfo, AdminLevelInfo, LoggingProvider, ReportInfo, AuditLogEntry } from "./types.ts"; 1 + import type { LevelInfo, AdminLevelInfo, FeaturedLevelInfo, LoggingProvider, ReportInfo, AuditLogEntry } from "./types.ts"; 2 2 3 3 /** 4 - * stores message IDs for each provider as JSON: { "discord": "123", "slack": "456" } 4 + * unified logging data structure stored as JSON in the database. 5 + * each provider has its own object with message IDs for different contexts. 6 + * example: 7 + * { 8 + * "discord": { "public": "123", "admin": "456" }, 9 + * "bluesky": { "public": "uri", "featured": "uri" } 10 + * } 5 11 */ 6 - export type ProviderMessageIds = Record<string, string>; 12 + export type ProviderMessageData = { 13 + public?: string; 14 + admin?: string; 15 + featured?: string; 16 + }; 17 + export type LoggingData = Record<string, ProviderMessageData>; 7 18 8 19 export class LoggingManager { 9 20 private providers: LoggingProvider[] = []; ··· 40 51 } 41 52 42 53 /** 54 + * parse logging data from JSON string, returns empty object on failure 55 + */ 56 + private parseLoggingData(json: string | null): LoggingData { 57 + if (!json) return {}; 58 + try { 59 + return JSON.parse(json); 60 + } catch { 61 + return {}; 62 + } 63 + } 64 + 65 + /** 66 + * stringify logging data to JSON, returns null if empty 67 + */ 68 + private stringifyLoggingData(data: LoggingData): string | null { 69 + if (Object.keys(data).length === 0) return null; 70 + return JSON.stringify(data); 71 + } 72 + 73 + /** 43 74 * send a level message to all providers 44 - * returns a JSON string of provider -> messageId mappings 75 + * returns updated logging data JSON string 45 76 */ 46 - async sendLevelMessage(level: LevelInfo): Promise<string | null> { 47 - if (this.providers.length === 0) return null; 77 + async sendLevelMessage(level: LevelInfo, existingDataJson: string | null = null): Promise<string | null> { 78 + if (this.providers.length === 0) return existingDataJson; 48 79 49 - const messageIds: ProviderMessageIds = {}; 80 + const data = this.parseLoggingData(existingDataJson); 50 81 51 82 await Promise.allSettled( 52 83 this.providers.map(async (provider) => { 53 84 const messageId = await provider.sendLevelMessage(level); 54 85 if (messageId) { 55 - messageIds[provider.name] = messageId; 86 + if (!data[provider.name]) data[provider.name] = {}; 87 + data[provider.name].public = messageId; 56 88 } 57 89 }) 58 90 ); 59 91 60 - if (Object.keys(messageIds).length === 0) return null; 61 - return JSON.stringify(messageIds); 92 + return this.stringifyLoggingData(data); 62 93 } 63 94 64 95 /** 65 96 * send an admin level message to all providers 66 - * returns a JSON string of provider -> messageId mappings 97 + * returns updated logging data JSON string 67 98 */ 68 - async sendAdminLevelMessage(level: AdminLevelInfo): Promise<string | null> { 69 - if (this.providers.length === 0) return null; 99 + async sendAdminLevelMessage(level: AdminLevelInfo, existingDataJson: string | null = null): Promise<string | null> { 100 + if (this.providers.length === 0) return existingDataJson; 70 101 71 - const messageIds: ProviderMessageIds = {}; 102 + const data = this.parseLoggingData(existingDataJson); 72 103 73 104 await Promise.allSettled( 74 105 this.providers.map(async (provider) => { 75 106 if ("sendAdminLevelMessage" in provider) { 76 107 const messageId = await (provider as any).sendAdminLevelMessage(level); 77 108 if (messageId) { 78 - messageIds[provider.name] = messageId; 109 + if (!data[provider.name]) data[provider.name] = {}; 110 + data[provider.name].admin = messageId; 111 + } 112 + } 113 + }) 114 + ); 115 + 116 + return this.stringifyLoggingData(data); 117 + } 118 + 119 + /** 120 + * send a featured level message to all providers 121 + * returns updated logging data JSON string 122 + */ 123 + async sendFeaturedMessage(level: FeaturedLevelInfo, existingDataJson: string | null = null): Promise<string | null> { 124 + if (this.providers.length === 0) return existingDataJson; 125 + 126 + const data = this.parseLoggingData(existingDataJson); 127 + 128 + await Promise.allSettled( 129 + this.providers.map(async (provider) => { 130 + if ("sendFeaturedMessage" in provider) { 131 + const messageId = await (provider as any).sendFeaturedMessage(level); 132 + if (messageId) { 133 + if (!data[provider.name]) data[provider.name] = {}; 134 + data[provider.name].featured = messageId; 79 135 } 80 136 } 81 137 }) 82 138 ); 83 139 84 - if (Object.keys(messageIds).length === 0) return null; 85 - return JSON.stringify(messageIds); 140 + return this.stringifyLoggingData(data); 86 141 } 87 142 88 143 /** 89 144 * edit a level message across all providers 90 - * messageIdsJson is the JSON string stored in the database 91 145 */ 92 - async editLevelMessage(messageIdsJson: string | null, level: LevelInfo): Promise<void> { 93 - if (!messageIdsJson || this.providers.length === 0) return; 146 + async editLevelMessage(loggingDataJson: string | null, level: LevelInfo): Promise<void> { 147 + if (!loggingDataJson || this.providers.length === 0) return; 94 148 95 - let messageIds: ProviderMessageIds; 96 - try { 97 - messageIds = JSON.parse(messageIdsJson); 98 - } catch { 99 - return; 100 - } 149 + const data = this.parseLoggingData(loggingDataJson); 101 150 102 151 await Promise.allSettled( 103 152 this.providers.map(async (provider) => { 104 - const messageId = messageIds[provider.name]; 153 + const messageId = data[provider.name]?.public; 105 154 if (messageId) { 106 155 await provider.editLevelMessage(messageId, level); 107 156 } ··· 110 159 } 111 160 112 161 /** 113 - * edits an admin level message across all providers 114 - * messageIdsJson is the JSON string stored in the database 162 + * edit an admin level message across all providers 115 163 */ 116 - async editAdminLevelMessage(messageIdsJson: string | null, level: AdminLevelInfo): Promise<void> { 117 - if (!messageIdsJson || this.providers.length === 0) return; 164 + async editAdminLevelMessage(loggingDataJson: string | null, level: AdminLevelInfo): Promise<void> { 165 + if (!loggingDataJson || this.providers.length === 0) return; 118 166 119 - let messageIds: ProviderMessageIds; 120 - try { 121 - messageIds = JSON.parse(messageIdsJson); 122 - } catch { 123 - return; 124 - } 167 + const data = this.parseLoggingData(loggingDataJson); 125 168 126 169 await Promise.allSettled( 127 170 this.providers.map(async (provider) => { 128 - const messageId = messageIds[provider.name]; 171 + const messageId = data[provider.name]?.admin; 129 172 if (messageId && "editAdminLevelMessage" in provider) { 130 173 await (provider as any).editAdminLevelMessage(messageId, level); 131 174 } ··· 135 178 136 179 /** 137 180 * delete a level message across all providers 138 - * messageIdsJson is the JSON string stored in the database 181 + * returns updated logging data JSON string with public message IDs removed 139 182 */ 140 - async deleteLevelMessage(messageIdsJson: string | null): Promise<void> { 141 - if (!messageIdsJson || this.providers.length === 0) return; 183 + async deleteLevelMessage(loggingDataJson: string | null): Promise<string | null> { 184 + if (!loggingDataJson || this.providers.length === 0) return loggingDataJson; 142 185 143 - let messageIds: ProviderMessageIds; 144 - try { 145 - messageIds = JSON.parse(messageIdsJson); 146 - } catch { 147 - return; 148 - } 186 + const data = this.parseLoggingData(loggingDataJson); 149 187 150 188 await Promise.allSettled( 151 189 this.providers.map(async (provider) => { 152 - const messageId = messageIds[provider.name]; 190 + const messageId = data[provider.name]?.public; 153 191 if (messageId) { 154 192 await provider.deleteLevelMessage(messageId); 193 + delete data[provider.name].public; 194 + // clean up empty provider objects 195 + if (Object.keys(data[provider.name]).length === 0) { 196 + delete data[provider.name]; 197 + } 155 198 } 156 199 }) 157 200 ); 201 + 202 + return this.stringifyLoggingData(data); 158 203 } 159 204 160 205 /** 161 - * delete an admin level message across all providers 162 - * messageIdsJson is the JSON string stored in the database 206 + * Delete an admin level message across all providers 207 + * Returns updated logging data JSON string with admin message IDs removed 163 208 */ 164 - async deleteAdminLevelMessage(messageIdsJson: string | null): Promise<void> { 165 - if (!messageIdsJson || this.providers.length === 0) return; 209 + async deleteAdminLevelMessage(loggingDataJson: string | null): Promise<string | null> { 210 + if (!loggingDataJson || this.providers.length === 0) return loggingDataJson; 166 211 167 - let messageIds: ProviderMessageIds; 168 - try { 169 - messageIds = JSON.parse(messageIdsJson); 170 - } catch { 171 - return; 172 - } 212 + const data = this.parseLoggingData(loggingDataJson); 173 213 174 214 await Promise.allSettled( 175 215 this.providers.map(async (provider) => { 176 - const messageId = messageIds[provider.name]; 216 + const messageId = data[provider.name]?.admin; 177 217 if (messageId && "deleteAdminLevelMessage" in provider) { 178 218 await (provider as any).deleteAdminLevelMessage(messageId); 219 + delete data[provider.name].admin; 220 + // clean up empty provider objects 221 + if (Object.keys(data[provider.name]).length === 0) { 222 + delete data[provider.name]; 223 + } 179 224 } 180 225 }) 181 226 ); 227 + 228 + return this.stringifyLoggingData(data); 182 229 } 183 230 184 231 /** 185 - * send a report message to all providers 232 + * Delete a featured level message across all providers 233 + * Returns updated logging data JSON string with featured message IDs removed 234 + */ 235 + async deleteFeaturedMessage(loggingDataJson: string | null): Promise<string | null> { 236 + if (!loggingDataJson || this.providers.length === 0) return loggingDataJson; 237 + 238 + const data = this.parseLoggingData(loggingDataJson); 239 + 240 + await Promise.allSettled( 241 + this.providers.map(async (provider) => { 242 + const messageId = data[provider.name]?.featured; 243 + if (messageId && "deleteFeaturedMessage" in provider) { 244 + await (provider as any).deleteFeaturedMessage(messageId); 245 + delete data[provider.name].featured; 246 + // clean up empty provider objects 247 + if (Object.keys(data[provider.name]).length === 0) { 248 + delete data[provider.name]; 249 + } 250 + } 251 + }) 252 + ); 253 + 254 + return this.stringifyLoggingData(data); 255 + } 256 + 257 + /** 258 + * Send a report message to all providers 186 259 */ 187 260 async sendReportMessage(report: ReportInfo): Promise<void> { 188 261 if (this.providers.length === 0) return; ··· 191 264 } 192 265 193 266 /** 194 - * send an audit log entry to all providers 267 + * Send an audit log entry to all providers 195 268 */ 196 269 async sendAuditLog(entry: AuditLogEntry): Promise<void> { 197 270 if (this.providers.length === 0) return;
+4
modules/logging/types.ts
··· 11 11 deleteUrl: string; 12 12 }; 13 13 14 + export type FeaturedLevelInfo = LevelInfo & { 15 + featuredAt: number; 16 + }; 17 + 14 18 export type AuditLogEntry = { 15 19 action: "edit" | "delete" | "feature" | "unfeature"; 16 20 levelId: number;
+29 -11
modules/routes/admin.ts
··· 104 104 105 105 const { name, author, sun, is_water, difficulty, favorites, plays, featured, featured_at } = req.body; 106 106 107 - // Build update query dynamically to only update provided fields 107 + // build update query dynamically to only update provided fields 108 108 const updates: string[] = []; 109 109 const updateParams: any[] = []; 110 110 ··· 199 199 200 200 if (typedUpdatedLevel.logging_data) { 201 201 await deps.loggingManager.editLevelMessage(typedUpdatedLevel.logging_data, levelInfo); 202 - } 203 - if (typedUpdatedLevel.admin_logging_data) { 202 + 204 203 const adminLevelInfo = { 205 204 ...levelInfo, 206 205 editUrl: `${config.backendUrl}/admin.html?token=${encodeURIComponent( ··· 210 209 dbCtx.createOneTimeTokenForLevel(levelId) 211 210 )}&action=delete&level=${levelId}`, 212 211 }; 213 - await deps.loggingManager.editAdminLevelMessage(typedUpdatedLevel.admin_logging_data, adminLevelInfo); 212 + await deps.loggingManager.editAdminLevelMessage(typedUpdatedLevel.logging_data, adminLevelInfo); 214 213 } 215 214 216 215 await deps.loggingManager.sendAuditLog({ ··· 244 243 return res.status(400).json({ error: "Invalid level ID" }); 245 244 } 246 245 247 - const exists = dbCtx.db.prepare("SELECT 1 FROM levels WHERE id = ?").get(levelId); 246 + const exists = dbCtx.db.prepare("SELECT logging_data FROM levels WHERE id = ?").get(levelId) as { logging_data: string | null } | undefined; 248 247 if (!exists) { 249 248 return res.status(404).json({ error: "Level not found" }); 250 249 } ··· 261 260 author: updatedLevel.author, 262 261 }); 263 262 263 + // send featured message to logging providers (e.g., bluesky) 264 + const loggingData = await deps.loggingManager.sendFeaturedMessage( 265 + { 266 + id: levelId, 267 + name: updatedLevel.name, 268 + author: updatedLevel.author, 269 + gameUrl: config.gameUrl, 270 + backendUrl: config.backendUrl, 271 + featuredAt: now, 272 + }, 273 + exists.logging_data 274 + ); 275 + 276 + if (loggingData) { 277 + dbCtx.db.prepare("UPDATE levels SET logging_data = ? WHERE id = ?").run(loggingData, levelId); 278 + } 279 + 264 280 res.json({ success: true, level: updatedLevel }); 265 281 } catch (error) { 266 282 console.error("Error featuring level:", error); ··· 280 296 return res.status(400).json({ error: "Invalid level ID" }); 281 297 } 282 298 283 - const exists = dbCtx.db.prepare("SELECT 1 FROM levels WHERE id = ?").get(levelId); 284 - if (!exists) { 299 + const levelRow = dbCtx.db.prepare("SELECT logging_data FROM levels WHERE id = ?").get(levelId) as { logging_data: string | null } | undefined; 300 + if (!levelRow) { 285 301 return res.status(404).json({ error: "Level not found" }); 286 302 } 287 303 288 - dbCtx.db.prepare("UPDATE levels SET featured = 0, featured_at = NULL WHERE id = ?").run(levelId); 304 + // delete featured message from logging providers (e.g., bluesky) 305 + const loggingData = await deps.loggingManager.deleteFeaturedMessage(levelRow.logging_data); 306 + 307 + dbCtx.db.prepare("UPDATE levels SET featured = 0, featured_at = NULL, logging_data = ? WHERE id = ?").run(loggingData, levelId); 289 308 290 309 const updatedLevel = dbCtx.db.prepare("SELECT * FROM levels WHERE id = ?").get(levelId) as LevelRecord; 291 310 ··· 326 345 // delete logging messages if they exist 327 346 if (typedLevel.logging_data) { 328 347 await deps.loggingManager.deleteLevelMessage(typedLevel.logging_data); 329 - } 330 - if (typedLevel.admin_logging_data) { 331 - await deps.loggingManager.deleteAdminLevelMessage(typedLevel.admin_logging_data); 348 + await deps.loggingManager.deleteAdminLevelMessage(typedLevel.logging_data); 349 + await deps.loggingManager.deleteFeaturedMessage(typedLevel.logging_data); 332 350 } 333 351 334 352 await deps.loggingManager.sendAuditLog({
+12 -12
modules/routes/levels.ts
··· 247 247 )}&action=delete&level=${levelId}`, 248 248 }; 249 249 250 - const messageIds = await deps.loggingManager.sendLevelMessage(levelInfo); 251 - const adminMessageIds = await deps.loggingManager.sendAdminLevelMessage(adminLevelInfo); 250 + let loggingData = await deps.loggingManager.sendLevelMessage(levelInfo); 251 + loggingData = await deps.loggingManager.sendAdminLevelMessage(adminLevelInfo, loggingData); 252 252 253 - if (messageIds || adminMessageIds) { 254 - dbCtx.db.prepare("UPDATE levels SET logging_data = ?, admin_logging_data = ? WHERE id = ?").run(messageIds, adminMessageIds, levelId); 253 + if (loggingData) { 254 + dbCtx.db.prepare("UPDATE levels SET logging_data = ? WHERE id = ?").run(loggingData, levelId); 255 255 } 256 256 } 257 257 ··· 300 300 let orderClause: string; 301 301 let useDiversitySort = false; 302 302 if (sort === "featured") { 303 - // Check if database has mature engagement data (any level with 100+ plays) 303 + // check if database has mature engagement data (any level with 100+ plays) 304 304 const maxPlaysResult = dbCtx.db.prepare("SELECT MAX(plays) as max_plays FROM levels").get() as { max_plays: number } | undefined; 305 305 const maxPlays = maxPlaysResult?.max_plays ?? 0; 306 306 const hasMatureData = maxPlays >= 100; 307 307 308 308 if (hasMatureData) { 309 - // Balanced approach: recency + quality 310 - // Recency weight: 1 point per day since epoch, quality: favorites * 100 + plays 309 + // balanced approach: recency + quality 310 + // recency weight: 1 point per day since epoch, quality: favorites * 100 + plays 311 311 orderClause = `(created_at / 86400.0 + favorites * 100 + plays) DESC`; 312 312 } else { 313 - // New database: heavily favor recency with minimal quality impact 314 - // Recency weight: 1 point per day, quality: favorites * 10 + plays / 10 315 - // This makes recency ~100x more important than in the mature formula 313 + // new database: heavily favor recency with minimal quality impact 314 + // recency weight: 1 point per day, quality: favorites * 10 + plays / 10 315 + // this makes recency ~100x more important than in the mature formula 316 316 orderClause = `(created_at / 86400.0 + favorites * 10 + plays / 10.0) DESC`; 317 317 } 318 318 useDiversitySort = true; ··· 345 345 params.push(version); 346 346 } 347 347 348 - // Featured sort only shows featured levels 348 + // featured sort only shows featured levels 349 349 if (tokenLevelId === null && sort === "featured") { 350 350 filters.push("featured = ?"); 351 351 params.push(1); ··· 357 357 query += " WHERE " + filters.join(" AND "); 358 358 } 359 359 360 - // For featured sort with diversity, fetch more results to allow for re-ranking 360 + // for featured sort with diversity, fetch more results to allow for re-ranking 361 361 const shouldApplyDiversity = useDiversitySort && tokenLevelId === null; 362 362 const fetchLimit = shouldApplyDiversity ? limit * 3 : limit; 363 363 const fetchOffset = shouldApplyDiversity ? offset : offset;