mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
storing last run in database
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
`);
|
||||
}
|
||||
@@ -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 {};
|
||||
|
||||
16
package.json
16
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"
|
||||
}
|
||||
}
|
||||
|
||||
110
test/services/jobs/dashboardRouter.test.js
Normal file
110
test/services/jobs/dashboardRouter.test.js
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
146
yarn.lock
146
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"
|
||||
|
||||
Reference in New Issue
Block a user