Compare commits

..

No commits in common. "819f36f26feaf61f3176c121c32395dfdc800f86" and "214bb5c3d1e06330f9f004f1a1ddde9a420afdda" have entirely different histories.

12 changed files with 67 additions and 734 deletions

View File

@ -1,19 +0,0 @@
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,26 +205,3 @@ 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> </button>
<div class="version-info"> <div class="version-info">
<span id="currentVersion">Version 1.5.1</span> <span id="currentVersion">Version 1.5.0</span>
<span <span
id="updateAvailable" id="updateAvailable"
style="display: none; color: red; cursor: pointer" style="display: none; color: red; cursor: pointer"

View File

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

View File

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

View File

@ -1,13 +1,6 @@
let allTrades = []; let allTrades = [];
let editTradeId = null;
let currentNoteId = null;
let currentNoteType = null;
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
noteModal = document.getElementById("noteModal");
window.addEventListener("click", (event) => {
if (event.target === noteModal) closeNoteModal();
});
loadTradeSummary(); loadTradeSummary();
}); });
@ -72,9 +65,6 @@ function renderTradeSummary(trades) {
let row = document.createElement("tr"); let row = document.createElement("tr");
let profitClass = trade.profit >= 0 ? "positive" : "negative"; let profitClass = trade.profit >= 0 ? "positive" : "negative";
let invest = trade.buyPrice * trade.amount; 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 = ` row.innerHTML = `
<td>${trade.symbol}</td> <td>${trade.symbol}</td>
<td>${trade.amount}</td> <td>${trade.amount}</td>
@ -84,7 +74,7 @@ function renderTradeSummary(trades) {
<td class="${profitClass}">${trade.profit.toFixed(2)}</td> <td class="${profitClass}">${trade.profit.toFixed(2)}</td>
<td class="${profitClass}">${trade.percentProfit.toFixed(2)}%</td> <td class="${profitClass}">${trade.percentProfit.toFixed(2)}%</td>
<td>${trade.buyDate}</td> <td>${trade.buyDate}</td>
<td>${trade.sellDate}${editButton}${noteButton}</td> <td>${trade.sellDate}</td>
`; `;
tbody.appendChild(row); tbody.appendChild(row);
@ -142,142 +132,6 @@ function updateBottomBar(trades) {
document.getElementById("totalTradePercentChange").textContent = pctChange.toFixed(2) + "%"; 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() { function openDateFilterModal() {
document.getElementById("dateFilterModal").style.display = "block"; document.getElementById("dateFilterModal").style.display = "block";
} }

View File

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

View File

@ -22,17 +22,6 @@
.content { .content {
flex: 1; 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> </style>
</head> </head>
<body> <body>
@ -133,24 +122,14 @@
<div class="modal-content"> <div class="modal-content">
<span class="close" onclick="closeEditTransactionModal()">&times;</span> <span class="close" onclick="closeEditTransactionModal()">&times;</span>
<h2>Edit / Delete Transaction</h2> <h2>Edit / Delete Transaction</h2>
<label for="editSymbol">Symbol:</label>
<div class="form-row"> <input type="text" id="editSymbol" disabled />
<label for="editSymbol">Symbol:</label> <label for="editAmount">Amount:</label>
<input type="text" id="editSymbol" disabled /> <input type="number" step="0.000001" id="editAmount" />
</div> <label for="editBuyPrice">Buy Price (USDT):</label>
<div class="form-row"> <input type="number" step="0.01" id="editBuyPrice" />
<label for="editAmount">Amount:</label> <label for="editDate">Date:</label>
<input type="number" step="0.000001" id="editAmount" /> <input type="date" id="editDate" />
</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"> <div class="edit-buttons">
<button onclick="saveEditedTransaction()">Save</button> <button onclick="saveEditedTransaction()">Save</button>
<button onclick="deleteTransaction()">Delete</button> <button onclick="deleteTransaction()">Delete</button>
@ -158,17 +137,6 @@
</div> </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="colorConfigModal" class="modal"> <div id="colorConfigModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<span class="close" onclick="closeColorConfigModal()">&times;</span> <span class="close" onclick="closeColorConfigModal()">&times;</span>
@ -177,16 +145,5 @@
<button onclick="saveColorConfig()">OK</button> <button onclick="saveColorConfig()">OK</button>
</div> </div>
</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> </body>
</html> </html>

View File

@ -7,7 +7,6 @@
<title>Portfolio: Trade Summary</title> <title>Portfolio: Trade Summary</title>
<link rel="stylesheet" href="css/portfolio.css" /> <link rel="stylesheet" href="css/portfolio.css" />
<script defer src="js/trade_summary.js"></script> <script defer src="js/trade_summary.js"></script>
<style> <style>
html, body { html, body {
height: 100%; height: 100%;
@ -22,17 +21,6 @@
.content { .content {
flex: 1; 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> </style>
</head> </head>
<body> <body>
@ -44,8 +32,8 @@
</div> </div>
<div class="grid-middle"> <div class="grid-middle">
<button onclick="window.location.href='portfolio.html'">Live Portfolio</button> <button onclick="window.location.href='portfolio.html'">Live Portfolio</button>
<button onclick="window.location.href='portfolio.html'">Add Transaction</button>
<button>Trade Summary</button> <button>Trade Summary</button>
<button onclick="window.location.href='portfolio.html'">Add Transaction</button>
</div> </div>
<div class="grid-right"> <div class="grid-right">
<button onclick="window.location.href='/logout'">Logout</button> <button onclick="window.location.href='/logout'">Logout</button>
@ -94,59 +82,5 @@
</div> </div>
</div> </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> </body>
</html> </html>

View File

@ -1,61 +0,0 @@
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 * Metadata
* Version: 1.5.1 * Version: 1.5.0
* Author/Dev: Gerald Hasani * Author/Dev: Gerald Hasani
* Name: HodlEye Crypto Price Tracker * Name: HodlEye Crypto Price Tracker
* Email: contact@gerald-hasani.com * Email: contact@gerald-hasani.com
@ -58,10 +58,6 @@ app.get("/logout", (req, res) => {
res.redirect("/login"); res.redirect("/login");
}); });
const server = http.createServer(app); const server = http.createServer(app);
const wss = new WebSocket.Server({ server }); const wss = new WebSocket.Server({ server });
const clients = new Set(); const clients = new Set();
@ -187,97 +183,43 @@ app.put("/api/cryptos", (req, res) => {
res.json({ success: true, cryptos: data.cryptos }); res.json({ success: true, cryptos: data.cryptos });
}); });
/* ------------- Notizen  ------------- */ app.get("/api/alarms", (req, res) => {
const data = readData();
app.post("/api/portfolio/note", (req, res) => { res.json(data.alarms);
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) => { app.post("/api/alarms", (req, res) => {
const { id, note } = req.body; const { symbol, price, frequency, direction } = req.body;
const ts = readTradeSummary(); if (!symbol || !price) {
const trade = ts.trades.find((x) => String(x.id) === String(id)); return res.status(400).json({ error: "symbol and price are required." });
if (!trade) return res.status(400).json({ error: "Invalid ID" }); }
trade.note = note; const data = readData();
writeTradeSummary(ts); const newAlarm = {
res.json({ success: true }); 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);
}); });
const alarmsRouter = require("./alarms"); app.delete("/api/alarms/:id", (req, res) => {
app.use("/api/alarms", alarmsRouter); 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 });
});
app.get("/api/notifications", (req, res) => { app.get("/api/notifications", (req, res) => {
const data = readData(); const data = readData();
res.json(data.notifications); 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) => { app.post("/api/notifications", (req, res) => {
const { message } = req.body; const { message } = req.body;
if (!message) { if (!message) {
@ -381,53 +323,6 @@ app.get("/api/portfolio", (req, res) => {
res.json(data); 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) => { app.post("/api/portfolio/buy", async (req, res) => {
const { id, symbol, amount, buyPrice, date } = req.body; const { id, symbol, amount, buyPrice, date } = req.body;
if (!symbol || !amount || !buyPrice) { if (!symbol || !amount || !buyPrice) {
@ -469,12 +364,10 @@ app.post("/api/portfolio/buy", async (req, res) => {
res.json({ success: true }); res.json({ success: true });
}); });
app.get("/api/portfolio/prices", async (req, res) => { app.get("/api/portfolio/prices", async (req, res) => {
const pf = readPortfolio(); const pf = readPortfolio();
let result = []; let result = [];
let changed = false; let changed = false;
for (let t of pf.transactions) { for (let t of pf.transactions) {
if (!t.id) { if (!t.id) {
t.id = String(Date.now() + Math.floor(Math.random() * 999999)); t.id = String(Date.now() + Math.floor(Math.random() * 999999));
@ -482,29 +375,29 @@ app.get("/api/portfolio/prices", async (req, res) => {
} else { } else {
t.id = String(t.id); t.id = String(t.id);
} }
let price = t.currentPrice;
try { try {
const binSup = await binanceSupported(t.symbol); const binSup = await binanceSupported(t.symbol);
const okxSup = await okxSupported(t.symbol); const okxSup = await okxSupported(t.symbol);
if (binSup) t.currentPrice = await getBinancePrice(t.symbol); if (binSup) {
else if (okxSup) t.currentPrice = await getOkxPrice(t.symbol); price = await getBinancePrice(t.symbol);
} catch { } else if (okxSup) {
price = await getOkxPrice(t.symbol);
} }
t.currentPrice = price;
} catch {}
result.push({ result.push({
id: t.id, id: t.id,
symbol: t.symbol, symbol: t.symbol,
amount: t.amount, amount: t.amount,
buyPrice: t.buyPrice, buyPrice: t.buyPrice,
date: t.date, date: t.date,
currentPrice: t.currentPrice, currentPrice: t.currentPrice
note: t.note,
}); });
} }
if (changed) {
if (changed) writePortfolio(pf); writePortfolio(pf);
}
res.json(result); res.json(result);
}); });
@ -522,52 +415,6 @@ app.post("/api/portfolio/edit", (req, res) => {
res.json({ success: true }); 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) => { app.post("/api/portfolio/delete", (req, res) => {
const { id } = req.body; const { id } = req.body;
const pf = readPortfolio(); const pf = readPortfolio();
@ -627,61 +474,6 @@ app.get("/api/trade_summary", (req, res) => {
app.use(express.static(path.join(__dirname, "..", "public"))); 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) => { wss.on("connection", (ws) => {
clients.add(ws); clients.add(ws);
ws.on("close", () => { ws.on("close", () => {

View File

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