Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified doc/screenshot1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified doc/screenshot2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 40 additions & 0 deletions lib/api/routes/jobRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,46 @@ jobRouter.get('/', async (req, res) => {
res.send();
});

jobRouter.get('/data', async (req, res) => {
const { page, pageSize = 50, activityFilter, sortfield = null, sortdir = 'asc', freeTextFilter } = req.query || {};

// normalize booleans
const toBool = (v) => {
if (v === true || v === 'true' || v === 1 || v === '1') return true;
if (v === false || v === 'false' || v === 0 || v === '0') return false;
return null;
};
const normalizedActivity = toBool(activityFilter);

const queryResult = jobStorage.queryJobs({
page: page ? parseInt(page, 10) : 1,
pageSize: pageSize ? parseInt(pageSize, 10) : 50,
freeTextFilter: freeTextFilter || null,
activityFilter: normalizedActivity,
sortField: sortfield || null,
sortDir: sortdir === 'desc' ? 'desc' : 'asc',
userId: req.session.currentUser,
isAdmin: isAdmin(req),
});

const isUserAdmin = isAdmin(req);

// Map result to include runtime status
queryResult.result = queryResult.result.map((job) => {
return {
...job,
running: isJobRunning(job.id),
isOnlyShared:
!isUserAdmin &&
job.userId !== req.session.currentUser &&
job.shared_with_user.includes(req.session.currentUser),
};
});

res.body = queryResult;
res.send();
});

