Compare commits

...

3 Commits

Author SHA1 Message Date
orangecoding
f66ceccbb4 next release version 2026-01-29 13:01:39 +01:00
orangecoding
a3db725af6 fixing image rendering 2026-01-29 13:01:07 +01:00
orangecoding
0663bd945f smaller demo improvements 2026-01-29 09:46:23 +01:00
11 changed files with 59 additions and 108 deletions

View File

@@ -1,94 +0,0 @@
Newer release changelog see https://github.com/orangecoding/fredy/releases
---
###### [V5.5.0]
- Upgrading dependencies
- fixing provider
- allow multiple instances of 1 provider
- **BREAKING**: Minimum node version is now 16
###### [V5.4.6]
- Adding Instana node.js monitoring
-
###### [V5.4.5]
- Adding Instana node.js monitoring
###### [V5.4.4]
- Add support for Immo Südwest Presse (immo.swp.de)
- Telegram: Use job name instead of ID and link in title
- Fix race condition if user ID is in session but not in user store
- Allow visiting the original provider URL
###### [V5.4.3]
- re-writing readme
- improving docker build
- using github's actions to build docker and test automatically
###### [V5.4.2]
- Fixing prod build
###### [V5.4.1]
- Upgrading dependencies
- Provider urls are now automagically been changed to include the correct sort order for search results
```
Note: It has been an point of confusion since the very beginning of Fredy, that people simply copied the url, but
did not take care of sorting the search results by date. If this is not done, Fredy will most likely not see the latest
results, thus cannot report them. This release fixes it by adding the necessary params (or replaces them).
```
###### [V5.3.0]
- Upgrading dependencies
- It's now possible to send mails to multiple receiver using comma separation for MailJet & Sendgrid
- Fixing Immowelt scraping
###### [V5.2.0]
- Upgrading dependencies
- Adding new similarity check layer (Duplicates are being removed now)
- Adding paging for search results
###### [V5.1.0]
- Upgrading dependencies
- NodeJS 12.13 is now the minimum supported version
- Adding general settings as new configuration page to ui
- Adding new feature working hours
###### [V5.0.0]
- Upgrading dependencies
- NodeJS 12 is now the minimum supported version
###### [V4.0.0]
Bringing back Immoscout :tada:
###### [V3.0.0]
This is basically a re-write, your old config file will not be compatible anymore. Please re-created your search jobs
on the new ui and use the values from your previous config file if needed.
```
- We're getting rid of manual config changes, Fredy, now ships with a UI so that it's easy for you to create and edit jobs
```
###### [V2.0.0]
```
- Fredy can now run multiple search job on one instance
- Changed lot's of the structure of Fredy to make this happen
[BREAKING CHANGES]
- The config has been changed, the config of V1.x will not work any longer
- Sources have been renamed to provider
```

View File

