您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Keeps track of the price of athome.lu housing prices
// ==UserScript== // @name Athome.lu Price Tracker // @namespace http://tampermonkey.net/ // @version 0.1.1 // @description Keeps track of the price of athome.lu housing prices // @license MIT // @author Filipe Neves ([email protected]), Brian Tacchi ([email protected]) // @match https://www.athome.lu/vente/* // @icon https://www.google.com/s2/favicons?sz=64&domain=athome.lu // @grant none // ==/UserScript== (function () { 'use strict'; const API_BASE = 'https://athome-lu-tracker.red-limit-7cac.workers.dev/api'; function getPageId() { const match = window.location.pathname.match(/\/vente\/[^/]+\/([^/]+\/id-\d+)/i); return match ? match[1] : null; } function getPrice() { const xpath = '/html/body/div[1]/div[1]/div/article/div[1]/div[1]/div[1]/div[2]/span[2]/span/span/span'; const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); const node = result.singleNodeValue; if (!node) return null; const raw = node.textContent.replace(/[^\d]/g, ''); return parseInt(raw, 10); } async function sendPriceData(id, price) { try { const res = await fetch(`${API_BASE}/record`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id, price }), }); const text = await res.text(); console.log('[Athome Tracker] ✅ Sent:', { id, price, status: res.status, body: text }); if (!res.ok) { console.warn('[Athome Tracker] ❌ Server error', res.status, text); } } catch (err) { console.error('[Athome Tracker] ❌ Network error:', err); } } async function fetchHistory(id) { try { const res = await fetch(`${API_BASE}/history?id=${encodeURIComponent(id)}`); if (!res.ok) throw new Error('Request failed'); return await res.json(); } catch (err) { console.error('[Athome Tracker] ❌ Failed to fetch history', err); return []; } } function injectHistoryGraph(id) { const infoBlock = document.querySelector('.info-block'); if (!infoBlock) { console.warn('[Athome Tracker] Could not find .info-block to insert chart after.'); return; } // Check if we already injected a graph to avoid duplicates if (document.getElementById('price-history-chart')) return; // Create characteristics-container + title const container = document.createElement('div'); container.className = 'characteristics-container'; const title = document.createElement('h2'); title.className = 'characteristics-main-title'; title.textContent = 'Price History'; container.appendChild(title); // Create chart wrapper const wrapper = document.createElement('div'); wrapper.style.marginTop = '10px'; wrapper.style.background = '#ffffff'; // ← white background wrapper.style.border = 'none'; // ← remove border wrapper.style.borderRadius = '8px'; wrapper.style.padding = '10px'; wrapper.style.width = '100%'; wrapper.style.boxSizing = 'border-box'; const canvas = document.createElement('canvas'); canvas.id = 'price-history-chart'; canvas.style.width = '100%'; canvas.style.height = '180px'; wrapper.appendChild(canvas); container.appendChild(wrapper); // Insert container after .info-block infoBlock.parentNode.insertBefore(container, infoBlock.nextSibling); // Load Chart.js and render const script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/chart.js'; script.onload = async () => { const history = await fetchHistory(id); if (!history || !history.length) { const note = document.createElement('div'); note.textContent = 'No price history available.'; wrapper.appendChild(note); return; } const labels = history.map(e => { const d = new Date(e.timestamp); return `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}/${d.getFullYear()}`; }); const prices = history.map(e => e.price); const ctx = canvas.getContext('2d'); new Chart(ctx, { type: 'line', data: { labels, datasets: [{ label: 'Price (€)', data: prices, borderColor: '#e4002b', backgroundColor: 'rgba(0, 150, 136, 0.15)', pointBackgroundColor: '#e4002b', pointBorderColor: '#fff', pointRadius: 3, pointHoverRadius: 5, borderWidth: 2, tension: 0.25 }] }, options: { maintainAspectRatio: false, responsive: true, plugins: { legend: { display: false }, tooltip: { backgroundColor: '#333', titleColor: '#fff', bodyColor: '#eee', padding: 8, cornerRadius: 4 } }, scales: { x: { ticks: { maxTicksLimit: 6, color: '#666', font: { size: 12 }, autoSkipPadding: 12 }, grid: { color: 'rgba(0,0,0,0.03)' } }, y: { beginAtZero: false, ticks: { color: '#666', font: { size: 12 }, callback: val => val.toLocaleString() + '€' }, grid: { color: 'rgba(0,0,0,0.05)' } } } } }); }; document.body.appendChild(script); } function observePageAndInject() { const observer = new MutationObserver(() => { const id = getPageId(); const price = getPrice(); const infoBlock = document.querySelector('.info-block'); if (id && price && infoBlock && !document.getElementById('price-history-chart')) { sendPriceData(id, price); injectHistoryGraph(id); } }); observer.observe(document.body, { childList: true, subtree: true }); } window.addEventListener('load', () => { observePageAndInject(); }); })();