mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
Map View in Fredy :D (#253)
* init map view * switching off 3d buildings when sattelite view is on * rename menu items * upgrading dependencies, adding provider to popups * adding screenshot for map view * fixing readme * next release version
This commit is contained in:
committed by
GitHub
parent
7fd8be07a2
commit
d43c5b3f97
@@ -119,7 +119,7 @@ Should you use [Unraid](https://unraid.net/), you can now install Fredy from the
|
|||||||
|
|
||||||
## 📸 Screenshots
|
## 📸 Screenshots
|
||||||
|
|
||||||
| Fredy Main Overview | Job Configuration | Found Listings |
|
| Fredy Maps View | Dashboard | Found Listings |
|
||||||
|--------------------------------------------------|-----------------------------------------------------------------------|-----------------------------------------------------------------------------|
|
|--------------------------------------------------|-----------------------------------------------------------------------|-----------------------------------------------------------------------------|
|
||||||
|  |  |  |
|
|  |  |  |
|
||||||
|
|
||||||
|
|||||||
12
copyright.js
12
copyright.js
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -30,12 +30,16 @@ async function getAllFiles(dir = '.') {
|
|||||||
|
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
async function addCopyright(files) {
|
async function addCopyright(files) {
|
||||||
|
const oldCopyrightRegex =
|
||||||
|
/^(\/\*\n \* Copyright \(c\) \d{4} by Christian Kellner\.\n \* Licensed under Apache-2.0 with Commons Clause and Attribution\/Naming Clause\n \*\/\n\n)+/;
|
||||||
for (let file of files) {
|
for (let file of files) {
|
||||||
try {
|
try {
|
||||||
let content = await fs.readFile(file, 'utf8');
|
let content = await fs.readFile(file, 'utf8');
|
||||||
if (!content.startsWith(COPYRIGHT)) {
|
const strippedContent = content.replace(oldCopyrightRegex, '');
|
||||||
await fs.writeFile(file, COPYRIGHT + content);
|
const newContent = COPYRIGHT + strippedContent;
|
||||||
console.log(`Added copyright to ${file}`);
|
if (content !== newContent) {
|
||||||
|
await fs.writeFile(file, newContent);
|
||||||
|
console.log(`Added/Updated copyright in ${file}`);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error processing ${file}: ${err}`);
|
console.error(`Error processing ${file}: ${err}`);
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 248 KiB After Width: | Height: | Size: 3.7 MiB |
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
4
index.js
4
index.js
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ import { cleanupDemoAtMidnight } from './lib/services/crons/demoCleanup-cron.js'
|
|||||||
import { initTrackerCron } from './lib/services/crons/tracker-cron.js';
|
import { initTrackerCron } from './lib/services/crons/tracker-cron.js';
|
||||||
import logger from './lib/services/logger.js';
|
import logger from './lib/services/logger.js';
|
||||||
import { initActiveCheckerCron } from './lib/services/crons/listing-alive-cron.js';
|
import { initActiveCheckerCron } from './lib/services/crons/listing-alive-cron.js';
|
||||||
|
import { initGeocodingCron } from './lib/services/crons/geocoding-cron.js';
|
||||||
import { getSettings } from './lib/services/storage/settingsStorage.js';
|
import { getSettings } from './lib/services/storage/settingsStorage.js';
|
||||||
import SqliteConnection, { computeDbPath } from './lib/services/storage/SqliteConnection.js';
|
import SqliteConnection, { computeDbPath } from './lib/services/storage/SqliteConnection.js';
|
||||||
import { initJobExecutionService } from './lib/services/jobs/jobExecutionService.js';
|
import { initJobExecutionService } from './lib/services/jobs/jobExecutionService.js';
|
||||||
@@ -61,6 +62,7 @@ ensureDemoUserExists();
|
|||||||
await initTrackerCron();
|
await initTrackerCron();
|
||||||
//do not wait for this to finish, let it run in the background
|
//do not wait for this to finish, let it run in the background
|
||||||
initActiveCheckerCron();
|
initActiveCheckerCron();
|
||||||
|
initGeocodingCron();
|
||||||
|
|
||||||
logger.info(`Started Fredy successfully. Ui can be accessed via http://localhost:${settings.port}`);
|
logger.info(`Started Fredy successfully. Ui can be accessed via http://localhost:${settings.port}`);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -9,6 +9,7 @@ import * as notify from './notification/notify.js';
|
|||||||
import Extractor from './services/extractor/extractor.js';
|
import Extractor from './services/extractor/extractor.js';
|
||||||
import urlModifier from './services/queryStringMutator.js';
|
import urlModifier from './services/queryStringMutator.js';
|
||||||
import logger from './services/logger.js';
|
import logger from './services/logger.js';
|
||||||
|
import { geocodeAddress } from './services/geocoding/geoCodingService.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} Listing
|
* @typedef {Object} Listing
|
||||||
@@ -79,12 +80,32 @@ class FredyPipelineExecutioner {
|
|||||||
.then(this._normalize.bind(this))
|
.then(this._normalize.bind(this))
|
||||||
.then(this._filter.bind(this))
|
.then(this._filter.bind(this))
|
||||||
.then(this._findNew.bind(this))
|
.then(this._findNew.bind(this))
|
||||||
|
.then(this._geocode.bind(this))
|
||||||
.then(this._save.bind(this))
|
.then(this._save.bind(this))
|
||||||
.then(this._filterBySimilarListings.bind(this))
|
.then(this._filterBySimilarListings.bind(this))
|
||||||
.then(this._notify.bind(this))
|
.then(this._notify.bind(this))
|
||||||
.catch(this._handleError.bind(this));
|
.catch(this._handleError.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Geocode new listings.
|
||||||
|
*
|
||||||
|
* @param {Listing[]} newListings New listings to geocode.
|
||||||
|
* @returns {Promise<Listing[]>} Resolves with the listings (potentially with added coordinates).
|
||||||
|
*/
|
||||||
|
async _geocode(newListings) {
|
||||||
|
for (const listing of newListings) {
|
||||||
|
if (listing.address) {
|
||||||
|
const coords = await geocodeAddress(listing.address);
|
||||||
|
if (coords) {
|
||||||
|
listing.latitude = coords.lat;
|
||||||
|
listing.longitude = coords.lng;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newListings;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch listings from the provider, using the default Extractor flow unless
|
* Fetch listings from the provider, using the default Extractor flow unless
|
||||||
* a provider-specific getListings override is supplied.
|
* a provider-specific getListings override is supplied.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -63,6 +63,19 @@ listingsRouter.get('/table', async (req, res) => {
|
|||||||
res.send();
|
res.send();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
listingsRouter.get('/map', async (req, res) => {
|
||||||
|
const { jobId, minPrice, maxPrice } = req.query || {};
|
||||||
|
|
||||||
|
res.body = listingStorage.getListingsForMap({
|
||||||
|
jobId: nullOrEmpty(jobId) ? null : jobId,
|
||||||
|
minPrice: minPrice ? parseInt(minPrice, 10) : null,
|
||||||
|
maxPrice: maxPrice ? parseInt(maxPrice, 10) : null,
|
||||||
|
userId: req.session.currentUser,
|
||||||
|
isAdmin: isAdminFn(req),
|
||||||
|
});
|
||||||
|
res.send();
|
||||||
|
});
|
||||||
|
|
||||||
// Toggle watch state for the current user on a listing
|
// Toggle watch state for the current user on a listing
|
||||||
listingsRouter.post('/watch', async (req, res) => {
|
listingsRouter.post('/watch', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
33
lib/services/crons/geocoding-cron.js
Normal file
33
lib/services/crons/geocoding-cron.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
import cron from 'node-cron';
|
||||||
|
import { getListingsToGeocode, updateListingGeocoordinates } from '../storage/listingsStorage.js';
|
||||||
|
import { geocodeAddress, isGeocodingPaused } from '../geocoding/geoCodingService.js';
|
||||||
|
|
||||||
|
async function runTask() {
|
||||||
|
const listings = getListingsToGeocode();
|
||||||
|
if (listings.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const listing of listings) {
|
||||||
|
if (isGeocodingPaused()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const coords = await geocodeAddress(listing.address);
|
||||||
|
if (coords) {
|
||||||
|
updateListingGeocoordinates(listing.id, coords.lat, coords.lng);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initGeocodingCron() {
|
||||||
|
// run directly on start
|
||||||
|
await runTask();
|
||||||
|
// then every 6 hours
|
||||||
|
cron.schedule('0 */6 * * *', runTask);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
105
lib/services/geocoding/client/nominatimClient.js
Normal file
105
lib/services/geocoding/client/nominatimClient.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
import os from 'os';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import https from 'https';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
import pThrottle from 'p-throttle';
|
||||||
|
import logger from '../../logger.js';
|
||||||
|
|
||||||
|
const API_URL = 'https://nominatim.openstreetmap.org/search';
|
||||||
|
|
||||||
|
const agent = new https.Agent({
|
||||||
|
keepAlive: true,
|
||||||
|
keepAliveMsecs: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const throttle = pThrottle({
|
||||||
|
limit: 1,
|
||||||
|
interval: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
function computeMachineId() {
|
||||||
|
const hostname = os.hostname() || 'unknown-host';
|
||||||
|
const nets = os.networkInterfaces?.() || {};
|
||||||
|
const macs = [];
|
||||||
|
|
||||||
|
for (const ifname of Object.keys(nets)) {
|
||||||
|
for (const addr of nets[ifname] || []) {
|
||||||
|
if (!addr) continue;
|
||||||
|
if (addr.internal) continue;
|
||||||
|
if (addr.mac && addr.mac !== '00:00:00:00:00:00') macs.push(addr.mac);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macs.sort();
|
||||||
|
|
||||||
|
const raw = [hostname, os.platform(), os.arch(), ...macs].join('|');
|
||||||
|
|
||||||
|
return crypto.createHash('sha256').update(raw).digest('hex').slice(0, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nominatim requires a specific User-Agent.
|
||||||
|
* Since Fredy is self-hosted, we use a unique machine ID to make it specific.
|
||||||
|
*/
|
||||||
|
const userAgent = `Fredy-Self-Hosted (${computeMachineId()}; https://github.com/orangecoding/fredy)`;
|
||||||
|
|
||||||
|
let last429 = 0;
|
||||||
|
const PAUSE_DURATION = 3600000; // 1 hour
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Geocodes an address using Nominatim.
|
||||||
|
*
|
||||||
|
* @param {string} address - The address to geocode.
|
||||||
|
* @returns {Promise<{lat: number, lng: number}|null>} The geocoordinates or null if error. {lat: -1, lng: -1} if not found.
|
||||||
|
*/
|
||||||
|
async function doGeocode(address) {
|
||||||
|
if (Date.now() - last429 < PAUSE_DURATION) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${API_URL}?q=${encodeURIComponent(address)}&format=json&countrycodes=de`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
agent,
|
||||||
|
headers: {
|
||||||
|
'User-Agent': userAgent,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 429) {
|
||||||
|
logger.warn('Nominatim rate limit hit. Pausing for 1 hour.');
|
||||||
|
last429 = Date.now();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
logger.error(`Nominatim API error: ${response.status} ${response.statusText}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (Array.isArray(data) && data.length > 0) {
|
||||||
|
const result = data[0];
|
||||||
|
return {
|
||||||
|
lat: parseFloat(result.lat),
|
||||||
|
lng: parseFloat(result.lon),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { lat: -1, lng: -1 };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error during Nominatim geocoding:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const geocode = throttle(doGeocode);
|
||||||
|
|
||||||
|
export const isPaused = () => Date.now() - last429 < PAUSE_DURATION;
|
||||||
43
lib/services/geocoding/geoCodingService.js
Normal file
43
lib/services/geocoding/geoCodingService.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getGeocoordinatesByAddress } from '../storage/listingsStorage.js';
|
||||||
|
import { geocode as nominatimGeocode, isPaused as isNominatimPaused } from './client/nominatimClient.js';
|
||||||
|
import logger from '../logger.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Geocodes an address using Nominatim or cached results from the database.
|
||||||
|
*
|
||||||
|
* @param {string} address - The address to geocode.
|
||||||
|
* @returns {Promise<{lat: number, lng: number}|null>} The geocoordinates or null if error. {lat: -1, lng: -1} if not found.
|
||||||
|
*/
|
||||||
|
export async function geocodeAddress(address) {
|
||||||
|
if (!address) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Check if we already have this address geocoded in our database
|
||||||
|
const cachedCoordinates = getGeocoordinatesByAddress(address);
|
||||||
|
if (cachedCoordinates) {
|
||||||
|
logger.debug(`Found cached geocoordinates for address: ${address}`);
|
||||||
|
return cachedCoordinates;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. If not, use Nominatim
|
||||||
|
return await nominatimGeocode(address);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error during geocoding:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if we are currently in a rate limit pause.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isGeocodingPaused() {
|
||||||
|
return isNominatimPaused();
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -173,9 +173,9 @@ export const storeListings = (jobId, providerId, listings) => {
|
|||||||
SqliteConnection.withTransaction((db) => {
|
SqliteConnection.withTransaction((db) => {
|
||||||
const stmt = db.prepare(
|
const stmt = db.prepare(
|
||||||
`INSERT INTO listings (id, hash, provider, job_id, price, size, title, image_url, description, address,
|
`INSERT INTO listings (id, hash, provider, job_id, price, size, title, image_url, description, address,
|
||||||
link, created_at, is_active)
|
link, created_at, is_active, latitude, longitude)
|
||||||
VALUES (@id, @hash, @provider, @job_id, @price, @size, @title, @image_url, @description, @address, @link,
|
VALUES (@id, @hash, @provider, @job_id, @price, @size, @title, @image_url, @description, @address, @link,
|
||||||
@created_at, 1)
|
@created_at, 1, @latitude, @longitude)
|
||||||
ON CONFLICT(job_id, hash) DO NOTHING`,
|
ON CONFLICT(job_id, hash) DO NOTHING`,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -193,6 +193,8 @@ export const storeListings = (jobId, providerId, listings) => {
|
|||||||
address: removeParentheses(item.address),
|
address: removeParentheses(item.address),
|
||||||
link: item.link,
|
link: item.link,
|
||||||
created_at: Date.now(),
|
created_at: Date.now(),
|
||||||
|
latitude: item.latitude || null,
|
||||||
|
longitude: item.longitude || null,
|
||||||
};
|
};
|
||||||
stmt.run(params);
|
stmt.run(params);
|
||||||
}
|
}
|
||||||
@@ -392,6 +394,105 @@ export const deleteListingsById = (ids) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return all listings that are active, have an address, and do not yet have geocoordinates.
|
||||||
|
*
|
||||||
|
* @returns {Object[]} Array of listing objects {id, address}.
|
||||||
|
*/
|
||||||
|
export const getListingsToGeocode = () => {
|
||||||
|
return SqliteConnection.query(
|
||||||
|
`SELECT id, address
|
||||||
|
FROM listings
|
||||||
|
WHERE is_active = 1
|
||||||
|
AND address IS NOT NULL
|
||||||
|
AND (latitude IS NULL OR longitude IS NULL)`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the geocoordinates for a listing.
|
||||||
|
*
|
||||||
|
* @param {string} id - The listing ID.
|
||||||
|
* @param {number} latitude
|
||||||
|
* @param {number} longitude
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export const updateListingGeocoordinates = (id, latitude, longitude) => {
|
||||||
|
SqliteConnection.execute(
|
||||||
|
`UPDATE listings
|
||||||
|
SET latitude = @latitude,
|
||||||
|
longitude = @longitude
|
||||||
|
WHERE id = @id`,
|
||||||
|
{ id, latitude, longitude },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return listings with geocoordinates for the map view, with optional filtering.
|
||||||
|
*
|
||||||
|
* @param {Object} params
|
||||||
|
* @param {string} [params.jobId]
|
||||||
|
* @param {boolean} [params.activeOnly=true]
|
||||||
|
* @param {number} [params.minPrice]
|
||||||
|
* @param {number} [params.maxPrice]
|
||||||
|
* @param {string} [params.userId]
|
||||||
|
* @param {boolean} [params.isAdmin=false]
|
||||||
|
* @returns {{listings: Object[], maxPrice: number}} Object containing listings and maxPrice.
|
||||||
|
*/
|
||||||
|
export const getListingsForMap = ({ jobId, minPrice, maxPrice, userId = null, isAdmin = false } = {}) => {
|
||||||
|
const baseWhereParts = [
|
||||||
|
'l.latitude IS NOT NULL',
|
||||||
|
'l.longitude IS NOT NULL',
|
||||||
|
'l.latitude != -1',
|
||||||
|
'l.longitude != -1',
|
||||||
|
'l.is_active = 1',
|
||||||
|
];
|
||||||
|
const params = { userId: userId || '__NO_USER__' };
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
baseWhereParts.push(
|
||||||
|
`(j.user_id = @userId OR EXISTS (SELECT 1 FROM json_each(j.shared_with_user) AS sw WHERE sw.value = @userId))`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jobId) {
|
||||||
|
params.jobId = jobId;
|
||||||
|
baseWhereParts.push('l.job_id = @jobId');
|
||||||
|
}
|
||||||
|
|
||||||
|
const wherePartsForListings = [...baseWhereParts];
|
||||||
|
if (minPrice !== undefined && minPrice !== null) {
|
||||||
|
params.minPrice = minPrice;
|
||||||
|
wherePartsForListings.push('l.price >= @minPrice');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxPrice !== undefined && maxPrice !== null) {
|
||||||
|
params.maxPrice = maxPrice;
|
||||||
|
wherePartsForListings.push('l.price <= @maxPrice');
|
||||||
|
}
|
||||||
|
|
||||||
|
const listings = SqliteConnection.query(
|
||||||
|
`SELECT l.*, j.name AS job_name
|
||||||
|
FROM listings l
|
||||||
|
LEFT JOIN jobs j ON j.id = l.job_id
|
||||||
|
WHERE ${wherePartsForListings.join(' AND ')}`,
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
|
||||||
|
const maxPriceRow = SqliteConnection.query(
|
||||||
|
`SELECT MAX(l.price) AS maxPrice
|
||||||
|
FROM listings l
|
||||||
|
LEFT JOIN jobs j ON j.id = l.job_id
|
||||||
|
WHERE ${baseWhereParts.join(' AND ')}`,
|
||||||
|
params,
|
||||||
|
)[0];
|
||||||
|
|
||||||
|
return {
|
||||||
|
listings,
|
||||||
|
maxPrice: maxPriceRow?.maxPrice || 0,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return all listings with only the fields: title, address, and price.
|
* Return all listings with only the fields: title, address, and price.
|
||||||
* This is the single helper requested for simple consumers.
|
* This is the single helper requested for simple consumers.
|
||||||
@@ -401,3 +502,24 @@ export const deleteListingsById = (ids) => {
|
|||||||
export const getAllEntriesFromListings = () => {
|
export const getAllEntriesFromListings = () => {
|
||||||
return SqliteConnection.query(`SELECT title, address, price FROM listings`);
|
return SqliteConnection.query(`SELECT title, address, price FROM listings`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return geocoordinates for a given address if it has been geocoded before.
|
||||||
|
*
|
||||||
|
* @param {string} address
|
||||||
|
* @returns {{lat: number, lng: number}|null}
|
||||||
|
*/
|
||||||
|
export const getGeocoordinatesByAddress = (address) => {
|
||||||
|
const row = SqliteConnection.query(
|
||||||
|
`SELECT latitude, longitude
|
||||||
|
FROM listings
|
||||||
|
WHERE address = @address
|
||||||
|
AND latitude IS NOT NULL
|
||||||
|
AND longitude IS NOT NULL
|
||||||
|
AND latitude != -1
|
||||||
|
AND longitude != -1
|
||||||
|
LIMIT 1`,
|
||||||
|
{ address },
|
||||||
|
)[0];
|
||||||
|
return row ? { lat: row.latitude, lng: row.longitude } : null;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Migration: Add geocoordinates to listings for map display
|
||||||
|
|
||||||
|
export function up(db) {
|
||||||
|
db.exec(`
|
||||||
|
ALTER TABLE listings ADD COLUMN latitude REAL;
|
||||||
|
ALTER TABLE listings ADD COLUMN longitude REAL;
|
||||||
|
`);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ function isOneOf(word, arr) {
|
|||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
function nullOrEmpty(val) {
|
function nullOrEmpty(val) {
|
||||||
return val == null || val.length === 0;
|
return val == null || val.length === 0 || val === 'null' || val === 'undefined';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
13934
package-lock.json
generated
Normal file
13934
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "fredy",
|
"name": "fredy",
|
||||||
"version": "17.1.0",
|
"version": "18.0.0",
|
||||||
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
|
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
@@ -59,25 +59,26 @@
|
|||||||
"Firefox ESR"
|
"Firefox ESR"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.5.16",
|
"@douyinfe/semi-icons": "^2.90.11",
|
||||||
"@douyinfe/semi-icons": "^2.90.10",
|
"@douyinfe/semi-ui": "2.90.11",
|
||||||
"@douyinfe/semi-ui": "2.90.10",
|
|
||||||
"@sendgrid/mail": "8.1.6",
|
"@sendgrid/mail": "8.1.6",
|
||||||
"@vitejs/plugin-react": "5.1.2",
|
"@vitejs/plugin-react": "5.1.2",
|
||||||
"better-sqlite3": "^12.5.0",
|
"adm-zip": "^0.5.16",
|
||||||
|
"better-sqlite3": "^12.6.0",
|
||||||
"body-parser": "2.2.2",
|
"body-parser": "2.2.2",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"cheerio": "^1.1.2",
|
"cheerio": "^1.1.2",
|
||||||
"cookie-session": "2.1.1",
|
"cookie-session": "2.1.1",
|
||||||
"handlebars": "4.7.8",
|
"handlebars": "4.7.8",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
|
"maplibre-gl": "^5.16.0",
|
||||||
"nanoid": "5.1.6",
|
"nanoid": "5.1.6",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"node-fetch": "3.3.2",
|
"node-fetch": "3.3.2",
|
||||||
"node-mailjet": "6.0.11",
|
"node-mailjet": "6.0.11",
|
||||||
"p-throttle": "^8.1.0",
|
"p-throttle": "^8.1.0",
|
||||||
"package-up": "^5.0.0",
|
"package-up": "^5.0.0",
|
||||||
"puppeteer": "^24.34.0",
|
"puppeteer": "^24.35.0",
|
||||||
"puppeteer-extra": "^3.3.6",
|
"puppeteer-extra": "^3.3.6",
|
||||||
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
||||||
"query-string": "9.3.1",
|
"query-string": "9.3.1",
|
||||||
@@ -92,7 +93,7 @@
|
|||||||
"slack": "11.0.2",
|
"slack": "11.0.2",
|
||||||
"vite": "7.3.1",
|
"vite": "7.3.1",
|
||||||
"x-var": "^3.0.1",
|
"x-var": "^3.0.1",
|
||||||
"zustand": "^5.0.9"
|
"zustand": "^5.0.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.28.5",
|
"@babel/core": "7.28.5",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 by Christian Kellner.
|
* Copyright (c) 2026 by Christian Kellner.
|
||||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user