mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
sending tracking information (#116)
* Ability to send tracking information
This commit is contained in:
committed by
GitHub
parent
3d59c0096d
commit
8f91267b5d
@@ -1,2 +0,0 @@
|
||||
sudo: false
|
||||
language: node_js
|
||||
@@ -95,6 +95,13 @@ Thanks to all the people who already contributed!
|
||||
|
||||
See [Contributing](https://github.com/orangecoding/fredy/blob/master/CONTRIBUTING.md)
|
||||
|
||||
# Analytics
|
||||
Fredy is completely free (and will always remain free). However, it would be a huge help if you’d allow me to collect some analytical data.
|
||||
Before you freak out, let me explain...
|
||||
If you agree, Fredy will send a ping to my Mixpanel project each time it runs.
|
||||
The data includes: names of active adapters/providers, OS, architecture, Node version, and language. The information is entirely anonymous and helps me understand which adapters/providers are most frequently used.</p>
|
||||
**Thanks**🤘
|
||||
|
||||
# Docker
|
||||
Use the Dockerfile in this repository to build an image.
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"interval":"60","port":9998,"scrapingAnt":{"apiKey":"","proxy":"datacenter"},"workingHours":{"from":"","to":""}}
|
||||
{"interval":"60","port":9998,"scrapingAnt":{"apiKey":"","proxy":"datacenter"},"workingHours":{"from":"","to":""},"demoMode":false,"analyticsEnabled":null}
|
||||
2
index.js
2
index.js
@@ -6,6 +6,7 @@ import * as jobStorage from './lib/services/storage/jobStorage.js';
|
||||
import FredyRuntime from './lib/FredyRuntime.js';
|
||||
import { duringWorkingHoursOrNotSet } from './lib/utils.js';
|
||||
import './lib/api/api.js';
|
||||
import {track} from './lib/services/tracking/Tracker.js';
|
||||
//if db folder does not exist, ensure to create it before loading anything else
|
||||
if (!fs.existsSync('./db')) {
|
||||
fs.mkdirSync('./db');
|
||||
@@ -25,6 +26,7 @@ setInterval(
|
||||
(function exec() {
|
||||
const isDuringWorkingHoursOrNotSet = duringWorkingHoursOrNotSet(config, Date.now());
|
||||
if (isDuringWorkingHoursOrNotSet) {
|
||||
track();
|
||||
config.lastRun = Date.now();
|
||||
jobStorage
|
||||
.getJobs()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import restana from 'restana';
|
||||
import { config, getDirName } from '../../utils.js';
|
||||
import {config, getDirName, readConfigFromStorage, refreshConfig} from '../../utils.js';
|
||||
import fs from 'fs';
|
||||
const service = restana();
|
||||
const generalSettingsRouter = service.newRouter();
|
||||
@@ -10,7 +10,9 @@ generalSettingsRouter.get('/', async (req, res) => {
|
||||
generalSettingsRouter.post('/', async (req, res) => {
|
||||
const settings = req.body;
|
||||
try {
|
||||
fs.writeFileSync(`${getDirName()}/../conf/config.json`, JSON.stringify(settings));
|
||||
const currentConfig = await readConfigFromStorage();
|
||||
fs.writeFileSync(`${getDirName()}/../conf/config.json`, JSON.stringify({...currentConfig, ...settings}));
|
||||
await refreshConfig();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.send(new Error('Error while trying to write settings.'));
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as userStorage from '../../services/storage/userStorage.js';
|
||||
import * as immoscoutProvider from '../../provider/immoscout.js';
|
||||
import { config } from '../../utils.js';
|
||||
import { isAdmin } from '../security.js';
|
||||
import {isScrapingAntApiKeySet} from '../../services/scrapingAnt.js';
|
||||
const service = restana();
|
||||
const jobRouter = service.newRouter();
|
||||
function doesJobBelongsToUser(job, req) {
|
||||
@@ -25,8 +26,8 @@ jobRouter.get('/', async (req, res) => {
|
||||
res.send();
|
||||
});
|
||||
jobRouter.get('/processingTimes', async (req, res) => {
|
||||
let scrapingAntData = null;
|
||||
if (config.scrapingAnt.apiKey != null && config.scrapingAnt.apiKey.length > 0) {
|
||||
let scrapingAntData = {};
|
||||
if (isScrapingAntApiKeySet()) {
|
||||
try {
|
||||
const response = await fetch(`https://api.scrapingant.com/v2/usage?x-api-key=${config.scrapingAnt.apiKey}`);
|
||||
scrapingAntData = await response.json();
|
||||
@@ -38,6 +39,7 @@ jobRouter.get('/processingTimes', async (req, res) => {
|
||||
interval: config.interval,
|
||||
lastRun: config.lastRun || null,
|
||||
scrapingAntData,
|
||||
error: scrapingAntData?.detail == null ? null : scrapingAntData?.detail
|
||||
};
|
||||
res.send();
|
||||
});
|
||||
|
||||
8
lib/defaultConfig.js
Normal file
8
lib/defaultConfig.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export const DEFAULT_CONFIG = {
|
||||
'interval': '60',
|
||||
'port': 9998,
|
||||
'scrapingAnt': {'apiKey': '', 'proxy': 'datacenter'},
|
||||
'workingHours': {'from': '', 'to': ''},
|
||||
'demoMode': false,
|
||||
'analyticsEnabled': null
|
||||
};
|
||||
@@ -22,7 +22,7 @@ export const transformUrlForScrapingAnt = (url, id) => {
|
||||
return url;
|
||||
};
|
||||
export const isScrapingAntApiKeySet = () => {
|
||||
return config.scrapingAnt != null && config.scrapingAnt.apiKey != null && config.scrapingAnt.apiKey.length > 0;
|
||||
return config.scrapingAnt != null && config.scrapingAnt.apiKey != null && config.scrapingAnt.apiKey.length > 8;
|
||||
};
|
||||
export const makeUrlResidential = (url) => {
|
||||
return url.replace('datacenter', 'residential');
|
||||
|
||||
42
lib/services/tracking/Tracker.js
Normal file
42
lib/services/tracking/Tracker.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import Mixpanel from 'mixpanel';
|
||||
import {getJobs} from '../storage/jobStorage.js';
|
||||
|
||||
import {config} from '../../utils.js';
|
||||
|
||||
export const track = function () {
|
||||
//only send tracking information if the user allowed to do so.
|
||||
if (config.analyticsEnabled) {
|
||||
|
||||
const mixpanelTracker = Mixpanel.init('718670ef1c58c0208256c1e408a3d75e');
|
||||
|
||||
const activeProvider = new Set();
|
||||
const activeAdapter = new Set();
|
||||
const platform = process.platform;
|
||||
const arch = process.arch;
|
||||
const language = process.env.LANG || 'en';
|
||||
const nodeVersion = process.version || 'N/A';
|
||||
|
||||
const jobs = getJobs();
|
||||
|
||||
if (jobs != null && jobs.length > 0) {
|
||||
jobs.forEach(job => {
|
||||
job.provider.forEach(provider => {
|
||||
activeProvider.add(provider.id);
|
||||
});
|
||||
job.notificationAdapter.forEach(adapter => {
|
||||
activeAdapter.add(adapter.id);
|
||||
});
|
||||
});
|
||||
|
||||
mixpanelTracker.track('fredy_tracking', {
|
||||
adapter: Array.from(activeAdapter),
|
||||
provider: Array.from(activeProvider),
|
||||
isDemo: config.demoMode,
|
||||
platform,
|
||||
arch,
|
||||
nodeVersion,
|
||||
language
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
19
lib/utils.js
19
lib/utils.js
@@ -2,6 +2,7 @@ import {dirname} from 'node:path';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import {readFile} from 'fs/promises';
|
||||
import {createHash} from 'crypto';
|
||||
import {DEFAULT_CONFIG} from './defaultConfig.js';
|
||||
|
||||
function isOneOf(word, arr) {
|
||||
if (arr == null || arr.length === 0) {
|
||||
@@ -52,7 +53,23 @@ function buildHash(...inputs) {
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
const config = JSON.parse(await readFile(new URL('../conf/config.json', import.meta.url)));
|
||||
let config = {};
|
||||
export async function readConfigFromStorage(){
|
||||
return JSON.parse(await readFile(new URL('../conf/config.json', import.meta.url)));
|
||||
}
|
||||
|
||||
export async function refreshConfig(){
|
||||
try {
|
||||
config = await readConfigFromStorage();
|
||||
//backwards compatability...
|
||||
config.analyticsEnabled ??= null;
|
||||
config.demoMode ??= false;
|
||||
} catch (error) {
|
||||
config = {...DEFAULT_CONFIG};
|
||||
console.error('Error reading config file', error);
|
||||
}
|
||||
}
|
||||
await refreshConfig();
|
||||
|
||||
export {isOneOf};
|
||||
export {nullOrEmpty};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fredy",
|
||||
"version": "10.2.0",
|
||||
"version": "10.3.0",
|
||||
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
@@ -64,6 +64,7 @@
|
||||
"lodash": "4.17.21",
|
||||
"lowdb": "6.0.1",
|
||||
"markdown": "^0.5.0",
|
||||
"mixpanel": "^0.18.0",
|
||||
"nanoid": "5.0.8",
|
||||
"node-fetch": "3.3.2",
|
||||
"node-mailjet": "6.0.6",
|
||||
|
||||
@@ -17,11 +17,13 @@ import Jobs from './views/jobs/Jobs';
|
||||
import { Route } from 'react-router';
|
||||
|
||||
import './App.less';
|
||||
import TrackingModal from './components/tracking/TrackingModal.jsx';
|
||||
|
||||
export default function FredyApp() {
|
||||
const dispatch = useDispatch();
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const currentUser = useSelector((state) => state.user.currentUser);
|
||||
const settings = useSelector((state) => state.generalSettings.settings);
|
||||
|
||||
useEffect(() => {
|
||||
async function init() {
|
||||
@@ -31,6 +33,7 @@ export default function FredyApp() {
|
||||
await dispatch.jobs.getJobs();
|
||||
await dispatch.jobs.getProcessingTimes();
|
||||
await dispatch.notificationAdapter.getAdapter();
|
||||
await dispatch.generalSettings.getGeneralSettings();
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -59,6 +62,7 @@ export default function FredyApp() {
|
||||
<Logout />
|
||||
<Logo width={190} white />
|
||||
<Menu isAdmin={isAdmin()} />
|
||||
{settings.analyticsEnabled === null && <TrackingModal/>}
|
||||
<Switch>
|
||||
<Route name="Insufficient Permission" path={'/403'} component={InsufficientPermission} />
|
||||
<Route name="Create new Job" path={'/jobs/new'} component={JobMutation} />
|
||||
|
||||
48
ui/src/components/tracking/TrackingModal.jsx
Normal file
48
ui/src/components/tracking/TrackingModal.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import {Modal} from '@douyinfe/semi-ui';
|
||||
import Logo from '../logo/Logo.jsx';
|
||||
import {xhrPost} from '../../services/xhr.js';
|
||||
|
||||
import './TrackingModal.less';
|
||||
|
||||
const saveResponse = async (analyticsEnabled) => {
|
||||
await xhrPost('/api/admin/generalSettings', {
|
||||
analyticsEnabled
|
||||
});
|
||||
};
|
||||
|
||||
export default function TrackingModal() {
|
||||
|
||||
return <Modal
|
||||
visible={true}
|
||||
onOk={async () => {
|
||||
await saveResponse(true);
|
||||
location.reload();
|
||||
}}
|
||||
onCancel={async () => {
|
||||
await saveResponse(false);
|
||||
location.reload();
|
||||
}}
|
||||
maskClosable={false}
|
||||
closable={false}
|
||||
okText="Yes! I want to help"
|
||||
cancelText="No, thanks"
|
||||
>
|
||||
<Logo white/>
|
||||
<div className="trackingModal__description">
|
||||
<p>Hey 👋</p>
|
||||
<p>Fed up with popups? Yeah, me too. But this one’s important, and I promise it will only appear once ;)</p>
|
||||
<p>Fredy is completely free (and will always remain free). If you’d like, you can support me by donating
|
||||
through my GitHub, but there’s absolutely no obligation to do so.</p>
|
||||
<p>However, it would be a huge
|
||||
help if you’d allow me to collect some analytical data. Wait, before you click "no", let me explain. If
|
||||
you
|
||||
agree, Fredy will send a ping to my Mixpanel project each time it runs.</p>
|
||||
<p>The data includes: names of
|
||||
active adapters/providers, OS, architecture, Node version, and language. The information is entirely
|
||||
anonymous and helps me understand which adapters/providers are most frequently used.</p>
|
||||
<p>Thanks🤘</p>
|
||||
</div>
|
||||
</Modal>;
|
||||
|
||||
}
|
||||
5
ui/src/components/tracking/TrackingModal.less
Normal file
5
ui/src/components/tracking/TrackingModal.less
Normal file
@@ -0,0 +1,5 @@
|
||||
.trackingModal {
|
||||
&__description {
|
||||
margin-top:10rem;
|
||||
}
|
||||
}
|
||||
@@ -1,245 +1,332 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
|
||||
import { Divider, Input, Radio, TimePicker, Button, RadioGroup } from '@douyinfe/semi-ui';
|
||||
import { InputNumber } from '@douyinfe/semi-ui';
|
||||
import {Divider, Input, Radio, TimePicker, Button, RadioGroup, Checkbox} from '@douyinfe/semi-ui';
|
||||
import {InputNumber} from '@douyinfe/semi-ui';
|
||||
import Headline from '../../components/headline/Headline';
|
||||
import { xhrPost } from '../../services/xhr';
|
||||
import { SegmentPart } from '../../components/segment/SegmentPart';
|
||||
import { Banner, Toast } from '@douyinfe/semi-ui';
|
||||
import { IconSave, IconCalendar, IconKey, IconRefresh, IconSignal } from '@douyinfe/semi-icons';
|
||||
import {xhrPost} from '../../services/xhr';
|
||||
import {SegmentPart} from '../../components/segment/SegmentPart';
|
||||
import {Banner, Toast} from '@douyinfe/semi-ui';
|
||||
import {IconSave, IconCalendar, IconKey, IconRefresh, IconSignal, IconLineChartStroked, IconSearch} from '@douyinfe/semi-icons';
|
||||
import './GeneralSettings.less';
|
||||
|
||||
function formatFromTimestamp(ts) {
|
||||
const date = new Date(ts);
|
||||
return `${date.getHours()}:${date.getMinutes() > 9 ? date.getMinutes() : '0' + date.getMinutes()}`;
|
||||
const date = new Date(ts);
|
||||
return `${date.getHours()}:${date.getMinutes() > 9 ? date.getMinutes() : '0' + date.getMinutes()}`;
|
||||
}
|
||||
|
||||
function formatFromTBackend(time) {
|
||||
if (time == null || time.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const date = new Date();
|
||||
const split = time.split(':');
|
||||
date.setHours(split[0]);
|
||||
date.setMinutes(split[1]);
|
||||
return date.getTime();
|
||||
if (time == null || time.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const date = new Date();
|
||||
const split = time.split(':');
|
||||
date.setHours(split[0]);
|
||||
date.setMinutes(split[1]);
|
||||
return date.getTime();
|
||||
}
|
||||
|
||||
const GeneralSettings = function GeneralSettings() {
|
||||
const dispatch = useDispatch();
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const dispatch = useDispatch();
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
|
||||
const settings = useSelector((state) => state.generalSettings.settings);
|
||||
const settings = useSelector((state) => state.generalSettings.settings);
|
||||
|
||||
const [interval, setInterval] = React.useState('');
|
||||
const [port, setPort] = React.useState('');
|
||||
const [scrapingAntApiKey, setScrapingAntApiKey] = React.useState('');
|
||||
const [scrapingAntProxy, setScrapingAntProxy] = React.useState('');
|
||||
const [workingHourFrom, setWorkingHourFrom] = React.useState(null);
|
||||
const [workingHourTo, setWorkingHourTo] = React.useState(null);
|
||||
React.useEffect(() => {
|
||||
async function init() {
|
||||
await dispatch.generalSettings.getGeneralSettings();
|
||||
setLoading(false);
|
||||
}
|
||||
const [interval, setInterval] = React.useState('');
|
||||
const [port, setPort] = React.useState('');
|
||||
const [scrapingAntApiKey, setScrapingAntApiKey] = React.useState('');
|
||||
const [scrapingAntProxy, setScrapingAntProxy] = React.useState('');
|
||||
const [workingHourFrom, setWorkingHourFrom] = React.useState(null);
|
||||
const [workingHourTo, setWorkingHourTo] = React.useState(null);
|
||||
const [demoMode, setDemoMode] = React.useState(null);
|
||||
const [analyticsEnabled, setAnalyticsEnabled] = React.useState(null);
|
||||
|
||||
init();
|
||||
}, []);
|
||||
React.useEffect(() => {
|
||||
async function init() {
|
||||
await dispatch.generalSettings.getGeneralSettings();
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
async function init() {
|
||||
setInterval(settings?.interval);
|
||||
setPort(settings?.port);
|
||||
setScrapingAntApiKey(settings?.scrapingAnt?.apiKey);
|
||||
setWorkingHourFrom(settings?.workingHours?.from);
|
||||
setWorkingHourTo(settings?.workingHours?.to);
|
||||
setScrapingAntProxy(settings?.scrapingAnt?.proxy || 'datacenter');
|
||||
}
|
||||
init();
|
||||
}, []);
|
||||
|
||||
init();
|
||||
}, [settings]);
|
||||
React.useEffect(() => {
|
||||
async function init() {
|
||||
setInterval(settings?.interval);
|
||||
setPort(settings?.port);
|
||||
setScrapingAntApiKey(settings?.scrapingAnt?.apiKey);
|
||||
setWorkingHourFrom(settings?.workingHours?.from);
|
||||
setWorkingHourTo(settings?.workingHours?.to);
|
||||
setScrapingAntProxy(settings?.scrapingAnt?.proxy || 'datacenter');
|
||||
setAnalyticsEnabled(settings?.analytics || false);
|
||||
setDemoMode(settings?.demoMode || false);
|
||||
}
|
||||
|
||||
const nullOrEmpty = (val) => val == null || val.length === 0;
|
||||
init();
|
||||
}, [settings]);
|
||||
|
||||
const throwMessage = (message, type) => {
|
||||
if (type === 'error') {
|
||||
Toast.error(message);
|
||||
} else {
|
||||
Toast.success(message);
|
||||
}
|
||||
};
|
||||
const nullOrEmpty = (val) => val == null || val.length === 0;
|
||||
|
||||
const onStore = async () => {
|
||||
if (nullOrEmpty(interval)) {
|
||||
throwMessage('Interval may not be empty.', 'error');
|
||||
return;
|
||||
}
|
||||
if (nullOrEmpty(port)) {
|
||||
throwMessage('Port may not be empty.', 'error');
|
||||
return;
|
||||
}
|
||||
if (
|
||||
(!nullOrEmpty(workingHourFrom) && nullOrEmpty(workingHourTo)) ||
|
||||
(nullOrEmpty(workingHourFrom) && !nullOrEmpty(workingHourTo))
|
||||
) {
|
||||
throwMessage('Working hours to and from must be set if either to or from has been set before.', 'error');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await xhrPost('/api/admin/generalSettings', {
|
||||
interval,
|
||||
port,
|
||||
scrapingAnt: {
|
||||
apiKey: scrapingAntApiKey,
|
||||
proxy: scrapingAntProxy,
|
||||
},
|
||||
workingHours: {
|
||||
from: workingHourFrom,
|
||||
to: workingHourTo,
|
||||
},
|
||||
});
|
||||
} catch (exception) {
|
||||
console.error(exception);
|
||||
throwMessage('Error while trying to store settings.', 'error');
|
||||
return;
|
||||
}
|
||||
throwMessage('Settings stored successfully. You MUST restart Fredy.', 'success');
|
||||
};
|
||||
const throwMessage = (message, type) => {
|
||||
if (type === 'error') {
|
||||
Toast.error(message);
|
||||
} else {
|
||||
Toast.success(message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!loading && (
|
||||
<React.Fragment>
|
||||
<Headline text="General Settings" />
|
||||
<Banner
|
||||
fullMode={false}
|
||||
type="info"
|
||||
closeIcon={null}
|
||||
title={<div style={{ fontWeight: 600, fontSize: '14px', lineHeight: '20px' }}>Info</div>}
|
||||
style={{ marginBottom: '1rem' }}
|
||||
description="If you change any settings, you must restart Fredy afterwards."
|
||||
/>
|
||||
<div>
|
||||
<SegmentPart
|
||||
name="Interval"
|
||||
helpText="Interval in minutes for running queries against the configured services."
|
||||
Icon={IconRefresh}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={1440}
|
||||
placeholder="Interval in minutes"
|
||||
value={interval}
|
||||
formatter={(value) => `${value}`.replace(/\D/g, '')}
|
||||
onChange={(value) => setInterval(value)}
|
||||
suffix={'minutes'}
|
||||
/>
|
||||
</SegmentPart>
|
||||
<Divider margin="1rem" />
|
||||
<SegmentPart name="Port" helpText="Port on which Fredy is running." Icon={IconSignal}>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={99999}
|
||||
placeholder="Port"
|
||||
value={port}
|
||||
formatter={(value) => `${value}`.replace(/\D/g, '')}
|
||||
onChange={(value) => setPort(value)}
|
||||
/>
|
||||
</SegmentPart>
|
||||
<Divider margin="1rem" />
|
||||
<SegmentPart
|
||||
name="ScrapingAnt Api Key"
|
||||
helpText="The api key for ScrapingAnt is used to be able to scrape Immoscout."
|
||||
Icon={IconKey}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="ScrapingAnt Api Key"
|
||||
value={scrapingAntApiKey}
|
||||
onChange={(val) => setScrapingAntApiKey(val)}
|
||||
/>
|
||||
</SegmentPart>
|
||||
<Divider margin="1rem" />
|
||||
<SegmentPart
|
||||
name="ScrapingAnt proxy settings"
|
||||
helpText="Scraping ant provides different proxies."
|
||||
Icon={IconKey}
|
||||
>
|
||||
<Banner
|
||||
fullMode={false}
|
||||
type="info"
|
||||
closeIcon={null}
|
||||
title={
|
||||
<div style={{ fontWeight: 600, fontSize: '14px', lineHeight: '20px' }}>
|
||||
ScrapingAnt is needed to scrape Immoscout. ScrapingAnt itself is using 2 different types of proxies
|
||||
</div>
|
||||
}
|
||||
style={{ marginBottom: '1rem' }}
|
||||
description={
|
||||
<div>
|
||||
<h4>Datacenter-Proxy</h4>
|
||||
Proxy server located in one of the datacenters across the world. Datacenter proxies are slower and
|
||||
more likely to fail, but they are cheaper. A call with a datacenter proxy cost 10 credits.
|
||||
<h4>Residential-Proxy</h4>
|
||||
High-quality proxy server located in one of the real people houses across the world. Datacenter
|
||||
proxies are faster and more likely to success, but they are more expensive.
|
||||
<br />
|
||||
<br />
|
||||
<b>
|
||||
On the free tier, you have 10.000 credits, so chose your option wisely. Keep in mind, only
|
||||
successful calls will be charged.
|
||||
</b>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
const onStore = async () => {
|
||||
if (nullOrEmpty(interval)) {
|
||||
throwMessage('Interval may not be empty.', 'error');
|
||||
return;
|
||||
}
|
||||
if (nullOrEmpty(port)) {
|
||||
throwMessage('Port may not be empty.', 'error');
|
||||
return;
|
||||
}
|
||||
if (
|
||||
(!nullOrEmpty(workingHourFrom) && nullOrEmpty(workingHourTo)) ||
|
||||
(nullOrEmpty(workingHourFrom) && !nullOrEmpty(workingHourTo))
|
||||
) {
|
||||
throwMessage('Working hours to and from must be set if either to or from has been set before.', 'error');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await xhrPost('/api/admin/generalSettings', {
|
||||
interval,
|
||||
port,
|
||||
scrapingAnt: {
|
||||
apiKey: scrapingAntApiKey,
|
||||
proxy: scrapingAntProxy,
|
||||
},
|
||||
workingHours: {
|
||||
from: workingHourFrom,
|
||||
to: workingHourTo,
|
||||
},
|
||||
demoMode,
|
||||
analyticsEnabled
|
||||
});
|
||||
} catch (exception) {
|
||||
console.error(exception);
|
||||
throwMessage('Error while trying to store settings.', 'error');
|
||||
return;
|
||||
}
|
||||
throwMessage('Settings stored successfully.', 'success');
|
||||
};
|
||||
|
||||
<RadioGroup value={scrapingAntProxy} onChange={(e) => setScrapingAntProxy(e.target.value)}>
|
||||
<Radio name="datacenter" value="datacenter" checked={scrapingAntProxy === 'datacenter'}>
|
||||
Datacenter proxy
|
||||
</Radio>
|
||||
<Radio name="residential" value="residential" checked={scrapingAntProxy === 'residential'}>
|
||||
Residential proxy
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</SegmentPart>
|
||||
<Divider margin="1rem" />
|
||||
<SegmentPart
|
||||
name="Working hours"
|
||||
helpText="During this hours, Fredy will search for new apartments. If nothing is configured, Fredy will search around the clock."
|
||||
Icon={IconCalendar}
|
||||
>
|
||||
<div className="generalSettings__timePickerContainer">
|
||||
<TimePicker
|
||||
format={'HH:mm'}
|
||||
insetLabel="From"
|
||||
value={formatFromTBackend(workingHourFrom)}
|
||||
placeholder=""
|
||||
onChange={(val) => {
|
||||
setWorkingHourFrom(val == null ? null : formatFromTimestamp(val));
|
||||
}}
|
||||
/>
|
||||
<TimePicker
|
||||
format={'HH:mm'}
|
||||
insetLabel="Until"
|
||||
value={formatFromTBackend(workingHourTo)}
|
||||
placeholder=""
|
||||
onChange={(val) => {
|
||||
setWorkingHourTo(val == null ? null : formatFromTimestamp(val));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</SegmentPart>
|
||||
<Divider margin="1rem" />
|
||||
<Button type="primary" theme="solid" onClick={onStore} icon={<IconSave />}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
{!loading && (
|
||||
<React.Fragment>
|
||||
<Headline text="General Settings"/>
|
||||
<Banner
|
||||
fullMode={false}
|
||||
type="info"
|
||||
closeIcon={null}
|
||||
title={<div style={{fontWeight: 600, fontSize: '14px', lineHeight: '20px'}}>Info</div>}
|
||||
style={{marginBottom: '1rem'}}
|
||||
description="If you change any settings, you must restart Fredy afterwards."
|
||||
/>
|
||||
<div>
|
||||
<SegmentPart
|
||||
name="Interval"
|
||||
helpText="Interval in minutes for running queries against the configured services."
|
||||
Icon={IconRefresh}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={1440}
|
||||
placeholder="Interval in minutes"
|
||||
value={interval}
|
||||
formatter={(value) => `${value}`.replace(/\D/g, '')}
|
||||
onChange={(value) => setInterval(value)}
|
||||
suffix={'minutes'}
|
||||
/>
|
||||
</SegmentPart>
|
||||
<Divider margin="1rem"/>
|
||||
<SegmentPart name="Port" helpText="Port on which Fredy is running." Icon={IconSignal}>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={99999}
|
||||
placeholder="Port"
|
||||
value={port}
|
||||
formatter={(value) => `${value}`.replace(/\D/g, '')}
|
||||
onChange={(value) => setPort(value)}
|
||||
/>
|
||||
</SegmentPart>
|
||||
<Divider margin="1rem"/>
|
||||
<SegmentPart
|
||||
name="ScrapingAnt Api Key"
|
||||
helpText="The api key for ScrapingAnt is used to be able to scrape Immoscout."
|
||||
Icon={IconKey}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="ScrapingAnt Api Key"
|
||||
value={scrapingAntApiKey}
|
||||
onChange={(val) => setScrapingAntApiKey(val)}
|
||||
/>
|
||||
</SegmentPart>
|
||||
<Divider margin="1rem"/>
|
||||
<SegmentPart
|
||||
name="ScrapingAnt proxy settings"
|
||||
helpText="Scraping ant provides different proxies."
|
||||
Icon={IconKey}
|
||||
>
|
||||
<Banner
|
||||
fullMode={false}
|
||||
type="info"
|
||||
closeIcon={null}
|
||||
title={
|
||||
<div style={{fontWeight: 600, fontSize: '14px', lineHeight: '20px'}}>
|
||||
ScrapingAnt is needed to scrape Immoscout. ScrapingAnt itself is using 2
|
||||
different types of proxies
|
||||
</div>
|
||||
}
|
||||
style={{marginBottom: '1rem'}}
|
||||
description={
|
||||
<div>
|
||||
<h4>Datacenter-Proxy</h4>
|
||||
Proxy server located in one of the datacenters across the world. Datacenter
|
||||
proxies are slower and
|
||||
more likely to fail, but they are cheaper. A call with a datacenter proxy cost
|
||||
10 credits.
|
||||
<h4>Residential-Proxy</h4>
|
||||
High-quality proxy server located in one of the real people houses across the
|
||||
world. Datacenter
|
||||
proxies are faster and more likely to success, but they are more expensive.
|
||||
<br/>
|
||||
<br/>
|
||||
<b>
|
||||
On the free tier, you have 10.000 credits, so chose your option wisely. Keep
|
||||
in mind, only
|
||||
successful calls will be charged.
|
||||
</b>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<RadioGroup value={scrapingAntProxy} onChange={(e) => setScrapingAntProxy(e.target.value)}>
|
||||
<Radio name="datacenter" value="datacenter" checked={scrapingAntProxy === 'datacenter'}>
|
||||
Datacenter proxy
|
||||
</Radio>
|
||||
<Radio name="residential" value="residential"
|
||||
checked={scrapingAntProxy === 'residential'}>
|
||||
Residential proxy
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
</SegmentPart>
|
||||
<Divider margin="1rem"/>
|
||||
<SegmentPart
|
||||
name="Working hours"
|
||||
helpText="During this hours, Fredy will search for new apartments. If nothing is configured, Fredy will search around the clock."
|
||||
Icon={IconCalendar}
|
||||
>
|
||||
<div className="generalSettings__timePickerContainer">
|
||||
<TimePicker
|
||||
format={'HH:mm'}
|
||||
insetLabel="From"
|
||||
value={formatFromTBackend(workingHourFrom)}
|
||||
placeholder=""
|
||||
onChange={(val) => {
|
||||
setWorkingHourFrom(val == null ? null : formatFromTimestamp(val));
|
||||
}}
|
||||
/>
|
||||
<TimePicker
|
||||
format={'HH:mm'}
|
||||
insetLabel="Until"
|
||||
value={formatFromTBackend(workingHourTo)}
|
||||
placeholder=""
|
||||
onChange={(val) => {
|
||||
setWorkingHourTo(val == null ? null : formatFromTimestamp(val));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</SegmentPart>
|
||||
<Divider margin="1rem"/>
|
||||
|
||||
<SegmentPart
|
||||
name="Analytics"
|
||||
helpText="Insights into the usage of Fredy."
|
||||
Icon={IconLineChartStroked}
|
||||
>
|
||||
<Banner
|
||||
fullMode={false}
|
||||
type="info"
|
||||
closeIcon={null}
|
||||
title={
|
||||
<div style={{fontWeight: 600, fontSize: '14px', lineHeight: '20px'}}>
|
||||
Explanation
|
||||
</div>
|
||||
}
|
||||
style={{marginBottom: '1rem'}}
|
||||
description={
|
||||
<div>
|
||||
Analytics are disabled by default. If you choose to enable them, we will begin tracking the following:<br/>
|
||||
<ul>
|
||||
<li>Name of active provider (e.g. Immoscout)</li>
|
||||
<li>Name of active adapter (e.g. Console)</li>
|
||||
<li>language</li>
|
||||
<li>os</li>
|
||||
<li>node version</li>
|
||||
<li>arch</li>
|
||||
</ul>
|
||||
The data is sent anonymously and helps me understand which providers or adapters are being used the most. In the end it helps me to improve fredy.
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
checked={analyticsEnabled}
|
||||
onChange={(e) => setAnalyticsEnabled(e.target.checked)}
|
||||
> Enabled
|
||||
</Checkbox>
|
||||
|
||||
</SegmentPart>
|
||||
|
||||
<Divider margin="1rem"/>
|
||||
|
||||
<SegmentPart
|
||||
name="Demo Mode"
|
||||
helpText="If enabled, Fredy runs in demo mode."
|
||||
Icon={IconSearch}
|
||||
>
|
||||
<Banner
|
||||
fullMode={false}
|
||||
type="info"
|
||||
closeIcon={null}
|
||||
title={
|
||||
<div style={{fontWeight: 600, fontSize: '14px', lineHeight: '20px'}}>
|
||||
Explanation
|
||||
</div>
|
||||
}
|
||||
style={{marginBottom: '1rem'}}
|
||||
description={
|
||||
<div>
|
||||
In demo mode, Fredy will not (really) search for any real estates. Fredy is in a lockdown mode. Also
|
||||
all database files will be set back to the default values at midnight.
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
checked={demoMode}
|
||||
onChange={(e) => setDemoMode(e.target.checked)}
|
||||
> Enabled
|
||||
</Checkbox>
|
||||
|
||||
</SegmentPart>
|
||||
|
||||
<Divider margin="1rem"/>
|
||||
<Button type="primary" theme="solid" onClick={onStore} icon={<IconSave/>}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GeneralSettings;
|
||||
|
||||
@@ -1,63 +1,86 @@
|
||||
import React from 'react';
|
||||
import { format } from '../../services/time/timeService';
|
||||
import { Card, Descriptions, Divider } from '@douyinfe/semi-ui';
|
||||
import { IconBolt } from '@douyinfe/semi-icons';
|
||||
export default function ProcessingTimes({ processingTimes }) {
|
||||
const { Meta } = Card;
|
||||
return (
|
||||
<>
|
||||
<Descriptions
|
||||
row
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: '#35363c',
|
||||
borderRadius: '4px',
|
||||
padding: '10px',
|
||||
}}
|
||||
>
|
||||
<Descriptions.Item itemKey="Processing Interval">{processingTimes.interval} min</Descriptions.Item>
|
||||
{processingTimes.lastRun && (
|
||||
<>
|
||||
<Descriptions.Item itemKey="Last run">{format(processingTimes.lastRun)}</Descriptions.Item>
|
||||
<Descriptions.Item itemKey="Next run">
|
||||
{format(processingTimes.lastRun + processingTimes.interval * 60000)}
|
||||
</Descriptions.Item>
|
||||
</>
|
||||
)}
|
||||
</Descriptions>
|
||||
import {format} from '../../services/time/timeService';
|
||||
import {Banner, Card, Descriptions, Divider} from '@douyinfe/semi-ui';
|
||||
import {IconBolt} from '@douyinfe/semi-icons';
|
||||
|
||||
{processingTimes.scrapingAntData != null && (
|
||||
<>
|
||||
<Divider margin="1rem" />
|
||||
<Card
|
||||
style={{ backgroundColor: '#35363c' }}
|
||||
export default function ProcessingTimes({processingTimes = {}}) {
|
||||
const {Meta} = Card;
|
||||
if (Object.keys(processingTimes).length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (processingTimes.error != null) {
|
||||
return <Banner
|
||||
fullMode={false}
|
||||
type="danger"
|
||||
closeIcon={null}
|
||||
title={
|
||||
<Meta
|
||||
title="Remaining ScrapingAnt calls"
|
||||
description="Information about your Scraping Ant Plan"
|
||||
avatar={<IconBolt />}
|
||||
/>
|
||||
<div style={{fontWeight: 600, fontSize: '14px', lineHeight: '20px'}}>
|
||||
Scraping Ant Error
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<p>Plan: {processingTimes.scrapingAntData.plan_name}</p>
|
||||
<p>
|
||||
Duration: {format(new Date(processingTimes.scrapingAntData.start_date))} -{' '}
|
||||
{format(new Date(processingTimes.scrapingAntData.end_date))}
|
||||
<br />
|
||||
Credits: {processingTimes.scrapingAntData.remained_credits}/
|
||||
{processingTimes.scrapingAntData.plan_total_credits}
|
||||
</p>
|
||||
If you want to scrape Immoscout or Immonet more often, you have to purchase a premium account of{' '}
|
||||
<a href="https://scrapingant.com/" target="_blank" rel="noreferrer">
|
||||
ScrapingAnt
|
||||
</a>
|
||||
. You can use the code <b>FREDY10</b> to get 10% off. (No affiliation, we are <b>not</b> getting paid to
|
||||
recommend ScrapingAnt.)
|
||||
</Card>
|
||||
style={{marginBottom: '1rem'}}
|
||||
description={
|
||||
<div>
|
||||
{processingTimes.error}
|
||||
</div>
|
||||
}
|
||||
/>;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Descriptions
|
||||
row
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: '#35363c',
|
||||
borderRadius: '4px',
|
||||
padding: '10px',
|
||||
}}
|
||||
>
|
||||
<Descriptions.Item itemKey="Processing Interval">{processingTimes.interval} min</Descriptions.Item>
|
||||
{processingTimes.lastRun && (
|
||||
<>
|
||||
<Descriptions.Item itemKey="Last run">{format(processingTimes.lastRun)}</Descriptions.Item>
|
||||
<Descriptions.Item itemKey="Next run">
|
||||
{format(processingTimes.lastRun + processingTimes.interval * 60000)}
|
||||
</Descriptions.Item>
|
||||
</>
|
||||
)}
|
||||
</Descriptions>
|
||||
|
||||
{(processingTimes.scrapingAntData != null && Object.keys(processingTimes.scrapingAntData).length > 0) &&(
|
||||
<>
|
||||
<Divider margin="1rem"/>
|
||||
<Card
|
||||
style={{backgroundColor: '#35363c'}}
|
||||
title={
|
||||
<Meta
|
||||
title="Remaining ScrapingAnt calls"
|
||||
description="Information about your Scraping Ant Plan"
|
||||
avatar={<IconBolt/>}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<p>Plan: {processingTimes.scrapingAntData.plan_name}</p>
|
||||
<p>
|
||||
Duration: {format(new Date(processingTimes.scrapingAntData.start_date))} -{' '}
|
||||
{format(new Date(processingTimes.scrapingAntData.end_date))}
|
||||
<br/>
|
||||
Credits: {processingTimes.scrapingAntData.remained_credits}/
|
||||
{processingTimes.scrapingAntData.plan_total_credits}
|
||||
</p>
|
||||
If you want to scrape Immoscout or Immonet more often, you have to purchase a premium account
|
||||
of{' '}
|
||||
<a href="https://scrapingant.com/" target="_blank" rel="noreferrer">
|
||||
ScrapingAnt
|
||||
</a>
|
||||
. You can use the code <b>FREDY10</b> to get 10% off. (No affiliation, we are <b>not</b> getting
|
||||
paid by ScrapingAnt.)
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
36
yarn.lock
36
yarn.lock
@@ -1626,6 +1626,13 @@ acorn@^8.9.0:
|
||||
resolved "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz"
|
||||
integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==
|
||||
|
||||
agent-base@6:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
|
||||
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
|
||||
dependencies:
|
||||
debug "4"
|
||||
|
||||
ajv@^6.12.4:
|
||||
version "6.12.4"
|
||||
resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz"
|
||||
@@ -2473,6 +2480,13 @@ debug@3.2.7, debug@^3.2.6:
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debug@4, debug@~4.3.6:
|
||||
version "4.3.7"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52"
|
||||
integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==
|
||||
dependencies:
|
||||
ms "^2.1.3"
|
||||
|
||||
debug@^3.1.0:
|
||||
version "3.2.6"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz"
|
||||
@@ -2508,13 +2522,6 @@ debug@^4.3.2:
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
debug@~4.3.6:
|
||||
version "4.3.7"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52"
|
||||
integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==
|
||||
dependencies:
|
||||
ms "^2.1.3"
|
||||
|
||||
decamelize@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz"
|
||||
@@ -4004,6 +4011,14 @@ http-outgoing@^0.12.0:
|
||||
resolved "https://registry.npmjs.org/http-outgoing/-/http-outgoing-0.12.0.tgz"
|
||||
integrity sha1-Zi86J8ek0UySS19TFJCe+r3hgw0=
|
||||
|
||||
https-proxy-agent@5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
|
||||
integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
|
||||
dependencies:
|
||||
agent-base "6"
|
||||
debug "4"
|
||||
|
||||
human-signals@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28"
|
||||
@@ -5457,6 +5472,13 @@ minimist@^1.2.5:
|
||||
resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz"
|
||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||
|
||||
mixpanel@^0.18.0:
|
||||
version "0.18.0"
|
||||
resolved "https://registry.yarnpkg.com/mixpanel/-/mixpanel-0.18.0.tgz#f010f2622902d0d4b434de238446ec8e5966ee32"
|
||||
integrity sha512-VyUoiLB/S/7abYYHGD5x0LijeuJCUabG8Hb+FvYU3Y99xHf1Qh+s4/pH9lt50fRitAHncWbU1FE01EknUfVVjQ==
|
||||
dependencies:
|
||||
https-proxy-agent "5.0.0"
|
||||
|
||||
mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
|
||||
version "0.5.3"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
|
||||
|
||||
Reference in New Issue
Block a user