Compare commits

...

2 Commits

Author SHA1 Message Date
819f36f26f Merge remote-tracking branch 'origin/main' 2025-05-11 07:15:30 +02:00
428408b659 Update Version 1.5.1
"Adding Note Option in Portfolio Page",
    "Fixing Editing Option in Trade Summary"
2025-05-11 07:14:30 +02:00
12 changed files with 734 additions and 67 deletions

19
docker-compose.yml Normal file
View File

@ -0,0 +1,19 @@
version: "3.9"
services:
hodleye:
build: .
image: hodleye-crypto-tracker:latest
container_name: hodleye-container
env_file:
- .env
ports:
- "3099:3099"
- "5001:5001"
volumes:
- hodleye_data:/app/data
restart: unless-stopped
volumes:
hodleye_data:
external: true

View File

@ -205,3 +205,26 @@ button {
}
.note-button, .edit-button {
cursor: pointer;
margin-left: 5px;
font-size: 16px;
}
.note-button.has-note {
color: green;
}
.note-button.no-note {
color: red;
}
.note-input-row {
display: flex;
align-items: center;
gap: 1rem;
}
.note-input-row textarea {
flex: 1;
}

View File

@ -68,7 +68,7 @@
</button>
<div class="version-info">
<span id="currentVersion">Version 1.5.0</span>
<span id="currentVersion">Version 1.5.1</span>
<span
id="updateAvailable"
style="display: none; color: red; cursor: pointer"

View File