@@ -8,6 +8,8 @@ import { getListingsToGeocode, updateListingGeocoordinates } from '../storage/li
import { geocodeAddress, isGeocodingPaused } from '../geocoding/geoCodingService.js'; import { geocodeAddress, isGeocodingPaused } from '../geocoding/geoCodingService.js';
import { getJobs } from '../storage/jobStorage.js'; import { getJobs } from '../storage/jobStorage.js';
import { calculateDistanceForJob } from '../geocoding/distanceService.js'; import { calculateDistanceForJob } from '../geocoding/distanceService.js';
import { getSettings } from '../storage/settingsStorage.js';
import logger from '../logger.js';
export async function runGeoCordTask() { export async function runGeoCordTask() {
const listings = getListingsToGeocode(); const listings = getListingsToGeocode();
@@ -32,6 +34,11 @@ export async function runGeoCordTask() {
} }
export async function initGeocodingCron() { export async function initGeocodingCron() {
const settings = await getSettings();
if (settings.demoMode) {
logger.info('Do not start geo service as we are in demo mode');
return;
}
// run directly on start // run directly on start
await runGeoCordTask(); await runGeoCordTask();
// then every 6 hours // then every 6 hours

View File

@@ -5,12 +5,19 @@
import cron from 'node-cron'; import cron from 'node-cron';
import runActiveChecker from '../listings/listingActiveService.js'; import runActiveChecker from '../listings/listingActiveService.js';
import logger from '../logger.js';
import { getSettings } from '../storage/settingsStorage.js';
async function runTask() { async function runTask() {
await runActiveChecker(); await runActiveChecker();
} }
export async function initActiveCheckerCron() { export async function initActiveCheckerCron() {
const settings = await getSettings();
if (settings.demoMode) {
logger.info('Do not start listing active checker as we are in demo mode');
return;
}
//run directly on start //run directly on start
await runTask(); await runTask();
// then every day at 1 am // then every day at 1 am

View File

@@ -1,6 +1,6 @@
{ {
"name": "fredy", "name": "fredy",
"version": "19.3.2", "version": "19.3.4",
"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",

View File

@@ -17,6 +17,8 @@
padding: 24px; padding: 24px;
background-color: var(--semi-color-bg-0); background-color: var(--semi-color-bg-0);
box-sizing: border-box; box-sizing: border-box;
display: flex;
flex-direction: column;
@media (max-width: 768px) { @media (max-width: 768px) {
padding: 12px; padding: 12px;

View File

@@ -8,12 +8,12 @@ import { Card } from '@douyinfe/semi-ui-19';
import './SegmentParts.less'; import './SegmentParts.less';
export const SegmentPart = ({ name, Icon = null, children, helpText = null }) => { export const SegmentPart = ({ name, Icon = null, children, helpText = null, className = '' }) => {
const { Meta } = Card; const { Meta } = Card;
return ( return (
<Card <Card
className="segmentParts" className={`segmentParts ${className}`}
title={ title={
(helpText || name) && ( (helpText || name) && (
<Meta title={name} description={helpText} avatar={Icon == null ? null : <Icon size="extra-extra-small" />} /> <Meta title={name} description={helpText} avatar={Icon == null ? null : <Icon size="extra-extra-small" />} />

View File

@@ -20,7 +20,6 @@ import {
import { useSelector, useActions } from '../../services/state/store'; import { useSelector, useActions } from '../../services/state/store';
import KpiCard from '../../components/cards/KpiCard.jsx'; import KpiCard from '../../components/cards/KpiCard.jsx';
import PieChartCard from '../../components/cards/PieChartCard.jsx'; import PieChartCard from '../../components/cards/PieChartCard.jsx';
import Headline from '../../components/headline/Headline.jsx';
import './Dashboard.less'; import './Dashboard.less';
import { SegmentPart } from '../../components/segment/SegmentPart.jsx'; import { SegmentPart } from '../../components/segment/SegmentPart.jsx';
@@ -39,8 +38,6 @@ export default function Dashboard() {
return ( return (
<div className="dashboard"> <div className="dashboard">
<Headline text="Dashboard" size={3} />
<Row gutter={[16, 16]} className="dashboard__row"> <Row gutter={[16, 16]} className="dashboard__row">
<Col span={12} xs={24} sm={24} md={24} lg={24} xl={12}> <Col span={12} xs={24} sm={24} md={24} lg={24} xl={12}>
<SegmentPart name="General" Icon={IconTerminal}> <SegmentPart name="General" Icon={IconTerminal}>
@@ -153,7 +150,12 @@ export default function Dashboard() {
</Col> </Col>
</Row> </Row>
<SegmentPart name="Provider Insights" Icon={IconStar} helpText="Percentage of found listings over all providers"> <SegmentPart
name="Provider Insights"
Icon={IconStar}
helpText="Percentage of found listings over all providers"
className="dashboard__provider-insights"
>
<PieChartCard data={pieData} /> <PieChartCard data={pieData} />
</SegmentPart> </SegmentPart>
</div> </div>

View File

@@ -1,4 +1,8 @@
.dashboard { .dashboard {
display: flex;
flex-direction: column;
flex: 1;
&__row { &__row {
margin-bottom: 24px; margin-bottom: 24px;
flex-wrap: wrap; flex-wrap: wrap;
@@ -7,4 +11,23 @@
margin-bottom: 0; // Handled by Row gutter margin-bottom: 0; // Handled by Row gutter
} }
} }
&__provider-insights {
flex: 1;
display: flex;
flex-direction: column;
margin: 0 !important;
.semi-card-body {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
max-height: 300px;
> * {
flex: 1;
}
}
}
} }

View File

@@ -324,7 +324,12 @@ export default function ListingDetail() {
<Row> <Row>
<Col span={24} lg={12}> <Col span={24} lg={12}>
<div className="listing-detail__image-container"> <div className="listing-detail__image-container">
<Image src={listing.image_url || no_image} fallback={no_image} preview={true} /> <Image
src={listing.image_url}
fallback={no_image}
style={{ width: '100%', height: '100%' }}
preview={true}
/>
</div> </div>
</Col> </Col>
<Col span={24} lg={12}> <Col span={24} lg={12}>

View File

@@ -378,7 +378,10 @@ export default function MapView() {
const popupContent = ` const popupContent = `
<div class="map-popup-content"> <div class="map-popup-content">
<img src="${listing.image_url || no_image}" alt="${listing.title}" /> <img
src="${listing.image_url}"
onerror="this.onerror=null;this.src='${no_image}'"
/>
<h4>${listing.title}</h4> <h4>${listing.title}</h4>
<div class="info"> <div class="info">
<span><strong>Price:</strong> ${listing.price ? listing.price + ' €' : 'N/A'}</span> <span><strong>Price:</strong> ${listing.price ? listing.price + ' €' : 'N/A'}</span>

View File

@@ -4,15 +4,13 @@
*/ */
import React, { useEffect, useState, useMemo } from 'react'; import React, { useEffect, useState, useMemo } from 'react';
import { Divider, Button, AutoComplete, Toast, Typography, Banner } from '@douyinfe/semi-ui-19'; import { Divider, Button, AutoComplete, Toast, Banner } from '@douyinfe/semi-ui-19';
import { IconSave, IconHome } from '@douyinfe/semi-icons'; import { IconSave, IconHome } from '@douyinfe/semi-icons';
import { useSelector, useActions } from '../../services/state/store'; import { useSelector, useActions } from '../../services/state/store';
import { xhrGet, xhrPost } from '../../services/xhr'; import { xhrGet, xhrPost } from '../../services/xhr';
import { SegmentPart } from '../../components/segment/SegmentPart'; import { SegmentPart } from '../../components/segment/SegmentPart';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
const { Title } = Typography;
const UserSettings = () => { const UserSettings = () => {
const actions = useActions(); const actions = useActions();
const homeAddress = useSelector((state) => state.userSettings.settings.home_address); const homeAddress = useSelector((state) => state.userSettings.settings.home_address);
@@ -72,8 +70,6 @@ const UserSettings = () => {
return ( return (
<div className="user-settings"> <div className="user-settings">
<Title heading={2}>User Specific Settings</Title>
<Divider />
<SegmentPart <SegmentPart
name="Distance claculation" name="Distance claculation"
Icon={IconHome} Icon={IconHome}