From bc9c56a224104dc4f60de73bcba8fffc0b94b5d1 Mon Sep 17 00:00:00 2001 From: orangecoding Date: Tue, 9 Jun 2026 16:52:37 +0200 Subject: [PATCH] storing last run in database --- lib/api/routes/dashboardRouter.js | 28 +++- lib/services/jobs/jobExecutionService.js | 8 +- lib/services/storage/jobStorage.js | 21 +++ .../migrations/sql/21.add-last-run-to-jobs.js | 20 +++ lib/types/job.js | 1 + package.json | 16 +- test/services/jobs/dashboardRouter.test.js | 110 +++++++++++++ .../services/jobs/jobExecutionService.test.js | 22 ++- yarn.lock | 146 +++++++++--------- 9 files changed, 287 insertions(+), 85 deletions(-) create mode 100644 lib/services/storage/migrations/sql/21.add-last-run-to-jobs.js create mode 100644 test/services/jobs/dashboardRouter.test.js diff --git a/lib/api/routes/dashboardRouter.js b/lib/api/routes/dashboardRouter.js index a2382ed..f05335f 100644 --- a/lib/api/routes/dashboardRouter.js +++ b/lib/api/routes/dashboardRouter.js @@ -20,6 +20,28 @@ function cap(val) { return String(val).charAt(0).toUpperCase() + String(val).slice(1); } +/** + * Compute the most recent job trigger timestamp across the given jobs. + * + * Returns `null` when none of the jobs has ever been triggered. The value is + * persisted per-job via `jobs.last_run_at`, so the dashboard reflects the + * scope visible to the current user (own + shared, or all for admins) rather + * than a process-wide in-memory value. + * + * @param {Array<{lastRunAt?: number|null}>} jobs + * @returns {number|null} + */ +function computeLastRun(jobs) { + let lastRun = null; + for (const job of jobs) { + const ts = job.lastRunAt; + if (typeof ts === 'number' && (lastRun == null || ts > lastRun)) { + lastRun = ts; + } + } + return lastRun; +} + /** * @param {import('fastify').FastifyInstance} fastify */ @@ -46,11 +68,13 @@ export default async function dashboardPlugin(fastify) { } : { labels: [], values: [] }; + const lastRun = computeLastRun(jobs); + return { general: { interval: settings.interval, - lastRun: settings.lastRun || null, - nextRun: settings.lastRun == null ? 0 : settings.lastRun + settings.interval * 60000, + lastRun, + nextRun: lastRun == null ? 0 : lastRun + settings.interval * 60000, }, kpis: { totalJobs, diff --git a/lib/services/jobs/jobExecutionService.js b/lib/services/jobs/jobExecutionService.js index 5577c88..a32ca14 100644 --- a/lib/services/jobs/jobExecutionService.js +++ b/lib/services/jobs/jobExecutionService.js @@ -104,7 +104,6 @@ export function initJobExecutionService({ providers, settings, intervalMs }) { logger.debug('Working hours set. Skipping as outside of working hours.'); return; } - settings.lastRun = now; const jobs = jobStorage.getJobs().filter((job) => { if (!context) return true; // startup/cron → all if (context.isAdmin) return true; // admin → all @@ -150,6 +149,13 @@ export function initJobExecutionService({ providers, settings, intervalMs }) { } const acquired = markRunning(job.id); if (!acquired) return; + // Persist the trigger time so the dashboard "last search" KPI can be + // derived per accessible user without an in-memory cache. + try { + jobStorage.updateJobLastRunAt(job.id, Date.now()); + } catch (err) { + logger.warn('Failed to persist last_run_at for job', job.id, err); + } // notify listeners (SSE) that the job started try { bus.emit('jobs:status', { jobId: job.id, running: true }); diff --git a/lib/services/storage/jobStorage.js b/lib/services/storage/jobStorage.js index 2a34cda..1796d54 100644 --- a/lib/services/storage/jobStorage.js +++ b/lib/services/storage/jobStorage.js @@ -97,6 +97,7 @@ export const getJob = (jobId) => { j.notification_adapter AS notificationAdapter, j.spatial_filter AS spatialFilter, j.spec_filter AS specFilter, + j.last_run_at AS lastRunAt, (SELECT COUNT(1) FROM listings l WHERE l.job_id = j.id AND l.is_active = 1 AND l.manually_deleted = 0) AS numberOfFoundListings FROM jobs j WHERE j.id = @id @@ -116,6 +117,24 @@ export const getJob = (jobId) => { }; }; +/** + * Record the timestamp at which a job was last triggered. + * + * Called from the job execution service when a job starts running. The value + * is persisted so that the dashboard "last search" KPI survives restarts and + * can be computed per accessible user. + * + * @param {string} jobId - Job primary key. + * @param {number} timestamp - Epoch milliseconds. + * @returns {void} + */ +export const updateJobLastRunAt = (jobId, timestamp) => { + SqliteConnection.execute(`UPDATE jobs SET last_run_at = @timestamp WHERE id = @id`, { + id: jobId, + timestamp, + }); +}; + /** * Update job enabled status. * @param {{jobId: string, status: boolean}} params - Parameters. @@ -164,6 +183,7 @@ export const getJobs = () => { j.notification_adapter AS notificationAdapter, j.spatial_filter AS spatialFilter, j.spec_filter AS specFilter, + j.last_run_at AS lastRunAt, (SELECT COUNT(1) FROM listings l WHERE l.job_id = j.id AND l.is_active = 1 AND l.manually_deleted = 0) AS numberOfFoundListings FROM jobs j WHERE j.enabled = 1 @@ -269,6 +289,7 @@ export const queryJobs = ({ j.notification_adapter AS notificationAdapter, j.spatial_filter AS spatialFilter, j.spec_filter AS specFilter, + j.last_run_at AS lastRunAt, (SELECT COUNT(1) FROM listings l WHERE l.job_id = j.id AND l.is_active = 1 AND l.manually_deleted = 0) AS numberOfFoundListings FROM jobs j ${whereSql} diff --git a/lib/services/storage/migrations/sql/21.add-last-run-to-jobs.js b/lib/services/storage/migrations/sql/21.add-last-run-to-jobs.js new file mode 100644 index 0000000..fc4c490 --- /dev/null +++ b/lib/services/storage/migrations/sql/21.add-last-run-to-jobs.js @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2026 by Christian Kellner. + * Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause + */ + +/** + * Migration: add `last_run_at` to the `jobs` table. + * + * Stores the epoch-ms timestamp at which a job was last triggered. Used by the + * dashboard "last search" KPI so the value survives restarts and reflects the + * actual jobs the requesting user can see (own, shared, or all for admins), + * replacing the previous in-memory `settings.lastRun` value. + * + * NULL means the job has not yet been triggered since this column was added. + */ +export function up(db) { + db.exec(` + ALTER TABLE jobs ADD COLUMN last_run_at INTEGER + `); +} diff --git a/lib/types/job.js b/lib/types/job.js index a96f689..586daa4 100644 --- a/lib/types/job.js +++ b/lib/types/job.js @@ -18,6 +18,7 @@ * @property {SpatialFilter | null} [spatialFilter] Optional spatial filter configuration as GeoJSON FeatureCollection. * @property {SpecFilter | null} [specFilter] Optional listing specifications. * @property {number} [numberOfFoundListings] Count of active listings for this job. + * @property {number | null} [lastRunAt] Epoch ms at which the job was last triggered, or null if never triggered. */ export {}; diff --git a/package.json b/package.json index dc83c97..31e40f9 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fredy", - "version": "22.5.0", + "version": "22.6.0", "description": "[F]ind [R]eal [E]states [d]amn eas[y].", "scripts": { "prepare": "husky", @@ -62,9 +62,9 @@ "Firefox ESR" ], "dependencies": { - "@douyinfe/semi-icons": "^2.99.3", - "@douyinfe/semi-ui": "2.99.3", - "@douyinfe/semi-ui-19": "^2.99.3", + "@douyinfe/semi-icons": "^2.100.0", + "@douyinfe/semi-ui": "2.100.0", + "@douyinfe/semi-ui-19": "^2.100.0", "@fastify/cookie": "^11.0.2", "@fastify/helmet": "^13.0.2", "@fastify/session": "^11.1.1", @@ -95,10 +95,10 @@ "react-chartjs-2": "^5.3.1", "react-dom": "19.2.7", "react-range-slider-input": "^3.3.5", - "react-router": "7.16.0", - "react-router-dom": "7.16.0", + "react-router": "7.17.0", + "react-router-dom": "7.17.0", "resend": "^6.12.4", - "semver": "^7.8.1", + "semver": "^7.8.3", "slack": "11.0.2", "vite": "8.0.16", "x-var": "^3.0.1", @@ -120,7 +120,7 @@ "less": "4.6.4", "lint-staged": "17.0.7", "nodemon": "^3.1.14", - "prettier": "3.8.3", + "prettier": "3.8.4", "vitest": "^4.1.8" } } diff --git a/test/services/jobs/dashboardRouter.test.js b/test/services/jobs/dashboardRouter.test.js new file mode 100644 index 0000000..4d2b3b1 --- /dev/null +++ b/test/services/jobs/dashboardRouter.test.js @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2026 by Christian Kellner. + * Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause + */ + +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import path from 'node:path'; +import Fastify from 'fastify'; + +describe('api/routes/dashboardRouter.js', () => { + let app; + let state; + + async function buildApp() { + const ROOT = path.resolve('.'); + const jobStoragePath = path.join(ROOT, 'lib', 'services', 'storage', 'jobStorage.js'); + const listingsStoragePath = path.join(ROOT, 'lib', 'services', 'storage', 'listingsStorage.js'); + const settingsStoragePath = path.join(ROOT, 'lib', 'services', 'storage', 'settingsStorage.js'); + const securityPath = path.join(ROOT, 'lib', 'api', 'security.js'); + + vi.resetModules(); + vi.doMock(jobStoragePath, () => ({ + getJobs: () => state.jobs.slice(), + })); + vi.doMock(listingsStoragePath, () => ({ + getListingsKpisForJobIds: () => ({ numberOfActiveListings: 0, medianPriceOfListings: 0 }), + getProviderDistributionForJobIds: () => [], + })); + vi.doMock(settingsStoragePath, () => ({ + getSettings: async () => ({ interval: 30 }), + })); + vi.doMock(securityPath, () => ({ + isAdmin: () => state.admin, + })); + + const mod = await import(path.join(ROOT, 'lib', 'api', 'routes', 'dashboardRouter.js')); + const plugin = mod.default; + const instance = Fastify({ logger: false }); + instance.addHook('onRequest', async (request) => { + request.session = { currentUser: state.currentUser, createdAt: Date.now() }; + }); + await instance.register(plugin, { prefix: '/api/dashboard' }); + await instance.ready(); + return instance; + } + + beforeEach(() => { + state = { + currentUser: 'u1', + admin: false, + jobs: [], + }; + }); + + afterEach(async () => { + if (app) await app.close(); + app = null; + }); + + it('derives lastRun from the most recent accessible job for a regular user', async () => { + state.jobs = [ + { id: 'a', userId: 'u1', shared_with_user: [], lastRunAt: 1000 }, + { id: 'b', userId: 'u1', shared_with_user: [], lastRunAt: 5000 }, + { id: 'c', userId: 'someone-else', shared_with_user: [], lastRunAt: 9999 }, + ]; + app = await buildApp(); + + const res = await app.inject({ method: 'GET', url: '/api/dashboard/' }); + expect(res.statusCode).toBe(200); + const body = res.json(); + expect(body.general.lastRun).toBe(5000); + expect(body.general.nextRun).toBe(5000 + 30 * 60000); + }); + + it('includes shared jobs in the lastRun calculation', async () => { + state.jobs = [ + { id: 'mine', userId: 'u1', shared_with_user: [], lastRunAt: 1000 }, + { id: 'shared', userId: 'someone-else', shared_with_user: ['u1'], lastRunAt: 4000 }, + ]; + app = await buildApp(); + + const res = await app.inject({ method: 'GET', url: '/api/dashboard/' }); + expect(res.json().general.lastRun).toBe(4000); + }); + + it('admins see lastRun across all jobs', async () => { + state.admin = true; + state.jobs = [ + { id: 'a', userId: 'someone', shared_with_user: [], lastRunAt: 1000 }, + { id: 'b', userId: 'another', shared_with_user: [], lastRunAt: 7000 }, + ]; + app = await buildApp(); + + const res = await app.inject({ method: 'GET', url: '/api/dashboard/' }); + expect(res.json().general.lastRun).toBe(7000); + }); + + it('returns null lastRun and 0 nextRun when no accessible job has ever run', async () => { + state.jobs = [ + { id: 'a', userId: 'u1', shared_with_user: [], lastRunAt: null }, + { id: 'b', userId: 'someone-else', shared_with_user: [], lastRunAt: 9999 }, + ]; + app = await buildApp(); + + const res = await app.inject({ method: 'GET', url: '/api/dashboard/' }); + const body = res.json(); + expect(body.general.lastRun).toBeNull(); + expect(body.general.nextRun).toBe(0); + }); +}); diff --git a/test/services/jobs/jobExecutionService.test.js b/test/services/jobs/jobExecutionService.test.js index ed48c65..0f8b5db 100644 --- a/test/services/jobs/jobExecutionService.test.js +++ b/test/services/jobs/jobExecutionService.test.js @@ -29,6 +29,7 @@ describe('services/jobs/jobExecutionService', () => { vi.doMock(jobStoragePath, () => ({ getJob: (id) => state.jobsById[id] || null, getJobs: () => state.jobsList.slice(), + updateJobLastRunAt: (id, timestamp) => calls.lastRunUpdates.push({ id, timestamp }), })); vi.doMock(userStoragePath, () => ({ getUsers: () => state.users.slice(), @@ -65,7 +66,7 @@ describe('services/jobs/jobExecutionService', () => { beforeEach(() => { bus = new EventEmitter(); - calls = { sent: [], markRunning: [] }; + calls = { sent: [], markRunning: [], lastRunUpdates: [] }; state = { jobsById: {}, jobsList: [], @@ -119,4 +120,23 @@ describe('services/jobs/jobExecutionService', () => { await new Promise((r) => setTimeout(r, 0)); expect(new Set(calls.markRunning)).toEqual(new Set(['j1', 'j2'])); }); + + it('persists last_run_at when a job is executed', async () => { + state.jobsById['j1'] = { id: 'j1', enabled: true, userId: 'u1', provider: [] }; + state.jobsList = [state.jobsById['j1']]; + state.users = [{ id: 'u1', isAdmin: false }]; + + await initService(); + + const before = Date.now(); + bus.emit('jobs:runOne', { jobId: 'j1' }); + await new Promise((r) => setTimeout(r, 0)); + const after = Date.now(); + + expect(calls.lastRunUpdates.length).toBe(1); + const [update] = calls.lastRunUpdates; + expect(update.id).toBe('j1'); + expect(update.timestamp).toBeGreaterThanOrEqual(before); + expect(update.timestamp).toBeLessThanOrEqual(after); + }); }); diff --git a/yarn.lock b/yarn.lock index 2b30669..a3f0df3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -950,34 +950,34 @@ dependencies: tslib "^2.0.0" -"@douyinfe/semi-animation-react@2.99.3": - version "2.99.3" - resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation-react/-/semi-animation-react-2.99.3.tgz#504e45896db45761d173be8a68cb2aa43157e8cd" - integrity sha512-0iUWQRO1t838Q1VaPE7DwOnYWeAuuu98MrNnaFkbD8JncYsct2K/2A5TDfa56DwSZ5iVz53jz2En8dMi7oF8sw== +"@douyinfe/semi-animation-react@2.100.0": + version "2.100.0" + resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation-react/-/semi-animation-react-2.100.0.tgz#f53cb41a259f4dfefafd68cab76964635a215736" + integrity sha512-zp224kBejXu+28z56uxLNasaijDJN55w0Ll+/JN+NaksTeKoUteEa93hx2SZVt6GGwZAM3H3mfDwF1UcE+fvLA== dependencies: - "@douyinfe/semi-animation" "2.99.3" - "@douyinfe/semi-animation-styled" "2.99.3" + "@douyinfe/semi-animation" "2.100.0" + "@douyinfe/semi-animation-styled" "2.100.0" classnames "^2.2.6" -"@douyinfe/semi-animation-styled@2.99.3": - version "2.99.3" - resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation-styled/-/semi-animation-styled-2.99.3.tgz#d810fd4fb1e2fa6c617b479b3bcbf6ef91d1e4d8" - integrity sha512-38/ui6SoIJFWRs2jHv1IiNV2CKHaQKhYB4WftCVXCaYYQGL24+0oQ3iLo6qUeaHEWiQK3EcK2Rt7pxtJCJxVOA== +"@douyinfe/semi-animation-styled@2.100.0": + version "2.100.0" + resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation-styled/-/semi-animation-styled-2.100.0.tgz#20a3fde32b94feb4d1fdf7eba037b19ad74c99ba" + integrity sha512-UHluoWLAHPSVYK2OpdreaSHQI3bh300rrp/dP0UCjsl3FngTUHhsOHVqdWPJ3flTWnc3Mg1Flqr2gUmFjHplhw== -"@douyinfe/semi-animation@2.99.3": - version "2.99.3" - resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation/-/semi-animation-2.99.3.tgz#3544687b8bc1f287c60a0f116494ce23adb42893" - integrity sha512-Uva9MLF+EjC+m6eBYnX9PFZIQKLxD+iKV6ps/nX/P1FWy17DCDxIsga/cByF0PIsVRLzrSdkCsddj3XETcDw9A== +"@douyinfe/semi-animation@2.100.0": + version "2.100.0" + resolved "https://registry.yarnpkg.com/@douyinfe/semi-animation/-/semi-animation-2.100.0.tgz#2f96f57d5c60d732eae5bd02a90ee1e6ec4d23b4" + integrity sha512-X9AxxUrrHWhgxxLkM4oJw8ZM/VAXsu7/fkr4dyIkkZHDhQcnMfMc2YtughqaVqkaicm3SV9zRx9npjYe/S5nVw== dependencies: bezier-easing "^2.1.0" -"@douyinfe/semi-foundation@2.99.3": - version "2.99.3" - resolved "https://registry.yarnpkg.com/@douyinfe/semi-foundation/-/semi-foundation-2.99.3.tgz#ac7f9afd4d141711a5aca14b0a1b6e4ffba70417" - integrity sha512-HKzrcdNGYoEZD81CKI6fj8jU2MWNrZx8HZ0NDHym+smBxSyhpoE/b0FrVo0PmLjCzbCDnySDdJ31GsK5GScmuw== +"@douyinfe/semi-foundation@2.100.0": + version "2.100.0" + resolved "https://registry.yarnpkg.com/@douyinfe/semi-foundation/-/semi-foundation-2.100.0.tgz#e503dbc31bbc18c2f8862653bcbdc1d5a330fd65" + integrity sha512-D2pjhpqOMOpjgw4M4Hg0Pj8KSnxl/jVsfynrIji5TwW7V2bGgt/aWOnBqdTXlrTLk4CHDmfAXKyr+rxY9aihhw== dependencies: - "@douyinfe/semi-animation" "2.99.3" - "@douyinfe/semi-json-viewer-core" "2.99.3" + "@douyinfe/semi-animation" "2.100.0" + "@douyinfe/semi-json-viewer-core" "2.100.0" "@mdx-js/mdx" "^3.0.1" async-validator "^3.5.0" classnames "^2.2.6" @@ -991,44 +991,44 @@ remark-gfm "^4.0.0" scroll-into-view-if-needed "^2.2.24" -"@douyinfe/semi-icons@2.99.3", "@douyinfe/semi-icons@^2.99.3": - version "2.99.3" - resolved "https://registry.yarnpkg.com/@douyinfe/semi-icons/-/semi-icons-2.99.3.tgz#295d4fd79b2bf987bbcd34c0a4e3fb364f96e509" - integrity sha512-Pm5H3Ua/PDumUCCsnJWwN+znVoKiyFCqag6DJy9/cuF6OOdd1+QUnvi0NHNg6+0fx/LHH088UwKFoOiZRkbaSw== +"@douyinfe/semi-icons@2.100.0", "@douyinfe/semi-icons@^2.100.0": + version "2.100.0" + resolved "https://registry.yarnpkg.com/@douyinfe/semi-icons/-/semi-icons-2.100.0.tgz#b0853f230bfa993acbf90a1c2e9fcbb97321819b" + integrity sha512-S/UZAOgzhbk2Dpwn0mUz/SrjswRpSTjSupzluLO0QmM8mCVuLSetmJ0Y/HO4MGM1eY9rEUrXON/FV3+SukFzxQ== dependencies: classnames "^2.2.6" -"@douyinfe/semi-illustrations@2.99.3": - version "2.99.3" - resolved "https://registry.yarnpkg.com/@douyinfe/semi-illustrations/-/semi-illustrations-2.99.3.tgz#e97d6c30830d44b7ec299d7e05f1a6e3c4938bf7" - integrity sha512-z1rQPgWOV2xtZS8NkmL8JCK1DltQ8FGiL1qYlXbSHjEs1XkNYruq4W3dKv0IJEpTVLIlPsbDg4VmPAuuwLCCkQ== +"@douyinfe/semi-illustrations@2.100.0": + version "2.100.0" + resolved "https://registry.yarnpkg.com/@douyinfe/semi-illustrations/-/semi-illustrations-2.100.0.tgz#4ca6623eedd1944817f1b7c8eba0095a6a7d2985" + integrity sha512-SN7plpE328WGBohLHOVpYe6FwWSO6RLS7Xf6LhqEdtarwK52ircr4C/b+OyRqIwcLOzRYMgIoqcWnAQGmowcUw== -"@douyinfe/semi-json-viewer-core@2.99.3": - version "2.99.3" - resolved "https://registry.yarnpkg.com/@douyinfe/semi-json-viewer-core/-/semi-json-viewer-core-2.99.3.tgz#a8dee4ea6cbf1bcac85c723696a9430b4faf3152" - integrity sha512-KEbZEyyM2qqGv9K+Yw/ZvAn4CEgcY2lQfL6a2ASEt80FlPoDAIWA7tGjpYxxM9/NcX9omNtsM/HLgDmrCjjBXQ== +"@douyinfe/semi-json-viewer-core@2.100.0": + version "2.100.0" + resolved "https://registry.yarnpkg.com/@douyinfe/semi-json-viewer-core/-/semi-json-viewer-core-2.100.0.tgz#c0c3bf50f722aa51008a8e6acf17ac7842baceeb" + integrity sha512-iQ6rX04YBngrsMz7Eds8zBI+W0MXb0mAICvfTaiX8RpoAwau9yFwbyHiCPKOVPSzI0hS8GwdMLSIYxdCOQPNqQ== dependencies: jsonc-parser "^3.3.1" -"@douyinfe/semi-theme-default@2.99.3": - version "2.99.3" - resolved "https://registry.yarnpkg.com/@douyinfe/semi-theme-default/-/semi-theme-default-2.99.3.tgz#e5ee0e4a8eec413ea3f58ed12f93415729c47251" - integrity sha512-r0IIjrN6vQE1bqbky7FIRi4HQ03x4ykzSIRMf4Za04BFp76IFV6CclyYyUg6cLJ6GjWCnEPMFtwTLKP+b8dAYA== +"@douyinfe/semi-theme-default@2.100.0": + version "2.100.0" + resolved "https://registry.yarnpkg.com/@douyinfe/semi-theme-default/-/semi-theme-default-2.100.0.tgz#919bb12307f6b3258016cf36e320c607717eb8c2" + integrity sha512-7tJjg5NiuUYtChWr/E5rQ4Kcko3izz8rTxlNDWSS4YR3RQg3S+lQTgG5bD7LMnBqX399erf3wgE35KLwQZKWTg== -"@douyinfe/semi-ui-19@^2.99.3": - version "2.99.3" - resolved "https://registry.yarnpkg.com/@douyinfe/semi-ui-19/-/semi-ui-19-2.99.3.tgz#236a8894ea38ac3cd9d4a4d9c784dc9712b2105a" - integrity sha512-HrXK1xIXfzS7OYzkrS+3PQKlMnx6J5HEw7wfYtDvGSIN/riSbjeD8vciHeIvP1tvhEAubFY8DMFwT07ZdmqfxA== +"@douyinfe/semi-ui-19@^2.100.0": + version "2.100.0" + resolved "https://registry.yarnpkg.com/@douyinfe/semi-ui-19/-/semi-ui-19-2.100.0.tgz#bee76e0a0eec57b49b64f8dd2d73b9039b7a2c1b" + integrity sha512-eL4DTJm4CPopWgr4d278dXIa2UwNgUundRJ37ksQ7Ev1TZnWr8SxCWLcmi4exl8kymZurAWV7j2w1sv7BHqtAA== dependencies: "@dnd-kit/core" "^6.0.8" "@dnd-kit/sortable" "^7.0.2" "@dnd-kit/utilities" "^3.2.1" - "@douyinfe/semi-animation" "2.99.3" - "@douyinfe/semi-animation-react" "2.99.3" - "@douyinfe/semi-foundation" "2.99.3" - "@douyinfe/semi-icons" "2.99.3" - "@douyinfe/semi-illustrations" "2.99.3" - "@douyinfe/semi-theme-default" "2.99.3" + "@douyinfe/semi-animation" "2.100.0" + "@douyinfe/semi-animation-react" "2.100.0" + "@douyinfe/semi-foundation" "2.100.0" + "@douyinfe/semi-icons" "2.100.0" + "@douyinfe/semi-illustrations" "2.100.0" + "@douyinfe/semi-theme-default" "2.100.0" "@tiptap/core" "^3.10.7" "@tiptap/extension-document" "^3.10.7" "@tiptap/extension-hard-break" "^3.10.7" @@ -1057,20 +1057,20 @@ scroll-into-view-if-needed "^2.2.24" utility-types "^3.10.0" -"@douyinfe/semi-ui@2.99.3": - version "2.99.3" - resolved "https://registry.yarnpkg.com/@douyinfe/semi-ui/-/semi-ui-2.99.3.tgz#a183ecc4db0e96c48c714d8733a6d30e9395bc4a" - integrity sha512-6NkeijjZZWzD31omteNVLz+oZuuMKQm3nEcwLI8+44Vv+VUSJPb87WnSFSD3F6eUIt/hZp2vJbCXHWW9SbCpDw== +"@douyinfe/semi-ui@2.100.0": + version "2.100.0" + resolved "https://registry.yarnpkg.com/@douyinfe/semi-ui/-/semi-ui-2.100.0.tgz#2964299a4c4da2501c4ba1fd2699fbfd2daef106" + integrity sha512-fTaqS6B1gHLjwMKgcWTcJWdMk9gY96h94I71Y3z9ee6qIXJyjAO8XiE8G6bihEIeVO3vTKXp1DOKiGhlgMVJKQ== dependencies: "@dnd-kit/core" "^6.0.8" "@dnd-kit/sortable" "^7.0.2" "@dnd-kit/utilities" "^3.2.1" - "@douyinfe/semi-animation" "2.99.3" - "@douyinfe/semi-animation-react" "2.99.3" - "@douyinfe/semi-foundation" "2.99.3" - "@douyinfe/semi-icons" "2.99.3" - "@douyinfe/semi-illustrations" "2.99.3" - "@douyinfe/semi-theme-default" "2.99.3" + "@douyinfe/semi-animation" "2.100.0" + "@douyinfe/semi-animation-react" "2.100.0" + "@douyinfe/semi-foundation" "2.100.0" + "@douyinfe/semi-icons" "2.100.0" + "@douyinfe/semi-illustrations" "2.100.0" + "@douyinfe/semi-theme-default" "2.100.0" "@tiptap/core" "^3.10.7" "@tiptap/extension-document" "^3.10.7" "@tiptap/extension-hard-break" "^3.10.7" @@ -6180,10 +6180,10 @@ prelude-ls@^1.2.1: resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier@3.8.3: - version "3.8.3" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.3.tgz#560f2de55bf01b4c0503bc629d5df99b9a1d09b0" - integrity sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw== +prettier@3.8.4: + version "3.8.4" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.4.tgz#f334f013ac04a96676f24dabc23c1c4ae1bae411" + integrity sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q== prismjs@^1.29.0: version "1.30.0" @@ -6525,17 +6525,17 @@ react-resizable@^3.0.5: prop-types "15.x" react-draggable "^4.0.3" -react-router-dom@7.16.0: - version "7.16.0" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.16.0.tgz#284a7cd021052aa7d0a9240dca4a02eec24eceb5" - integrity sha512-kMUAbimWB5FVbF4Bce4bJsiKJWLIUHq/mEG8+CFDnCSgltptBiG5nguducmsJeGKytlCvQud9Qhzpn49iduTlA== +react-router-dom@7.17.0: + version "7.17.0" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.17.0.tgz#e77527b4b7862f7b47ff26dd5b9315fb897b82a7" + integrity sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw== dependencies: - react-router "7.16.0" + react-router "7.17.0" -react-router@7.16.0: - version "7.16.0" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.16.0.tgz#fb41536aef2ccc2c7be12ea6be819a1e56eb6343" - integrity sha512-wArC8lVyJb3+jM9OpDyW6hLCizACWkvQR/sSGqSs+o5uEXEtGlqdZ4v8hENR3Jad6i+LRkK93q/+bQAcvl6V1A== +react-router@7.17.0: + version "7.17.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.17.0.tgz#88bbe817c6e37ab36faf140623b5d4678bf81e41" + integrity sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ== dependencies: cookie "^1.0.1" set-cookie-parser "^2.6.0" @@ -6974,10 +6974,10 @@ semver@^7.6.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a" integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== -semver@^7.8.1: - version "7.8.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.1.tgz#bf4970b5e70fda0686363cc18bfe8805d5ed957e" - integrity sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg== +semver@^7.8.3: + version "7.8.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.3.tgz#c350de61c2a1ddbc96d7fae3e5b6fcf92d477fbe" + integrity sha512-wnilbGyMxzbY7dNOl7jpKbLSjcfeweJWU5j4+u5qW+6/wuGD9KzIGOyZnQVSBM9E7DtWaaH3CyHkppYrKYoxwg== send@^1.1.0: version "1.2.1"