@ -1,24 +1,37 @@
let portfolioData = [];
let coinColors = {};
let addTransactionModal, editTransactionModal, colorConfigModal;
let addTransactionModal, editTransactionModal, colorConfigModal, noteModal;
let buyForm, sellForm;
let portfolioList, pieChartCanvas, chartLegend;
let editIdGlobal = null;
let currentNoteId = null;
let currentNoteType = null;
function formatPrice(value) {
if (isNaN(value)) return "-";
if (value < 0.01) return value.toFixed(6);
if (value < 1) return value.toFixed(4);
return value.toFixed(2);
}
document.addEventListener("DOMContentLoaded", () => {
addTransactionModal = document.getElementById("addTransactionModal");
editTransactionModal = document.getElementById("editTransactionModal");
colorConfigModal = document.getElementById("colorConfigModal");
noteModal = document.getElementById("noteModal");
buyForm = document.getElementById("buyForm");
sellForm = document.getElementById("sellForm");
portfolioList = document.getElementById("portfolioList");
pieChartCanvas = document.getElementById("portfolioPieChart");
chartLegend = document.getElementById("chartLegend");
window.addEventListener("click", (event) => {
if (event.target === addTransactionModal) closeAddTransactionModal();
if (event.target === editTransactionModal) closeEditTransactionModal();
if (event.target === colorConfigModal) closeColorConfigModal();
if (event.target === noteModal) closeNoteModal();
});
loadCoinColorsFromStorage();
loadPortfolioData();
setInterval(() => {
@ -55,12 +68,14 @@ function renderPortfolio() {
if (!grouped[t.symbol]) grouped[t.symbol] = [];
grouped[t.symbol].push(t);
});
for (let sym in grouped) {
const section = document.createElement("div");
section.className = "coin-section";
const heading = document.createElement("h2");
heading.textContent = sym;
section.appendChild(heading);
const table = document.createElement("table");
const thead = document.createElement("thead");
thead.innerHTML = `
@ -76,9 +91,11 @@ function renderPortfolio() {
</tr>
`;
table.appendChild(thead);
const tbody = document.createElement("tbody");
let totalAmount = 0;
let totalCost = 0;
grouped[sym].forEach(tx => {
const currentTotal = tx.amount * tx.currentPrice;
const initialTotal = tx.amount * tx.buyPrice;
@ -86,33 +103,40 @@ function renderPortfolio() {
const pct = initialTotal === 0 ? 0 : (diff / initialTotal) * 100;
totalAmount += tx.amount;
totalCost += initialTotal;
const row = document.createElement("tr");
const diffClass = diff >= 0 ? "positive" : "negative";
const editButton = `<span class="edit-button" onclick="openEditTransactionModal('${tx.id}')">✎</span>`;
const noteButton = `<span class="note-button ${tx.note ? 'has-note' : 'no-note'}" onclick="openNoteModal('${tx.id}', 'portfolio')">✉</span>`;
row.innerHTML = `
<td>${tx.symbol}</td>
<td>${tx.amount}</td>
<td>${tx.buyPrice}</td>
<td>${tx.currentPrice.toFixed(2)}</td>
<td>${formatPrice(tx.buyPrice)}</td>
<td>${formatPrice(tx.currentPrice)}</td>
<td>${(tx.amount * tx.buyPrice).toFixed(2)}</td>
<td class="${diffClass}">${diff.toFixed(2)}</td>
<td class="${diffClass}">${pct.toFixed(2)}%</td>
<td>${tx.date}${editButton}</td>
<td>${tx.date}${editButton}${noteButton}</td>
`;
tbody.appendChild(row);
});
const totalCurrent = totalAmount * (grouped[sym][0].currentPrice || 0);
const totalDiff = totalCurrent - totalCost;
const totalPct = totalCost === 0 ? 0 : (totalDiff / totalCost) * 100;
const avgBuyPrice = totalAmount === 0 ? 0 : totalCost / totalAmount;
const totalRow = document.createElement("tr");
totalRow.className = "summary-row";
const summaryClass = totalDiff >= 0 ? "positive" : "negative";
totalRow.innerHTML = `
<td>${sym} (Total)</td>
<td>${totalAmount.toFixed(6)}</td>
<td>Ø ${avgBuyPrice.toFixed(2)}</td>
<td>${(grouped[sym][0].currentPrice || 0).toFixed(2)}</td>
<td>Ø ${formatPrice(avgBuyPrice)}</td>
<td>${formatPrice(grouped[sym][0].currentPrice || 0)}</td>
<td>${totalCost.toFixed(2)}</td>
<td class="${summaryClass}">${totalDiff.toFixed(2)}</td>
<td class="${summaryClass}">${totalPct.toFixed(2)}%</td>
@ -147,6 +171,7 @@ function drawPieChart() {
if (!grouped[t.symbol]) grouped[t.symbol] = 0;
grouped[t.symbol] += t.currentPrice * t.amount;
});
let totalAll = 0;
let coinValues = [];
for (let s in grouped) {
@ -155,12 +180,14 @@ function drawPieChart() {
for (let s in grouped) {
coinValues.push({ symbol: s, value: grouped[s] });
}
const ctx = pieChartCanvas.getContext("2d");
ctx.clearRect(0, 0, pieChartCanvas.width, pieChartCanvas.height);
const centerX = pieChartCanvas.width / 2;
const centerY = pieChartCanvas.height / 2;
const radius = Math.min(centerX, centerY) - 10;
let startAngle = 0;
coinValues.forEach(item => {
const sliceAngle = totalAll === 0 ? 0 : (item.value / totalAll) * 2 * Math.PI;
if (!coinColors[item.symbol]) {
@ -174,6 +201,7 @@ function drawPieChart() {
ctx.fill();
startAngle += sliceAngle;
});
renderChartLegend(coinValues, totalAll);
}
@ -191,6 +219,50 @@ function renderChartLegend(coinValues, totalAll) {
});
}
function openNoteModal(id, type) {
currentNoteId = id;
currentNoteType = type;
noteModal.style.display = "block";
if (type === 'portfolio') {
const tx = portfolioData.find(x => String(x.id) === String(id));
document.getElementById("noteText").value = tx?.note || "";
}
}
function closeNoteModal() {
noteModal.style.display = "none";
}
function saveNote() {
const noteText = document.getElementById("noteText").value;
const endpoint = currentNoteType === 'portfolio'
? "/api/portfolio/note"
: "/api/trade_summary/note";
fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
id: currentNoteId,
note: noteText
}),
credentials: "include"
})
.then(r => r.json())
.then(j => {
if (j.error) {
alert(j.error);
} else {
closeNoteModal();
updatePrices(); //
}
})
.catch(() => alert("Error saving note"));
}
function openAddTransactionModal() {
addTransactionModal.style.display = "block";
showBuyForm();
@ -216,10 +288,12 @@ function confirmBuy() {
const amount = parseFloat(document.getElementById("buyAmount").value);
const buyPrice = parseFloat(document.getElementById("buyPrice").value);
const buyDate = document.getElementById("buyDate").value;
if (!symbol || isNaN(amount) || isNaN(buyPrice)) {
alert("Bitte gültige Werte eingeben");
return;
}
const newId = Date.now() + Math.floor(Math.random() * 999999);
fetch("/api/coinPrice?symbol=" + symbol, { credentials: "include" })
.then(r => r.json())
@ -257,18 +331,22 @@ function confirmSell() {
const sel = document.getElementById("sellSelectTransaction");
const val = sel.value;
if (!val) return;
const sellAmount = parseFloat(document.getElementById("sellAmount").value);
const sellPrice = parseFloat(document.getElementById("sellPrice").value);
const sellDate = document.getElementById("sellDate").value;
if (isNaN(sellAmount) || isNaN(sellPrice) || !sellDate) {
alert("Bitte gültige Werte eingeben");
return;
}
const parts = val.split("|");
const sym = parts[0];
const idx = parseInt(parts[1], 10);
let selectedTransaction = null;
let count = 0;
for (let i = 0; i < portfolioData.length; i++) {
if (portfolioData[i].symbol === sym) {
if (count === idx) {
@ -278,6 +356,7 @@ function confirmSell() {
count++;
}
}
if (!selectedTransaction) {
alert("Ungültig");
return;
@ -314,6 +393,7 @@ function populateSellDropdown() {
if (!grouped[t.symbol]) grouped[t.symbol] = [];
grouped[t.symbol].push(t);
});
for (let sym in grouped) {
grouped[sym].forEach((tx, i) => {
const opt = document.createElement("option");
@ -348,13 +428,16 @@ function saveEditedTransaction() {
alert("Keine gültige Transaktion gefunden");
return;
}
const newAmount = parseFloat(document.getElementById("editAmount").value);
const newBuyPrice = parseFloat(document.getElementById("editBuyPrice").value);
const newDate = document.getElementById("editDate").value;
if (isNaN(newAmount) || isNaN(newBuyPrice)) {
alert("Bitte gültige Werte eingeben");
return;
}
fetch("/api/portfolio/edit", {
method: "POST",
headers: { "Content-Type": "application/json" },
@ -380,11 +463,25 @@ function saveEditedTransaction() {
}
function deleteTransaction() {
openDeleteConfirmationModal();
}
function openDeleteConfirmationModal() {
document.getElementById("deleteConfirmationModal").style.display = "block";
}
function closeDeleteConfirmationModal() {
document.getElementById("deleteConfirmationModal").style.display = "none";
}
function confirmDeleteTransaction() {
let t = portfolioData.find(x => String(x.id) === String(editIdGlobal));
if (!t) {
alert("Keine gültige Transaktion gefunden");
alert("No valid transaction found");
closeDeleteConfirmationModal();
return;
}
fetch("/api/portfolio/delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
@ -396,11 +493,12 @@ function deleteTransaction() {
if (j.error) {
alert(j.error);
} else {
closeDeleteConfirmationModal();
closeEditTransactionModal();
updatePrices();
}
})
.catch(() => alert("Fehler bei Delete"));
.catch(() => alert("Error deleting transaction"));
}
function openColorConfigModal() {
@ -417,12 +515,14 @@ function renderColorConfigList() {
container.innerHTML = "";
let syms = {};
portfolioData.forEach(t => { syms[t.symbol] = true; });
Object.keys(syms).forEach(s => {
const div = document.createElement("div");
div.style.marginBottom = "0.5rem";
const label = document.createElement("label");
label.textContent = s + ": ";
div.appendChild(label);
const input = document.createElement("input");
input.type = "color";
if (!coinColors[s]) {
@ -467,3 +567,4 @@ function loadCoinColorsFromStorage() {
function saveCoinColorsToStorage() {
localStorage.setItem("coinColors", JSON.stringify(coinColors));
}

View File

@ -17,7 +17,7 @@ document.querySelectorAll('.modal').forEach(modal => {
credentials: "same-origin"
}).then(response => {
if (response.redirected) {
window.location.href = response.url; // Leitet zur Login-Seite um
window.location.href = response.url;
}
}).catch(error => console.error("Logout-Fehler:", error));
}

View File

@ -1,6 +1,13 @@
let allTrades = [];
let allTrades = [];
let editTradeId = null;
let currentNoteId = null;
let currentNoteType = null;
document.addEventListener("DOMContentLoaded", () => {
noteModal = document.getElementById("noteModal");
window.addEventListener("click", (event) => {
if (event.target === noteModal) closeNoteModal();
});
loadTradeSummary();
});
@ -65,6 +72,9 @@ function renderTradeSummary(trades) {
let row = document.createElement("tr");
let profitClass = trade.profit >= 0 ? "positive" : "negative";
let invest = trade.buyPrice * trade.amount;
const editButton = `<span class="edit-button" onclick="openEditTradeModal('${trade.id}')">✎</span>`;
const noteButton = `<span class="note-button ${trade.note ? 'has-note' : 'no-note'}" onclick="openNoteModal('${trade.id}', 'trade')">✉</span>`;
row.innerHTML = `
<td>${trade.symbol}</td>
<td>${trade.amount}</td>
@ -74,7 +84,7 @@ function renderTradeSummary(trades) {
<td class="${profitClass}">${trade.profit.toFixed(2)}</td>
<td class="${profitClass}">${trade.percentProfit.toFixed(2)}%</td>
<td>${trade.buyDate}</td>
<td>${trade.sellDate}</td>
<td>${trade.sellDate}${editButton}${noteButton}</td>
`;
tbody.appendChild(row);
@ -132,6 +142,142 @@ function updateBottomBar(trades) {
document.getElementById("totalTradePercentChange").textContent = pctChange.toFixed(2) + "%";
}
function openNoteModal(id, type) {
currentNoteId = id;
currentNoteType = type;
document.getElementById("noteModal").style.display = "block";
const trade = allTrades.find(x => String(x.id) === String(id));
document.getElementById("noteText").value = trade?.note || "";
}
function closeNoteModal() {
document.getElementById("noteModal").style.display = "none";
}
function saveNote() {
const noteText = document.getElementById("noteText").value;
fetch("/api/trade_summary/note", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
id: currentNoteId,
note: noteText
}),
credentials: "include"
})
.then(r => r.json())
.then(j => {
if (j.error) {
alert(j.error);
} else {
closeNoteModal();
loadTradeSummary();
}
})
.catch(() => alert("Error saving note"));
}
function openEditTradeModal(id) {
editTradeId = id;
const trade = allTrades.find(t => String(t.id) === String(id));
if (!trade) {
alert("Invalid trade");
return;
}
document.getElementById("editTradeAmount").value = trade.amount;
document.getElementById("editTradeBuyPrice").value = trade.buyPrice;
document.getElementById("editTradeSellPrice").value = trade.sellPrice;
document.getElementById("editTradeBuyDate").value = trade.buyDate;
document.getElementById("editTradeSellDate").value = trade.sellDate;
document.getElementById("editTradeModal").style.display = "block";
}
function closeEditTradeModal() {
document.getElementById("editTradeModal").style.display = "none";
}
function saveEditedTrade() {
const trade = allTrades.find(t => String(t.id) === String(editTradeId));
if (!trade) {
alert("Invalid trade");
return;
}
const amount = parseFloat(document.getElementById("editTradeAmount").value);
const buyPrice = parseFloat(document.getElementById("editTradeBuyPrice").value);
const sellPrice = parseFloat(document.getElementById("editTradeSellPrice").value);
const buyDate = document.getElementById("editTradeBuyDate").value;
const sellDate = document.getElementById("editTradeSellDate").value;
if (isNaN(amount) || isNaN(buyPrice) || isNaN(sellPrice)) {
alert("Please enter valid values");
return;
}
fetch("/api/trade_summary/edit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
id: editTradeId,
amount,
buyPrice,
sellPrice,
buyDate,
sellDate
}),
credentials: "include"
})
.then(r => r.json())
.then(j => {
if (j.error) {
alert(j.error);
} else {
closeEditTradeModal();
loadTradeSummary();
}
})
.catch(() => alert("Error saving trade"));
}
function deleteTrade() {
openDeleteTradeConfirmationModal();
}
function openDeleteTradeConfirmationModal() {
document.getElementById("deleteTradeConfirmationModal").style.display = "block";
}
function closeDeleteTradeConfirmationModal() {
document.getElementById("deleteTradeConfirmationModal").style.display = "none";
}
function confirmDeleteTrade() {
fetch("/api/trade_summary/delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: editTradeId }),
credentials: "include"
})
.then(r => r.json())
.then(j => {
if (j.error) {
alert(j.error);
} else {
closeDeleteTradeConfirmationModal();
closeEditTradeModal();
loadTradeSummary();
}
})
.catch(() => alert("Error deleting trade"));
}
function openDateFilterModal() {
document.getElementById("dateFilterModal").style.display = "block";
}
@ -177,4 +323,4 @@ function applyDateFilter() {
renderTradeSummary(filtered);
updateBottomBar(filtered);
closeDateFilterModal();
}
}

View File

@ -1,4 +1,4 @@
const CURRENT_VERSION = "1.5.0";
const CURRENT_VERSION = "1.5.1";
function getUpdateUrl() {
return "/api/update?t=" + new Date().getTime();

View File

@ -22,6 +22,17 @@
.content {
flex: 1;
}
.note-button, .edit-button {
cursor: pointer;
margin-left: 5px;
font-size: 16px;
}
.note-button.has-note {
color: green;
}
.note-button.no-note {
color: red;
}
</style>
</head>
<body>
@ -122,14 +133,24 @@
<div class="modal-content">
<span class="close" onclick="closeEditTransactionModal()">&times;</span>
<h2>Edit / Delete Transaction</h2>
<label for="editSymbol">Symbol:</label>
<input type="text" id="editSymbol" disabled />
<label for="editAmount">Amount:</label>
<input type="number" step="0.000001" id="editAmount" />
<label for="editBuyPrice">Buy Price (USDT):</label>
<input type="number" step="0.01" id="editBuyPrice" />
<label for="editDate">Date:</label>
<input type="date" id="editDate" />
<div class="form-row">
<label for="editSymbol">Symbol:</label>
<input type="text" id="editSymbol" disabled />
</div>
<div class="form-row">
<label for="editAmount">Amount:</label>
<input type="number" step="0.000001" id="editAmount" />
</div>
<div class="form-row">
<label for="editBuyPrice">Buy Price (USDT):</label>
<input type="number" step="0.01" id="editBuyPrice" />
</div>
<div class="form-row">
<label for="editDate">Date:</label>
<input type="date" id="editDate" />
</div>
<div class="edit-buttons">
<button onclick="saveEditedTransaction()">Save</button>
<button onclick="deleteTransaction()">Delete</button>
@ -137,6 +158,17 @@
</div>
</div>
<div id="noteModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeNoteModal()">&times;</span>
<h2>Note</h2>
<div class="note-input-row">
<textarea id="noteText" rows="4" cols="50"></textarea>
<button onclick="saveNote()">Save</button>
</div>
</div>
</div>
<div id="colorConfigModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeColorConfigModal()">&times;</span>
@ -145,5 +177,16 @@
<button onclick="saveColorConfig()">OK</button>
</div>
</div>
<div id="deleteConfirmationModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeDeleteConfirmationModal()">&times;</span>
<h2>Delete Transaction</h2>
<p>Are you sure you want to delete this transaction?</p>
<div class="edit-buttons">
<button onclick="confirmDeleteTransaction()">Delete</button>
<button onclick="closeDeleteConfirmationModal()">Cancel</button>
</div>
</div>
</div>
</body>
</html>

View File

@ -7,6 +7,7 @@
<title>Portfolio: Trade Summary</title>
<link rel="stylesheet" href="css/portfolio.css" />
<script defer src="js/trade_summary.js"></script>
<style>
html, body {
height: 100%;
@ -21,6 +22,17 @@
.content {
flex: 1;
}
.note-button, .edit-button {
cursor: pointer;
margin-left: 5px;
font-size: 16px;
}
.note-button.has-note {
color: green;
}
.note-button.no-note {
color: red;
}
</style>
</head>
<body>
@ -32,8 +44,8 @@
</div>
<div class="grid-middle">
<button onclick="window.location.href='portfolio.html'">Live Portfolio</button>
<button>Trade Summary</button>
<button onclick="window.location.href='portfolio.html'">Add Transaction</button>
<button>Trade Summary</button>
</div>
<div class="grid-right">
<button onclick="window.location.href='/logout'">Logout</button>
@ -82,5 +94,59 @@
</div>
</div>
</div>
<div id="editTradeModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeEditTradeModal()">&times;</span>
<h2>Edit Trade</h2>
<div class="form-row">
<label for="editTradeAmount">Amount:</label>
<input type="number" step="0.000001" id="editTradeAmount" />
</div>
<div class="form-row">
<label for="editTradeBuyPrice">Buy Price (USDT):</label>
<input type="number" step="0.01" id="editTradeBuyPrice" />
</div>
<div class="form-row">
<label for="editTradeSellPrice">Sell Price (USDT):</label>
<input type="number" step="0.01" id="editTradeSellPrice" />
</div>
<div class="form-row">
<label for="editTradeBuyDate">Buy Date:</label>
<input type="date" id="editTradeBuyDate" />
</div>
<div class="form-row">
<label for="editTradeSellDate">Sell Date:</label>
<input type="date" id="editTradeSellDate" />
</div>
<div class="edit-buttons">
<button onclick="saveEditedTrade()">Save</button>
<button onclick="deleteTrade()">Delete</button>
</div>
</div>
</div>
<div id="noteModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeNoteModal()">&times;</span>
<h2>Note</h2>
<div class="note-input-row">
<textarea id="noteText" rows="4" cols="50"></textarea>
<button onclick="saveNote()">Save</button>
</div>
</div>
</div>
<div id="deleteTradeConfirmationModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeDeleteTradeConfirmationModal()">&times;</span>
<h2>Delete Trade</h2>
<p>Are you sure you want to delete this trade?</p>
<div class="edit-buttons">
<button onclick="confirmDeleteTrade()">Delete</button>
<button onclick="closeDeleteTradeConfirmationModal()">Cancel</button>
</div>
</div>
</div>
</body>
</html>

61
server/alarms.js Normal file
View File

@ -0,0 +1,61 @@
const express = require("express");
const fs = require("fs");
const path = require("path");
const router = express.Router();
const DATA_FILE = path.join(__dirname, "..", "data", "data.json");
function readData() {
return JSON.parse(fs.readFileSync(DATA_FILE, "utf8"));
}
function writeData(data) {
fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2));
}
router.get("/", (req, res) => {
try {
const data = readData();
res.json(data.alarms);
} catch (err) {
res.status(500).json({ error: "Fehler beim Lesen der Daten" });
}
});
router.post("/", (req, res) => {
const { symbol, price, frequency, direction } = req.body;
if (!symbol || !price) {
return res.status(400).json({ error: "Symbol und Preis sind erforderlich." });
}
try {
const data = readData();
const newAlarm = {
id: Date.now(),
symbol: symbol.toUpperCase(),
price: parseFloat(price),
frequency: frequency || "Once",
direction: direction || "Rising",
triggered: false
};
data.alarms.push(newAlarm);
writeData(data);
res.json(newAlarm);
} catch (err) {
res.status(500).json({ error: "Fehler beim Schreiben der Daten" });
}
});
router.delete("/:id", (req, res) => {
const alarmId = parseInt(req.params.id, 10);
try {
const data = readData();
data.alarms = data.alarms.filter(a => a.id !== alarmId);
writeData(data);
res.json({ success: true, alarms: data.alarms });
} catch (err) {
res.status(500).json({ error: "Fehler beim Löschen des Alarms" });
}
});
module.exports = router;

View File

@ -10,7 +10,7 @@ const fetch = require("node-fetch");
/*
* Metadata
* Version: 1.5.0
* Version: 1.5.1
* Author/Dev: Gerald Hasani
* Name: HodlEye Crypto Price Tracker
* Email: contact@gerald-hasani.com
@ -58,6 +58,10 @@ app.get("/logout", (req, res) => {
res.redirect("/login");
});
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });
const clients = new Set();
@ -183,43 +187,97 @@ app.put("/api/cryptos", (req, res) => {
res.json({ success: true, cryptos: data.cryptos });
});
app.get("/api/alarms", (req, res) => {
const data = readData();
res.json(data.alarms);
/* ------------- Notizen  ------------- */
app.post("/api/portfolio/note", (req, res) => {
const { id, note } = req.body;
const pf = readPortfolio();
const tx = pf.transactions.find((x) => String(x.id) === String(id));
if (!tx) return res.status(400).json({ error: "Invalid ID" });
tx.note = note;
writePortfolio(pf);
res.json({ success: true });
});
app.post("/api/alarms", (req, res) => {
const { symbol, price, frequency, direction } = req.body;
if (!symbol || !price) {
return res.status(400).json({ error: "symbol and price are required." });
}
const data = readData();
const newAlarm = {
id: Date.now(),
symbol: symbol.toUpperCase(),
price: parseFloat(price),
frequency: frequency || "Once",
direction: direction || "Rising",
triggered: false
};
data.alarms.push(newAlarm);
writeData(data);
res.json(newAlarm);
app.post("/api/trade_summary/note", (req, res) => {
const { id, note } = req.body;
const ts = readTradeSummary();
const trade = ts.trades.find((x) => String(x.id) === String(id));
if (!trade) return res.status(400).json({ error: "Invalid ID" });
trade.note = note;
writeTradeSummary(ts);
res.json({ success: true });
});
app.delete("/api/alarms/:id", (req, res) => {
const alarmId = parseInt(req.params.id, 10);
const data = readData();
data.alarms = data.alarms.filter(a => a.id !== alarmId);
writeData(data);
res.json({ success: true, alarms: data.alarms });
});
const alarmsRouter = require("./alarms");
app.use("/api/alarms", alarmsRouter);
app.get("/api/notifications", (req, res) => {
const data = readData();
res.json(data.notifications);
});
app.post("/api/portfolio/note", (req, res) => {
const { id, note } = req.body;
const pf = readPortfolio();
const tx = pf.transactions.find(x => String(x.id) === String(id));
if (!tx) {
return res.status(400).json({ error: "Invalid ID" });
}
tx.note = note;
writePortfolio(pf);
res.json({ success: true });
});
app.post("/api/trade_summary/note", (req, res) => {
const { id, note } = req.body;
const ts = readTradeSummary();
const trade = ts.trades.find(x => String(x.id) === String(id));
if (!trade) {
return res.status(400).json({ error: "Invalid ID" });
}
trade.note = note;
writeTradeSummary(ts);
res.json({ success: true });
});
app.post("/api/trade_summary/edit", (req, res) => {
const { id, amount, buyPrice, sellPrice, buyDate, sellDate } = req.body;
const ts = readTradeSummary();
const trade = ts.trades.find(x => String(x.id) === String(id));
if (!trade) {
return res.status(400).json({ error: "Invalid ID" });
}
trade.amount = parseFloat(amount);
trade.buyPrice = parseFloat(buyPrice);
trade.sellPrice = parseFloat(sellPrice);
trade.buyDate = buyDate;
trade.sellDate = sellDate;
trade.profit = (trade.sellPrice - trade.buyPrice) * trade.amount;
trade.percentProfit = trade.buyPrice > 0
? (trade.profit / (trade.buyPrice * trade.amount)) * 100
: 0;
writeTradeSummary(ts);
res.json({ success: true });
});
app.post("/api/trade_summary/delete", (req, res) => {
const { id } = req.body;
const ts = readTradeSummary();
const before = ts.trades.length;
ts.trades = ts.trades.filter(t => String(t.id) !== String(id));
if (ts.trades.length === before) {
return res.status(400).json({ error: "No trade with given ID" });
}
writeTradeSummary(ts);
res.json({ success: true });
});
app.post("/api/notifications", (req, res) => {
const { message } = req.body;
if (!message) {
@ -323,6 +381,53 @@ app.get("/api/portfolio", (req, res) => {
res.json(data);
});
app.post("/api/portfolio/buy", async (req, res) => {
const { id, symbol, amount, buyPrice, date } = req.body;
if (!symbol || !amount || !buyPrice) {
return res.status(400).json({ error: "Invalid data" });
}
const upSym = symbol.toUpperCase();
const binSup = await binanceSupported(upSym);
const okxSup = await okxSupported(upSym);
if (!binSup && !okxSup) {
return res.status(400).json({ error: "Coin not supported" });
}
let realPrice;
if (binSup) {
try {
realPrice = await getBinancePrice(upSym);
} catch {
realPrice = parseFloat(buyPrice);
}
} else if (okxSup) {
try {
realPrice = await getOkxPrice(upSym);
} catch {
realPrice = parseFloat(buyPrice);
}
} else {
realPrice = parseFloat(buyPrice);
}
const pf = readPortfolio();
const transactionId = id ? String(id) : String(Date.now() + Math.floor(Math.random() * 999999));
pf.transactions.push({
id: transactionId,
symbol: upSym,
amount: parseFloat(amount),
buyPrice: parseFloat(buyPrice),
date: date || new Date().toISOString().split("T")[0],
currentPrice: realPrice,
note: ""
});
writePortfolio(pf);
res.json({ success: true });
});
app.get("/api/portfolio", (req, res) => {
const data = readPortfolio();
res.json(data);
});
app.post("/api/portfolio/buy", async (req, res) => {
const { id, symbol, amount, buyPrice, date } = req.body;
if (!symbol || !amount || !buyPrice) {
@ -364,10 +469,12 @@ app.post("/api/portfolio/buy", async (req, res) => {
res.json({ success: true });
});
app.get("/api/portfolio/prices", async (req, res) => {
const pf = readPortfolio();
let result = [];
let changed = false;
for (let t of pf.transactions) {
if (!t.id) {
t.id = String(Date.now() + Math.floor(Math.random() * 999999));
@ -375,29 +482,29 @@ app.get("/api/portfolio/prices", async (req, res) => {
} else {
t.id = String(t.id);
}
let price = t.currentPrice;
try {
const binSup = await binanceSupported(t.symbol);
const okxSup = await okxSupported(t.symbol);
if (binSup) {
price = await getBinancePrice(t.symbol);
} else if (okxSup) {
price = await getOkxPrice(t.symbol);
}
t.currentPrice = price;
} catch {}
if (binSup) t.currentPrice = await getBinancePrice(t.symbol);
else if (okxSup) t.currentPrice = await getOkxPrice(t.symbol);
} catch {
}
result.push({
id: t.id,
symbol: t.symbol,
amount: t.amount,
buyPrice: t.buyPrice,
date: t.date,
currentPrice: t.currentPrice
currentPrice: t.currentPrice,
note: t.note,
});
}
if (changed) {
writePortfolio(pf);
}
if (changed) writePortfolio(pf);
res.json(result);
});
@ -415,6 +522,52 @@ app.post("/api/portfolio/edit", (req, res) => {
res.json({ success: true });
});
app.get("/api/trade_summary", (req, res) => {
const data = readTradeSummary();
data.trades.sort((a, b) => new Date(b.sellDate) - new Date(a.sellDate));
res.json(data.trades);
});
app.post("/api/portfolio/sell", (req, res) => {
const { id, sellAmount, sellPrice, sellDate } = req.body;
if (!id || isNaN(sellAmount) || isNaN(sellPrice) || !sellDate) {
return res.status(400).json({ error: "Ungültige Daten" });
}
const pf = readPortfolio();
let txIndex = pf.transactions.findIndex(x => String(x.id) === String(id));
if (txIndex === -1) {
return res.status(400).json({ error: "Transaktion nicht gefunden" });
}
let tx = pf.transactions[txIndex];
if (sellAmount > tx.amount) {
return res.status(400).json({ error: "Verkaufsmenge überschreitet die vorhandene Menge" });
}
const profit = (parseFloat(sellPrice) - parseFloat(tx.buyPrice)) * sellAmount;
const percentProfit = tx.buyPrice > 0 ? (profit / (tx.buyPrice * sellAmount)) * 100 : 0;
const tradeEntry = {
id: Date.now(),
symbol: tx.symbol,
amount: sellAmount,
buyPrice: tx.buyPrice,
sellPrice: parseFloat(sellPrice),
profit: profit,
percentProfit: percentProfit,
buyDate: tx.date,
sellDate: sellDate,
note: tx.note || ""
};
if (sellAmount < tx.amount) {
tx.amount = tx.amount - sellAmount;
} else {
pf.transactions.splice(txIndex, 1);
}
writePortfolio(pf);
const tsData = readTradeSummary();
tsData.trades.push(tradeEntry);
writeTradeSummary(tsData);
res.json({ success: true, trade: tradeEntry });
});
app.post("/api/portfolio/delete", (req, res) => {
const { id } = req.body;
const pf = readPortfolio();
@ -474,6 +627,61 @@ app.get("/api/trade_summary", (req, res) => {
app.use(express.static(path.join(__dirname, "..", "public")));
function readTradeSummary() {
try {
const content = fs.readFileSync(TRADE_SUMMARY_FILE, "utf8");
const data = JSON.parse(content);
if (data.trades) {
data.trades.forEach(trade => {
if (!trade.hasOwnProperty('note')) {
trade.note = "";
}
});
}
return data;
} catch (err) {
console.error("Error reading trade summary:", err);
return { trades: [] };
}
}
function writeTradeSummary(data) {
try {
fs.writeFileSync(TRADE_SUMMARY_FILE, JSON.stringify(data, null, 2));
} catch (err) {
console.error("Error writing trade summary:", err);
}
}
function readPortfolio() {
try {
const content = fs.readFileSync(PORTFOLIO_FILE, "utf8");
const data = JSON.parse(content);
if (data.transactions) {
data.transactions.forEach(tx => {
if (!tx.hasOwnProperty('note')) {
tx.note = "";
}
});
}
return data;
} catch (err) {
console.error("Error reading portfolio:", err);
return { transactions: [] };
}
}
function writePortfolio(data) {
try {
fs.writeFileSync(PORTFOLIO_FILE, JSON.stringify(data, null, 2));
} catch (err) {
console.error("Error writing portfolio:", err);
}
}
wss.on("connection", (ws) => {
clients.add(ws);
ws.on("close", () => {

View File

@ -1,8 +1,8 @@
{
"version": "1.5.0",
"version": "1.5.1",
"changelog": [
"Adding Portfolio Page",
"Fixing Alarm Layout with Category",
"Automatically delete alarms that have expired"
"Adding Note Option in Portfolio Page",
"Fixing Editing Option in Trade Summary"
]
}