// Server-Sent Events for job status updates
jobRouter.get('/events', async (req, res) => {
const userId = req.session.currentUser;
Expand Down
12 changes: 8 additions & 4 deletions lib/api/routes/listingsRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,14 @@ listingsRouter.get('/table', async (req, res) => {
freeTextFilter,
} = req.query || {};

// normalize booleans (accept true, 'true', 1, '1')
const toBool = (v) => v === true || v === 'true' || v === 1 || v === '1';
const normalizedActivity = toBool(activityFilter) ? true : null;
const normalizedWatch = toBool(watchListFilter) ? true : null;
// normalize booleans (accept true, 'true', 1, '1' for true; false, 'false', 0, '0' for false)
const toBool = (v) => {
if (v === true || v === 'true' || v === 1 || v === '1') return true;
if (v === false || v === 'false' || v === 0 || v === '0') return false;
return null;
};
const normalizedActivity = toBool(activityFilter);
const normalizedWatch = toBool(watchListFilter);

let jobFilter = null;
let jobIdFilter = null;
Expand Down
6 changes: 5 additions & 1 deletion lib/services/extractor/puppeteerExtractor.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,11 @@ export default async function execute(url, waitForSelector, options) {
result = pageSource || (await page.content());
}
} catch (error) {
logger.warn('Error executing with puppeteer executor', error);
if (error?.message?.includes('Timeout')) {
logger.debug('Error executing with puppeteer executor', error);
} else {
logger.warn('Error executing with puppeteer executor', error);
}
result = null;
} finally {
try {
Expand Down
8 changes: 0 additions & 8 deletions lib/services/jobs/run-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,3 @@ export function markRunning(jobId) {
export function markFinished(jobId) {
running.delete(jobId);
}

/**
* Retrieve all currently running job IDs.
* @returns {string[]}
*/
export function getRunningJobIds() {
return Array.from(running);
}
106 changes: 106 additions & 0 deletions lib/services/storage/jobStorage.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,109 @@ export const getJobs = () => {
notificationAdapter: fromJson(row.notificationAdapter, []),
}));
};

/**
* Query jobs with pagination, filtering and sorting.
*
* @param {Object} params
* @param {number} [params.pageSize=50]
* @param {number} [params.page=1]
* @param {string} [params.freeTextFilter]
* @param {object} [params.activityFilter]
* @param {string|null} [params.sortField=null]
* @param {('asc'|'desc')} [params.sortDir='asc']
* @param {string} [params.userId] - Current user id used to scope jobs (ignored for admins).
* @param {boolean} [params.isAdmin=false] - When true, returns all jobs.
* @returns {{ totalNumber:number, page:number, result:Object[] }}
*/
export const queryJobs = ({
pageSize = 50,
page = 1,
activityFilter,
freeTextFilter,
sortField = null,
sortDir = 'asc',
userId = null,
isAdmin = false,
} = {}) => {
// sanitize inputs
const safePageSize = Number.isFinite(pageSize) && pageSize > 0 ? Math.min(500, Math.floor(pageSize)) : 50;
const safePage = Number.isFinite(page) && page > 0 ? Math.floor(page) : 1;
const offset = (safePage - 1) * safePageSize;

// build WHERE filter
const whereParts = [];
const params = { limit: safePageSize, offset };
params.userId = userId || '__NO_USER__';

if (!isAdmin) {
whereParts.push(
`(j.user_id = @userId OR EXISTS (SELECT 1 FROM json_each(j.shared_with_user) AS sw WHERE sw.value = @userId))`,
);
}

if (freeTextFilter && String(freeTextFilter).trim().length > 0) {
params.filter = `%${String(freeTextFilter).trim()}%`;
whereParts.push(`(j.name LIKE @filter)`);
}

if (activityFilter === true) {
whereParts.push('(j.enabled = 1)');
} else if (activityFilter === false) {
whereParts.push('(j.enabled = 0)');
}

const whereSql = whereParts.length ? `WHERE ${whereParts.join(' AND ')}` : '';

// whitelist sortable fields
const sortable = new Set(['name', 'numberOfFoundListings', 'enabled']);
const safeSortField = sortField && sortable.has(sortField) ? sortField : null;
const safeSortDir = String(sortDir).toLowerCase() === 'desc' ? 'DESC' : 'ASC';

let orderSql = 'ORDER BY j.name IS NULL, j.name ASC';
if (safeSortField) {
if (safeSortField === 'numberOfFoundListings') {
orderSql = `ORDER BY numberOfFoundListings ${safeSortDir}`;
} else {
orderSql = `ORDER BY j.${safeSortField} ${safeSortDir}`;
}
}

// count total
const countRow = SqliteConnection.query(
`SELECT COUNT(1) as cnt
FROM jobs j
${whereSql}`,
params,
);
const totalNumber = countRow?.[0]?.cnt ?? 0;

// fetch page
const rows = SqliteConnection.query(
`SELECT j.id,
j.user_id AS userId,
j.enabled,
j.name,
j.blacklist,
j.provider,
j.shared_with_user,
j.notification_adapter AS notificationAdapter,
(SELECT COUNT(1) FROM listings l WHERE l.job_id = j.id) AS numberOfFoundListings
FROM jobs j
${whereSql}
${orderSql}
LIMIT @limit OFFSET @offset`,
params,
);

const result = rows.map((row) => ({
...row,
enabled: !!row.enabled,
blacklist: fromJson(row.blacklist, []),
provider: fromJson(row.provider, []),
shared_with_user: fromJson(row.shared_with_user, []),
notificationAdapter: fromJson(row.notificationAdapter, []),
}));

return { totalNumber, page: safePage, result };
};
8 changes: 6 additions & 2 deletions lib/services/storage/listingsStorage.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,9 +277,11 @@ export const queryListings = ({
params.filter = `%${String(freeTextFilter).trim()}%`;
whereParts.push(`(title LIKE @filter OR address LIKE @filter OR provider LIKE @filter OR link LIKE @filter)`);
}
// activityFilter: when true -> only active listings (is_active = 1)
// activityFilter: when true -> only active listings (is_active = 1), false -> only inactive
if (activityFilter === true) {
whereParts.push('(is_active = 1)');
} else if (activityFilter === false) {
whereParts.push('(is_active = 0)');
}
// Prefer filtering by job id when provided (unambiguous and robust)
if (jobIdFilter && String(jobIdFilter).trim().length > 0) {
Expand All @@ -295,9 +297,11 @@ export const queryListings = ({
params.providerName = String(providerFilter).trim();
whereParts.push('(provider = @providerName)');
}
// watchListFilter: when true -> only watched listings
// watchListFilter: when true -> only watched listings, false -> only unwatched
if (watchListFilter === true) {
whereParts.push('(wl.id IS NOT NULL)');
} else if (watchListFilter === false) {
whereParts.push('(wl.id IS NULL)');
}

const whereSql = whereParts.length ? `WHERE ${whereParts.join(' AND ')}` : '';
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fredy",
"version": "16.3.0",
"version": "17.0.0",
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
"scripts": {
"prepare": "husky",
Expand All @@ -12,7 +12,7 @@
"format": "prettier --write \"**/*.js\"",
"format:check": "prettier --check \"**/*.js\"",
"test": "node --import ./test/esmock-loader.mjs ./node_modules/mocha/bin/mocha.js --timeout 60000 test/**/*.test.js",
"testGH": "node --import ./test/esmock-loader.mjs ./node_modules/mocha/bin/mocha.js --timeout 60000 --exclude test/provider/immonet.test.js --exclude test/provider/immowelt.test.js test/**/*.test.js",
"testGH": "node --import ./test/esmock-loader.mjs ./node_modules/mocha/bin/mocha.js --timeout 60000 --exclude test/provider/immonet.test.js --exclude test/provider/immobilienDe.test.js --exclude test/provider/immowelt.test.js test/**/*.test.js",
"lint": "eslint .",
"lint:fix": "yarn lint --fix",
"migratedb": "node lib/services/storage/migrations/migrate.js",
Expand Down Expand Up @@ -77,7 +77,7 @@
"node-mailjet": "6.0.11",
"p-throttle": "^8.1.0",
"package-up": "^5.0.0",
"puppeteer": "^24.33.1",
"puppeteer": "^24.34.0",
"puppeteer-extra": "^3.3.6",
"puppeteer-extra-plugin-stealth": "^2.11.2",
"query-string": "9.3.1",
Expand All @@ -99,7 +99,7 @@
"@babel/eslint-parser": "7.28.5",
"@babel/preset-env": "7.28.5",
"@babel/preset-react": "7.28.5",
"chai": "6.2.1",
"chai": "6.2.2",
"eslint": "9.39.2",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-react": "7.37.5",
Expand Down
4 changes: 2 additions & 2 deletions ui/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ export default function FredyApp() {
if (!needsLogin()) {
await actions.features.getFeatures();
await actions.provider.getProvider();
await actions.jobs.getJobs();
await actions.jobs.getSharableUserList();
await actions.jobsData.getJobs();
await actions.jobsData.getSharableUserList();
await actions.notificationAdapter.getAdapter();
await actions.generalSettings.getGeneralSettings();
await actions.versionUpdate.getVersionUpdate();
Expand Down
4 changes: 2 additions & 2 deletions ui/src/components/cards/DashboardCardColors.less
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
@color-blue-text: #60a5fa;

@color-orange-bg: rgba(250, 91, 5, 0.12);
@color-orange-border: #d33601;
@color-orange-border: #992f0c;
@color-orange-text: #FB923CFF;

@color-green-bg: rgba(38, 250, 5, 0.12);
@color-green-border: #00c316;
@color-green-border: #278832;
@color-green-text: #33f308;

@color-purple-bg: rgba(91, 3, 218, 0.38);
Expand Down
Loading
Loading