Compare commits

...

4 Commits

Author SHA1 Message Date
Gerald-H
e2d93733a4
Update README.md 2025-06-14 19:04:04 +02:00
Gerald-H
b3809fae16
Update README.md 2025-06-14 19:02:05 +02:00
f0481bffec Update 1.6.0 2025-06-14 18:53:03 +02:00
f03b405691 Update 1.6.0
"Revamped Alarm Modal with grouped alarms per coin and cleaner layout",
    "Improved alignment and responsiveness of alarm input fields",
    "Compact alarm boxes with hover effects and subtle delete buttons",
    "Alarm modal now retains focus and resets fields after adding an alarm",
    "Enter key submits alarm from any input field",
    "Prevents duplicate alarms for same coin/price",
    "Alarm titles now displayed with center-aligned divider lines",
    "Alarm logic improved: server-side checks even without browser",
    "Recurring alarms now persist and trigger correctly",
    "Alarms are sorted and grouped by symbol and price",
    "Last price check is stored per alarm to avoid threshold skips",
    "API improved: endpoints usable for external tools (e.g., Windows app)",
    "Better error handling and logging for API actions",
    "Windows App UI redesigned: modern English layout, centered elements",
    "Tray icon with context menu (Open/Exit) and minimize/close behavior",
    "All texts and notifications are now in English",
    "Auto-reconnect and connection recovery implemented",
    "Notification appears if disconnected for more than 2 minutes",
    "Resolved issue where HodlEye_Notify could not receive alarms from the 'HodlEye-Crypto-Price-Tracker' Docker container when no browser session was active"
2025-06-14 18:36:31 +02:00
14 changed files with 1038 additions and 248 deletions

121
README.md
View File

@ -1,4 +1,4 @@
# HodlEye Crypto Portfolio & Price Tracker
# HodlEye Crypto Price Tracker
A lightweight Docker-based web tool to monitor cryptocurrency prices (via Binance and OKX) with **unlimited alarms** and **unlimited crypto tracking**, outshining typical TradingView limitations. It also provides quick access to multiple RSS-based crypto news sources and a live Economic Calendar.
@ -6,7 +6,9 @@ A lightweight Docker-based web tool to monitor cryptocurrency prices (via Binanc
## Demo
Check out the live demo here: [HodlEye Demo](https://hodleye.gerald-hasani.com/)
Visit [HodlEye Demo](https://hodleye.gerald-hasani.com)
**Default Login Data**
**User:** admin
@ -25,20 +27,18 @@ Check out the live demo here: [HodlEye Demo](https://hodleye.gerald-hasani.com/)
- [Alarm Functionality](#alarm-functionality)
- [Crypto News](#crypto-news)
- [Economic Calendar](#economic-calendar)
- [TradingView Chart](#tradingview-chart)
3. [Portfolio Management](#portfolio-management)
4. [Installation & Usage](#installation--usage)
- [TradingView Chart](#tradingview-chart)
3. [Installation & Usage](#installation--usage)
- [Requirements](#requirements)
- [Environment variables (.env)](#environment-variables-env)
- [Docker Build & Run](#docker-build--run)
5. [Windows Notification App: HodlEye_Notify](#windows-notification-app-hodleye_notify)
6. [Project Structure](#project-structure)
4. [Windows Notification App: HodlEye_Notify](#windows-notification-app-hodleye_notify)
5. [Project Structure](#project-structure)
- [Frontend (index.html & magic.js)](#frontend-indexhtml--magicjs)
- [News Feed Server (Node.js)](#news-feed-server-nodejs)
7. [Important Notes / Limitations](#important-notes--limitations)
8. [Coming Soon](#coming-soon)
9. [Privacy & Data Disclaimer](#privacy--data-disclaimer)
10. [License](#license)
6. [Important Notes / Limitations](#important-notes--limitations)
7. [Coming Soon](#coming-soon)
8. [Privacy & Data Disclaimer](#privacy--data-disclaimer)
9. [License](#license)
---
@ -115,39 +115,8 @@ The tool refreshes prices every **1 seconds**, which may introduce a slight dela
- The **TradingView Chart** Crypto Box Currency click opens a modal with a Tradingview Chart Window iframe, get a better overview of the charts.
<img src="https://github.com/user-attachments/assets/53bd1553-7679-40c1-afa8-0330cd28a71b" width="600" height="auto">
&nbsp;
### Login Screen
- The **Login Screen** provides a certain level of security from prying eyes.
<img src="https://github.com/user-attachments/assets/03ff8333-78fc-49f6-b794-f6698546ab49" width="500" height="auto">
---
## Portfolio Management
<img src="https://github.com/user-attachments/assets/afea1c01-2016-46b1-b800-bafbf6c43351" width="500" height="auto">
<img src="https://github.com/user-attachments/assets/933ea5ad-d480-4bc8-b142-5c607eea956b" width="500" height="auto">
&nbsp;
HodlEye includes robust portfolio management features to help you monitor and analyze your cryptocurrency investments:
- **Live Portfolio**:
View your active investments in real-time. This page displays essential details such as the coin symbol, amount, buy price, current price, invested amount (calculated as _amount × buy price_), profit/loss, percentage change, and buy date.
The charts in “Portfolio Live” are updated every 10 seconds
**Important:** When opening the Live Portfolio page, expect a 1-3 second delay while data is recalculated in real-time. Additionally, the live chart is refreshed every 10 seconds to ensure up-to-date pricing information.
- **Trade Summary**:
This section provides a comprehensive breakdown of your closed trades, showing the actual profits or losses realized upon selling your assets. It includes information like the coin symbol, amount, buy price, invested amount, sell price, profit, percentage profit, buy date, and sell date. A date filter is available to help you analyze trade performance over specific time ranges.
The bottom bar in the Trade Summary page displays the cumulative invested amount, overall profit/loss, and overall percentage change.
These portfolio features enable you to have a clear, up-to-date overview of both your active and completed investments, empowering you to make informed trading decisions.
---
&nbsp;
## Installation & Usage
### Requirements
@ -155,28 +124,6 @@ These portfolio features enable you to have a clear, up-to-date overview of both
- [Docker](https://www.docker.com/) installed.
- (Optional) [Docker-Compose](https://docs.docker.com/compose/) if you want a more complex or multi-container setup.
&nbsp;
---
### Environment Variables (.env)
You can store the username, password, and a secret key in the `.env` file to protect the application from unauthorized access. **Make sure to change the default credentials to your own secure values**:
```
LOGIN_USER=admin
LOGIN_PASS=admin
SESSION_SECRET=some_secret_key
```
- **LOGIN_USER**: The username for logging into the application.
- **LOGIN_PASS**: The password for logging into the application.
- **SESSION_SECRET**: A random, secret value to secure sessions.
After building and starting the container, a login prompt will appear when accessing the application, ensuring that only authorized users can proceed.
&nbsp;
---
### Docker Build & Run
1. **Clone this repository**
@ -256,7 +203,6 @@ Below is an example directory tree (based on your structure). Yours may vary sli
```
HodlEye-Crypto-Price-Tracker
├── .env
├── Dockerfile
├── LICENSE.txt
├── PRIVACY.md
@ -264,11 +210,6 @@ HodlEye-Crypto-Price-Tracker
├── data
│ └── data.json
├── public
│ ├── css
│ │ ├── login.css
│ │ ├── portfolio.css
│ │ ├── responsive.css
│ │ └── style.css
│ ├── font
│ │ └── BreeSerif-Regular.ttf
│ ├── images
@ -277,22 +218,23 @@ HodlEye-Crypto-Price-Tracker
│ │ ├── favicon.png
│ │ └── github-mark.svg
│ ├── index.html
│ ├── js
│ │ ├── magic.js
│ │ ├── news.js
│ │ ├── portfolio.js
│ │ ├── script.js
│ │ ├── trade_summary.js
│ │ ├── tradingview.js
│ │ └── update.js
│ ├── login.html
│ ├── portfolio.html
│ ├── magic.js
│ ├── news.js
│ ├── tradingview.js
│ ├── script.js
│ ├── responsive.css
│ ├── sound
│ │ ├── cashing.mp3
│ │ └── ping.mp3
│ └── trade_summary.html
│ ├── style.css
│ └── update.js
├── server
│ ├── newsfeed
│ │ ├── node_modules
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ └── server.js
│ ├── node_modules
│ ├── package-lock.json
│ ├── package.json
│ └── server.js
@ -300,7 +242,6 @@ HodlEye-Crypto-Price-Tracker
│ ├── cashing.mp3
│ └── ping.mp3
└── update.json
```
&nbsp;
@ -365,13 +306,14 @@ _(Within Docker, its already bundled, so just expose `5001`.)_
---
&nbsp;
## Upcoming planned changes with the next versions
## Coming Soon
Exciting new features and improvements are on the way! Here are some planned updates:
- **Big Movement Alarm**: Alarm function for rapid short or long events.
- **Android**: Android app with synchronization option to HodlEye Docker (First early alpha already available internally)
- **Portfolio Management**: Track your crypto holdings in real-time with easy-to-read analytics.
- **More Integrations**: Expanding support for additional exchanges and data sources.
- **Automatic alarm in the event of a sharp price drop of 10% within 1 hour**
- **HodlEye Notify Alarm with various sound selections and HodlEye Alarms**
- **Windows HodlEye Notify Update**: Windows app bugfix and updates
Stay tuned for updates!
@ -387,12 +329,9 @@ Stay tuned for updates!
- **External Services**: Certain features (e.g., news feeds, iframes) rely on third-party websites or APIs. We do not control and are not responsible for the data-collection practices or privacy policies of these external providers. Please refer to the privacy policies of those services for details.
---
&nbsp;
## 🛡️ License
Custom Non-Commercial License. See `LICENSE` file for details.
---
&nbsp;

View File

@ -238,10 +238,9 @@ body.light .crypto-box {
#buyMeModal .modal-content {
width: 30%;
width: 400px;
max-width: 90%;
overflow-x: hidden;
text-align: center;
position: absolute;
top: 35%;
@ -251,9 +250,12 @@ body.light .crypto-box {
flex-direction: column;
align-items: center;
justify-content: center;
padding: 30px;
border-radius: 15px;
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}
#buyMeModal .close {
position: absolute;
top: 15px;
@ -314,6 +316,7 @@ body.light .crypto-box {
position: absolute;
top: 35%;
left: 50%;
max-width: 100%;
transform: translate(-50%, -45%);
flex-direction: column;
@ -347,6 +350,35 @@ body.light .crypto-box {
cursor: pointer;
}
#alarmModal .modal-content h3 {
display: flex;
align-items: center;
justify-content: center;
font-size: 1.15em;
font-weight: bold;
margin: 18px 0 8px 0;
color: #00bfff;
padding-bottom: 6px;
letter-spacing: 1px;
}
#alarmModal .modal-content h3::before,
#alarmModal .modal-content h3::after {
content: "";
flex: 1;
border-bottom: 1.5px solid #444;
margin: 0 12px;
opacity: 0.5;
}
body.light #alarmModal .modal-content h3 {
color: #0077b6;
}
body.light #alarmModal .modal-content h3::before,
body.light #alarmModal .modal-content h3::after {
border-bottom: 1.5px solid #bbb;
}
@ -453,21 +485,80 @@ body.light .api-label {
.alarm-list-container {
display: grid;
grid-template-columns: repeat(2, 2fr);
gap: 10px;
max-height: 300px;
display: block;
max-height: 200px;
overflow-y: auto;
margin: 1em 0;
}
.alarm-group {
margin-bottom: 10px;
}
.alarm-group-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
#alarmModal .modal-content .alarm-row {
display: flex;
gap: 24px;
align-items: flex-start;
margin-bottom: 10px;
justify-content: center;
}
#alarmModal .modal-content .alarm-row .alarm-col {
flex: 1 1 0;
min-width: 180px;
max-width: 240px;
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: flex-start;
}
#alarmModal .modal-content .alarm-row label {
margin-bottom: 4px;
font-size: 15px;
font-weight: 500;
text-align: left;
padding-left: 6px;
}
#alarmModal .modal-content .alarm-row select,
#alarmModal .modal-content .alarm-row input {
width: 100%;
min-width: 120px;
max-width: 240px;
box-sizing: border-box;
margin-bottom: 0;
font-size: 15px;
}
.alarm-item {
display: flex;
align-items: center;
justify-content: space-between;
background-color: #333;
padding: 6px;
padding: 3px 8px;
border-radius: 3px;
font-size: 14px;
min-height: 28px;
transition: background 0.15s, box-shadow 0.15s;
height: 36px;
}
.alarm-item span {
display: flex;
align-items: center;
height: 100%;
}
.alarm-item:hover {
background-color: #444;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
body.light .alarm-item {
@ -480,12 +571,20 @@ body.light .alarm-item {
border: none;
color: white;
cursor: pointer;
border-radius: 5px;
padding: 5px 8px;
border-radius: 4px;
padding: 2px 6px;
font-size: 15px;
margin-left: 8px;
transition: background 0.15s;
height: 22px;
width: 22px;
display: flex;
align-items: center;
justify-content: center;
}
.alarm-delete-btn:hover {
background-color: #005f73;
background-color: #005f7a;
}
@font-face {
@ -741,6 +840,279 @@ body.light .alarm-item {
padding: 8px;
}
#alarmModal .modal-content button:last-child {
margin-top: 20px;
}
.coffee-modal-header {
margin-bottom: 25px;
}
.coffee-icon-large {
font-size: 48px;
margin-bottom: 15px;
animation: coffeeSteam 2s ease-in-out infinite;
}
@keyframes coffeeSteam {
0%, 100% { transform: translateY(0) scale(1); }
50% { transform: translateY(-5px) scale(1.05); }
}
.coffee-modal-header h2 {
color: #ecf0f1;
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
}
.coffee-subtitle {
color: #bdc3c7;
margin: 0;
font-size: 14px;
}
.coffee-content {
width: 100%;
}
.payment-methods {
margin-bottom: 25px;
}
.payment-method {
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 20px;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.payment-icon {
font-size: 32px;
margin-bottom: 10px;
}
.payment-method h3 {
color: #ecf0f1;
margin: 0 0 15px 0;
font-size: 18px;
}
.chain-selector {
display: flex;
justify-content: center;
gap: 15px;
margin: 0;
}
.chain-option {
padding: 8px 16px;
border-radius: 20px;
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
font-weight: 500;
}
.chain-option.active {
background: #3498db;
color: white;
border-color: #2980b9;
}
.chain-option:not(.active) {
background: rgba(255, 255, 255, 0.1);
color: #bdc3c7;
border-color: rgba(255, 255, 255, 0.2);
}
.chain-option:hover:not(.active) {
background: rgba(255, 255, 255, 0.2);
color: #ecf0f1;
}
.wallet-address-section {
display: flex;
gap: 20px;
margin-bottom: 25px;
align-items: flex-start;
}
.qr-code-placeholder {
background: rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 20px;
border: 1px solid rgba(255, 255, 255, 0.2);
min-width: 100px;
text-align: center;
}
.qr-icon {
font-size: 32px;
margin-bottom: 8px;
}
.qr-code-placeholder p {
color: #bdc3c7;
margin: 0;
font-size: 12px;
}
.address-container {
flex: 1;
text-align: left;
}
.address-container label {
color: #ecf0f1;
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
display: block;
}
.address-box {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
padding: 12px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: space-between;
font-family: 'Courier New', monospace;
font-size: 12px;
word-break: break-all;
}
.address-box:hover {
background: rgba(255, 255, 255, 0.15);
border-color: #3498db;
transform: translateY(-1px);
}
.address-box span:first-child {
color: #ecf0f1;
flex: 1;
margin-right: 10px;
}
.copy-icon {
color: #3498db;
font-size: 16px;
flex-shrink: 0;
}
.copy-hint {
color: #95a5a6;
font-size: 12px;
margin: 8px 0 0 0;
text-align: center;
}
.coffee-footer {
border-top: 1px solid rgba(255, 255, 255, 0.2);
padding-top: 20px;
}
.thank-you {
color: #ecf0f1;
font-size: 16px;
margin: 0 0 15px 0;
font-weight: 500;
}
.coffee-benefits {
display: flex;
justify-content: center;
gap: 15px;
}
.coffee-benefits span {
font-size: 20px;
animation: benefitFloat 3s ease-in-out infinite;
}
.coffee-benefits span:nth-child(2) {
animation-delay: 1s;
}
.coffee-benefits span:nth-child(3) {
animation-delay: 2s;
}
@keyframes benefitFloat {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-3px); }
}
/* Light mode styles */
body.light #buyMeModal .modal-content {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
}
body.light .coffee-modal-header h2 {
color: #2c3e50;
}
body.light .coffee-subtitle {
color: #6c757d;
}
body.light .payment-method {
background: rgba(0, 0, 0, 0.05);
border-color: rgba(0, 0, 0, 0.1);
}
body.light .payment-method h3 {
color: #2c3e50;
}
body.light .chain-option:not(.active) {
background: rgba(0, 0, 0, 0.05);
color: #6c757d;
border-color: rgba(0, 0, 0, 0.1);
}
body.light .chain-option:hover:not(.active) {
background: rgba(0, 0, 0, 0.1);
color: #495057;
}
body.light .qr-code-placeholder {
background: rgba(0, 0, 0, 0.05);
border-color: rgba(0, 0, 0, 0.1);
}
body.light .qr-code-placeholder p {
color: #6c757d;
}
body.light .address-container label {
color: #2c3e50;
}
body.light .address-box {
background: rgba(0, 0, 0, 0.05);
border-color: rgba(0, 0, 0, 0.1);
}
body.light .address-box:hover {
background: rgba(0, 0, 0, 0.1);
border-color: #3498db;
}
body.light .address-box span:first-child {
color: #2c3e50;
}
body.light .copy-hint {
color: #6c757d;
}
body.light .coffee-footer {
border-color: rgba(0, 0, 0, 0.1);
}
body.light .thank-you {
color: #2c3e50;
}

View File

@ -68,7 +68,7 @@
</button>
<div class="version-info">
<span id="currentVersion">Version 1.5.1</span>
<span id="currentVersion">Version 1.6.0</span>
<span
id="updateAvailable"
style="display: none; color: red; cursor: pointer"
@ -201,33 +201,50 @@
<div id="buyMeModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeBuyMeModal()">&times;</span>
<h2>Buy me a Coffee</h2>
<p><strong>Send me:</strong> USDT / Ethereum</p>
<p>
<strong>Chain:</strong>
<span
style="text-decoration: underline; cursor: pointer"
onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')"
>
ETH
</span>
/
<span
style="text-decoration: underline; cursor: pointer"
onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')"
>
BSC
</span>
</p>
<p>
<span
style="text-decoration: underline; cursor: pointer"
onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')"
>
0x26c2E3F6C854Af006520ec2ce433982866bB7632
</span>
</p>
<p>(When clicked, the address is copied to the clipboard.)</p>
<div class="coffee-modal-header">
<div class="coffee-icon-large"></div>
<h2>Buy me a Coffee</h2>
<p class="coffee-subtitle">Support the development of HodlEye</p>
</div>
<div class="coffee-content">
<div class="payment-methods">
<div class="payment-method">
<div class="payment-icon">💎</div>
<h3>USDT / Ethereum</h3>
<p class="chain-selector">
<span class="chain-option active" onclick="selectChain('ETH')">ETH</span>
<span class="chain-option" onclick="selectChain('BSC')">BSC</span>
</p>
</div>
</div>
<div class="wallet-address-section">
<div class="qr-code-placeholder">
<div class="qr-icon">📱</div>
<p>QR Code</p>
</div>
<div class="address-container">
<label>Wallet Address:</label>
<div class="address-box" onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')">
<span id="walletAddress">0x26c2E3F6C854Af006520ec2ce433982866bB7632</span>
<span class="copy-icon">📋</span>
</div>
<p class="copy-hint">Click to copy address</p>
</div>
</div>
<div class="coffee-footer">
<p class="thank-you">Thank you for your support! 🙏</p>
<div class="coffee-benefits">
<span></span>
<span>🚀</span>
<span>💡</span>
</div>
</div>
</div>
</div>
</div>
@ -254,8 +271,8 @@
<span class="close" onclick="closeCryptoNewsModal()">&times;</span>
<h2>Crypto News</h2>
<input type="text" id="search" placeholder="Suche nach Artikeln..." />
<div id="news-feed">Lade News...</div>
<input type="text" id="search" placeholder="Search for articles..." />
<div id="news-feed">Loading news...</div>
</div>
</div>
@ -292,7 +309,7 @@
<div id="updateModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeUpdateModal()">&times;</span>
<h2 id="updateTitle">Update auf <span id="updateVersion"></span></h2>
<h2 id="updateTitle">Update to <span id="updateVersion"></span></h2>
<p id="updateChanges"></p>
<button onclick="performUpdate()">Update</button>

View File

@ -27,7 +27,7 @@ async function loadCryptosFromServer() {
cryptoList = await resp.json();
renderCryptoGrid();
} catch (err) {
console.error("Fehler beim Laden der Kryptoliste:", err);
console.error("Error loading crypto list:", err);
}
}
@ -72,9 +72,20 @@ async function loadAlarmsFromServer() {
try {
const resp = await fetch("/api/alarms");
alarms = await resp.json();
alarms.sort((a, b) => {
if (a.symbol !== b.symbol) {
return a.symbol.localeCompare(b.symbol);
}
return parseFloat(b.price) - parseFloat(a.price);
});
renderAlarmList();
} catch (err) {
console.error("Fehler beim Laden der Alarme:", err);
console.error("Error loading alarms:", err);
}
}
@ -84,18 +95,35 @@ async function addAlarm() {
const frequency = document.getElementById("alarmFrequency").value;
const direction = document.getElementById("alarmDirection").value;
if (!symbol || isNaN(price)) return;
if (alarms.some(a => a.symbol === symbol && parseFloat(a.price) === price)) {
showErrorMessage("Alarm for this symbol and price already exists.");
if (!symbol || isNaN(price)) {
showErrorMessage("Please enter symbol and price!");
return;
}
const duplicate = alarms.some(a => a.symbol === symbol && parseFloat(a.price) === price && a.frequency === frequency && a.direction === direction);
if (duplicate) {
showErrorMessage("An alarm with these settings already exists!");
return;
}
try {
await fetch("/api/alarms", {
const resp = await fetch("/api/alarms", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({symbol, price, frequency, direction}),
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ symbol, price, frequency, direction })
});
loadAlarmsFromServer();
await resp.json();
await loadAlarmsFromServer();
document.getElementById("alarmPrice").value = "";
document.getElementById("alarmPrice").focus();
console.log("✅ Alarm successfully added");
} catch (err) {
showErrorMessage("Error adding alarm: " + err.message);
}
@ -117,7 +145,7 @@ async function loadNotificationsFromServer() {
notifications = await resp.json();
renderNotifications();
} catch (err) {
console.error("Fehler beim Laden der Notifications:", err);
console.error("Error loading notifications:", err);
}
}
@ -130,7 +158,7 @@ async function addNotification(msg) {
});
loadNotificationsFromServer();
} catch (err) {
console.error("Fehler beim Hinzufügen einer Notification:", err);
console.error("Error adding notification:", err);
}
}
@ -139,29 +167,67 @@ async function clearNotifications() {
await fetch("/api/notifications", {method: "DELETE"});
loadNotificationsFromServer();
} catch (err) {
console.error("Fehler beim Löschen der Notifications:", err);
console.error("Error deleting notifications:", err);
}
}
async function init() {
document.getElementById("alarmSound").src = "sound/" + userOptions.soundFile;
updateTheme(userOptions.darkMode);
await loadCryptosFromServer();
await loadAlarmsFromServer();
await loadNotificationsFromServer();
setInterval(() => {
cryptoList.forEach((symbol, index) => {
const elementId = "crypto-" + index;
fetchCryptoData(symbol, elementId).catch(() => setNotSupported(elementId));
});
}, 1000);
renderNotifications();
try {
await loadCryptosFromServer();
await loadAlarmsFromServer();
await loadNotificationsFromServer();
await sortExistingAlarms();
renderCryptoGrid();
renderAlarmList();
renderNotifications();
const savedOptions = localStorage.getItem("userOptions");
if (savedOptions) {
userOptions = { ...userOptions, ...JSON.parse(savedOptions) };
}
const savedApiPref = localStorage.getItem("apiPreference");
if (savedApiPref) {
apiPreference = JSON.parse(savedApiPref);
}
updateTheme(userOptions.darkMode);
document.getElementById("alarmSound").src = "sound/" + userOptions.soundFile;
setInterval(() => {
cryptoList.forEach((symbol, index) => {
const elementId = "crypto-" + index;
fetchCryptoData(symbol, elementId).catch(() => setNotSupported(elementId));
});
}, 1000);
} catch (err) {
console.error("Error during initialization:", err);
}
}
async function sortExistingAlarms() {
try {
const resp = await fetch("/api/alarms/sort", {
method: "POST",
headers: { "Content-Type": "application/json" }
});
const result = await resp.json();
if (result.success) {
console.log("✅ Existing alarms sorted");
}
} catch (err) {
console.error("Error sorting existing alarms:", err);
}
}
function renderCryptoGrid() {
const grid = document.getElementById("cryptoGrid");
grid.innerHTML = "";
@ -254,16 +320,26 @@ function renderAlarmList() {
const container = document.getElementById("alarmListContainer");
container.innerHTML = "";
let grouped = {};
alarms.forEach((alarm) => {
if (!grouped[alarm.symbol]) {
grouped[alarm.symbol] = [];
}
grouped[alarm.symbol].push(alarm);
});
Object.keys(grouped).forEach((symbol) => {
grouped[symbol].sort((a, b) => parseFloat(b.price) - parseFloat(a.price));
});
Object.keys(grouped).forEach((symbol) => {
const groupDiv = document.createElement("div");
groupDiv.className = "alarm-group";
const groupHeader = document.createElement("h3");
groupHeader.textContent = symbol;
container.appendChild(groupHeader);
groupDiv.appendChild(groupHeader);
const groupList = document.createElement("div");
groupList.className = "alarm-group-list";
grouped[symbol].forEach((alarm) => {
const item = document.createElement("div");
item.className = "alarm-item";
@ -275,8 +351,10 @@ function renderAlarmList() {
delBtn.className = "alarm-delete-btn";
delBtn.onclick = () => deleteAlarm(alarm.id);
item.appendChild(delBtn);
container.appendChild(item);
groupList.appendChild(item);
});
groupDiv.appendChild(groupList);
container.appendChild(groupDiv);
});
}
@ -315,11 +393,24 @@ function openAlarmModal() {
option.textContent = symbol;
dropdown.appendChild(option);
});
setupEnterKeyHandlers();
document.getElementById("alarmPrice").focus();
renderAlarmList();
}
function closeAlarmModal() {
document.getElementById("alarmModal").style.display = "none";
document.getElementById("alarmSymbol").value = "";
document.getElementById("alarmPrice").value = "";
document.getElementById("alarmFrequency").value = "Once";
document.getElementById("alarmDirection").value = "Rising";
}
function openOptionsModal() {
@ -480,7 +571,7 @@ async function saveCryptoList() {
body: JSON.stringify({cryptoList: cryptoList}),
});
} catch (err) {
console.error("Fehler beim Speichern der Kryptoliste:", err);
console.error("Error saving crypto list:", err);
}
}
@ -647,7 +738,7 @@ function updateCryptoBox({symbol, elementId, apiUsed, dailyOpen, hourlyOpen, las
function checkAlarms(symbol, currentPrice) {
alarms.forEach((alarm) => {
if (alarm.symbol !== symbol) return;
if (alarm.triggered) return;
if (alarm.triggered && alarm.frequency === "Once") return;
const alarmPrice = parseFloat(alarm.price);
const prevPrice = lastPrices[symbol] || null;
@ -667,10 +758,12 @@ function checkAlarms(symbol, currentPrice) {
if (conditionMet) {
const msg = `⚠️ ALARM (${alarm.frequency}, ${alarm.direction}): ${symbol} reached ${alarmPrice}!`;
showAlarmPopup(msg);
if (alarm.frequency === "Once") {
deleteAlarm(alarm.id);
} else {
alarm.triggered = true;
} else if (alarm.frequency === "Recurring") {
alarm.triggered = false;
updateAlarmOnServer(alarm);
}
}
});
@ -717,8 +810,8 @@ function copyToClipboard(address) {
alert("Address is copied to the clipboard.");
})
.catch((err) => {
console.error("Clipboard API Fehler:", err);
alert("Fehler beim Kopieren der Adresse.");
console.error("Clipboard API error:", err);
alert("Error copying address.");
});
} else {
let textArea = document.createElement("textarea");
@ -740,8 +833,8 @@ function copyToClipboard(address) {
document.execCommand("copy");
alert("Address is copied to the clipboard.");
} catch (err) {
console.error("Fallback Copy Fehler:", err);
alert("Fehler beim Kopieren der Adresse.");
console.error("Fallback copy error:", err);
alert("Error copying address.");
}
document.body.removeChild(textArea);
}
@ -788,8 +881,8 @@ function copyToClipboard(address) {
alert("Address is copied to the clipboard.");
})
.catch((err) => {
console.error("Clipboard API Fehler:", err);
alert("Fehler beim Kopieren der Adresse.");
console.error("Clipboard API error:", err);
alert("Error copying address.");
});
} else {
let textArea = document.createElement("textarea");
@ -811,9 +904,81 @@ function copyToClipboard(address) {
document.execCommand("copy");
alert("Address is copied to the clipboard.");
} catch (err) {
console.error("Fallback Copy Fehler:", err);
alert("Fehler beim Kopieren der Adresse.");
console.error("Fallback copy error:", err);
alert("Error copying address.");
}
document.body.removeChild(textArea);
}
}
async function updateAlarmOnServer(alarm) {
try {
await fetch(`/api/alarms/${alarm.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(alarm)
});
} catch (error) {
console.error('Error updating the alarm on the server:', error);
}
}
function setupEnterKeyHandlers() {
const priceInput = document.getElementById("alarmPrice");
const symbolSelect = document.getElementById("alarmSymbol");
const frequencySelect = document.getElementById("alarmFrequency");
const directionSelect = document.getElementById("alarmDirection");
priceInput.addEventListener("keypress", function(event) {
if (event.key === "Enter") {
event.preventDefault();
addAlarm();
}
});
symbolSelect.addEventListener("keypress", function(event) {
if (event.key === "Enter") {
event.preventDefault();
addAlarm();
}
});
frequencySelect.addEventListener("keypress", function(event) {
if (event.key === "Enter") {
event.preventDefault();
addAlarm();
}
});
directionSelect.addEventListener("keypress", function(event) {
if (event.key === "Enter") {
event.preventDefault();
addAlarm();
}
});
}
function selectChain(chain) {
document.querySelectorAll('.chain-option').forEach(option => {
option.classList.remove('active');
});
event.target.classList.add('active');
const walletAddress = document.getElementById('walletAddress');
if (chain === 'ETH') {
walletAddress.textContent = '0x26c2E3F6C854Af006520ec2ce433982866bB7632';
} else if (chain === 'BSC') {
walletAddress.textContent = '0x26c2E3F6C854Af006520ec2ce433982866bB7632';
}
const addressBox = document.querySelector('.address-box');
addressBox.onclick = () => copyToClipboard(walletAddress.textContent);
}

View File

@ -4,7 +4,6 @@ document.addEventListener("DOMContentLoaded", function () {
const searchInput = document.getElementById("search");
let allArticles = [];
function refreshNewsFeed() {
fetch(apiUrl)
.then(response => response.json())
@ -18,7 +17,6 @@ document.addEventListener("DOMContentLoaded", function () {
});
}
function displayArticles(items) {
newsFeed.innerHTML = "";
items.forEach(item => {
@ -38,7 +36,6 @@ document.addEventListener("DOMContentLoaded", function () {
});
}
searchInput.addEventListener("input", function () {
const searchTerm = searchInput.value.toLowerCase();
const filteredArticles = allArticles.filter(item =>
@ -48,23 +45,21 @@ document.addEventListener("DOMContentLoaded", function () {
displayArticles(filteredArticles);
});
function getTimeAgo(date) {
const now = new Date();
const seconds = Math.floor((now - new Date(date)) / 1000);
if (seconds < 60) return `vor ${seconds} Sekunden`;
if (seconds < 60) return `${seconds} seconds ago`;
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `vor ${minutes} Minuten`;
if (minutes < 60) return `${minutes} minutes ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `vor ${hours} Stunden`;
if (hours < 24) return `${hours} hours ago`;
const days = Math.floor(hours / 24);
if (days < 7) return `vor ${days} Tagen`;
if (days < 30) return `vor ${Math.floor(days / 7)} Wochen`;
if (days < 365) return `vor ${Math.floor(days / 30)} Monaten`;
return `vor ${Math.floor(days / 365)} Jahren`;
if (days < 7) return `${days} days ago`;
if (days < 30) return `${Math.floor(days / 7)} weeks ago`;
if (days < 365) return `${Math.floor(days / 30)} months ago`;
return `${Math.floor(days / 365)} years ago`;
}
function formatSourceName(source) {
const sourceMap = {
"crypto_news": "Crypto News",
@ -77,7 +72,6 @@ document.addEventListener("DOMContentLoaded", function () {
return sourceMap[source] || source;
}
refreshNewsFeed();
setInterval(refreshNewsFeed, 180000);
});

View File

@ -256,10 +256,10 @@ function saveNote() {
alert(j.error);
} else {
closeNoteModal();
updatePrices(); //
updatePrices();
}
})
.catch(() => alert("Error saving note"));
.catch(() => alert("Network error"));
}
@ -290,7 +290,7 @@ function confirmBuy() {
const buyDate = document.getElementById("buyDate").value;
if (!symbol || isNaN(amount) || isNaN(buyPrice)) {
alert("Bitte gültige Werte eingeben");
alert("Please enter valid values");
return;
}
@ -299,7 +299,7 @@ function confirmBuy() {
.then(r => r.json())
.then(j => {
if (j.error) {
alert("Dieser Coin wird nicht unterstützt");
alert("This coin is not supported");
} else {
fetch("/api/portfolio/buy", {
method: "POST",
@ -324,7 +324,7 @@ function confirmBuy() {
});
}
})
.catch(() => alert("Netzwerkfehler"));
.catch(() => alert("Network error"));
}
function confirmSell() {
@ -337,7 +337,7 @@ function confirmSell() {
const sellDate = document.getElementById("sellDate").value;
if (isNaN(sellAmount) || isNaN(sellPrice) || !sellDate) {
alert("Bitte gültige Werte eingeben");
alert("Please enter valid values");
return;
}
@ -358,7 +358,7 @@ function confirmSell() {
}
if (!selectedTransaction) {
alert("Ungültig");
alert("Invalid");
return;
}
@ -382,7 +382,7 @@ function confirmSell() {
closeAddTransactionModal();
}
})
.catch(() => alert("Fehler beim Sell"));
.catch(() => alert("Error during sell"));
}
function populateSellDropdown() {
@ -409,7 +409,7 @@ function openEditTransactionModal(id) {
editIdGlobal = id;
let t = portfolioData.find(x => String(x.id) === String(id));
if (!t) {
alert("Keine gültige Transaktion gefunden");
alert("No valid transaction found");
return;
}
document.getElementById("editSymbol").value = t.symbol;
@ -425,7 +425,7 @@ function closeEditTransactionModal() {
function saveEditedTransaction() {
let t = portfolioData.find(x => String(x.id) === String(editIdGlobal));
if (!t) {
alert("Keine gültige Transaktion gefunden");
alert("No valid transaction found");
return;
}
@ -434,7 +434,7 @@ function saveEditedTransaction() {
const newDate = document.getElementById("editDate").value;
if (isNaN(newAmount) || isNaN(newBuyPrice)) {
alert("Bitte gültige Werte eingeben");
alert("Please enter valid values");
return;
}
@ -459,7 +459,7 @@ function saveEditedTransaction() {
updatePrices();
}
})
.catch(() => alert("Fehler bei Edit"));
.catch(() => alert("Error during edit"));
}
function deleteTransaction() {

View File

@ -1,4 +1,3 @@
document.querySelectorAll('.modal').forEach(modal => {
modal.addEventListener('click', function(event) {
@ -19,5 +18,5 @@ document.querySelectorAll('.modal').forEach(modal => {
if (response.redirected) {
window.location.href = response.url;
}
}).catch(error => console.error("Logout-Fehler:", error));
}).catch(error => console.error("Logout error:", error));
}

View File

@ -19,7 +19,7 @@ function loadTradeSummary() {
renderTradeSummary(allTrades);
updateBottomBar(allTrades);
})
.catch(err => console.error("Fehler beim Laden der Trade Summary", err));
.catch(err => console.error("Error loading trade summary", err));
}
function renderTradeSummary(trades) {

View File

@ -1,6 +1,6 @@
function openTradingViewModal(exchange, symbol) {
const tradingViewSymbol = `${exchange}:${symbol}USDT`;
console.log("TradingView-Symbol =", tradingViewSymbol);
console.log("TradingView symbol =", tradingViewSymbol);
const modal = document.getElementById("tradingViewModal");
const container = document.getElementById("tradingViewModalContent");
@ -45,9 +45,9 @@ function openTradingViewModal(exchange, symbol) {
const widgetContainer = container.querySelector(".tradingview-widget-container");
if (widgetContainer) {
widgetContainer.appendChild(script);
console.log("TradingView-Widget-Skript korrekt dem Container hinzugefügt.");
console.log("TradingView widget script correctly added to container.");
} else {
console.error("Fehler: Widget-Container nicht gefunden.");
console.error("Error: Widget container not found.");
}
}
@ -59,5 +59,5 @@ function closeTradingViewModal() {
const container = document.getElementById("tradingViewModalContent");
container.innerHTML = "";
console.log("TradingView-Modal geschlossen und Inhalt bereinigt.");
console.log("TradingView modal closed and content cleared.");
}

View File

@ -1,4 +1,4 @@
const CURRENT_VERSION = "1.5.1";
const CURRENT_VERSION = "1.6.0";
function getUpdateUrl() {
return "/api/update?t=" + new Date().getTime();
@ -8,9 +8,7 @@ document.addEventListener("DOMContentLoaded", () => {
document.getElementById("currentVersion").textContent = `Version ${CURRENT_VERSION}`;
checkForUpdates();
setInterval(checkForUpdates, 86400000);
});
function checkForUpdates() {
@ -19,18 +17,15 @@ function checkForUpdates() {
.then((data) => {
let skippedVersion = localStorage.getItem("skippedVersion") || null;
if (skippedVersion && data.version !== skippedVersion) {
localStorage.removeItem("skippedVersion");
skippedVersion = null;
}
if (compareVersions(data.version, CURRENT_VERSION) > 0) {
const updateAvailableEl = document.getElementById("updateAvailable");
updateAvailableEl.style.display = "inline";
updateAvailableEl.onclick = () => {
fetch(getUpdateUrl())
.then((response) => response.json())
@ -43,13 +38,13 @@ function checkForUpdates() {
openUpdateModal(data);
})
.catch((error) =>
console.error("Fehler beim erneuten Abrufen des Updates:", error)
console.error("Error fetching update again:", error)
);
};
}
})
.catch((error) =>
console.error("Fehler beim Abrufen des Updates:", error)
console.error("Error fetching update:", error)
);
}
@ -66,14 +61,11 @@ function compareVersions(v1, v2) {
}
function openUpdateModal(data) {
document.getElementById("updateVersion").textContent = data.version;
let changelogContainer = document.getElementById("updateChanges");
changelogContainer.innerHTML = "";
if (Array.isArray(data.changelog)) {
let ul = document.createElement("ul");
data.changelog.forEach(item => {
@ -83,14 +75,11 @@ function openUpdateModal(data) {
});
changelogContainer.appendChild(ul);
} else {
changelogContainer.textContent = data.changelog || "Kein Changelog vorhanden.";
changelogContainer.textContent = data.changelog || "No changelog available.";
}
window._updateData = data;
document.getElementById("updateModal").style.display = "block";
}
@ -99,13 +88,11 @@ function closeUpdateModal() {
}
function performUpdate() {
window.open("https://github.com/Gerald-Ha/HodlEye-Crypto-Price-Tracker", "_blank");
closeUpdateModal();
}
function skipUpdate() {
if (window._updateData && window._updateData.version) {
localStorage.setItem("skippedVersion", window._updateData.version);
}

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="de">
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
@ -85,8 +85,8 @@
<div style="margin-top: 1rem;">
<label>
<input type="radio" name="dateFilterType" value="range">
Von <input type="date" id="filterFromDate">
Bis <input type="date" id="filterToDate">
From <input type="date" id="filterFromDate">
To <input type="date" id="filterToDate">
</label>
</div>
<div style="margin-top: 1rem;">

View File

@ -17,16 +17,27 @@ function writeData(data) {
router.get("/", (req, res) => {
try {
const data = readData();
res.json(data.alarms);
const sortedAlarms = data.alarms.sort((a, b) => {
if (a.symbol !== b.symbol) {
return a.symbol.localeCompare(b.symbol);
}
return parseFloat(b.price) - parseFloat(a.price);
});
res.json(sortedAlarms);
} catch (err) {
res.status(500).json({ error: "Fehler beim Lesen der Daten" });
res.status(500).json({ error: "Error reading data" });
}
});
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." });
return res.status(400).json({ error: "Symbol and price are required." });
}
try {
const data = readData();
@ -42,7 +53,7 @@ router.post("/", (req, res) => {
writeData(data);
res.json(newAlarm);
} catch (err) {
res.status(500).json({ error: "Fehler beim Schreiben der Daten" });
res.status(500).json({ error: "Error writing data" });
}
});
@ -54,7 +65,49 @@ router.delete("/:id", (req, res) => {
writeData(data);
res.json({ success: true, alarms: data.alarms });
} catch (err) {
res.status(500).json({ error: "Fehler beim Löschen des Alarms" });
res.status(500).json({ error: "Error deleting the alarm" });
}
});
router.put("/:id", (req, res) => {
const alarmId = parseInt(req.params.id, 10);
const updatedAlarm = req.body;
try {
const data = readData();
const alarmIndex = data.alarms.findIndex(a => a.id === alarmId);
if (alarmIndex === -1) {
return res.status(404).json({ error: "Alarm not found" });
}
data.alarms[alarmIndex] = { ...data.alarms[alarmIndex], ...updatedAlarm };
writeData(data);
res.json({ success: true, alarm: data.alarms[alarmIndex] });
} catch (err) {
res.status(500).json({ error: "Error updating the alarm" });
}
});
router.post("/sort", (req, res) => {
try {
const data = readData();
data.alarms.sort((a, b) => {
if (a.symbol !== b.symbol) {
return a.symbol.localeCompare(b.symbol);
}
return parseFloat(b.price) - parseFloat(a.price);
});
writeData(data);
res.json({ success: true, message: "Alarms sorted", alarms: data.alarms });
} catch (err) {
res.status(500).json({ error: "Error sorting alarms" });
}
});

View File

@ -10,7 +10,7 @@ const fetch = require("node-fetch");
/*
* Metadata
* Version: 1.5.1
* Version: 1.6.0
* Author/Dev: Gerald Hasani
* Name: HodlEye Crypto Price Tracker
* Email: contact@gerald-hasani.com
@ -28,7 +28,7 @@ app.use(session({
}));
app.use((req, res, next) => {
const publicPaths = ["/login", "/login.html"];
const publicPaths = ["/login", "/login.html", "/api/test-alarm", "/api/create-test-alarm", "/api/test-notification"];
const staticFileExtensions = [".css", ".js", ".png", ".jpg", ".jpeg", ".svg"];
if (req.session.loggedIn || publicPaths.includes(req.path) || staticFileExtensions.some(ext => req.path.endsWith(ext))) {
return next();
@ -53,6 +53,17 @@ app.post("/login", (req, res) => {
}
});
app.post("/api/login", (req, res) => {
const { username, password } = req.body;
if (username === process.env.LOGIN_USER && password === process.env.LOGIN_PASS) {
req.session.loggedIn = true;
return res.json({ success: true, message: "Login successful" });
} else {
return res.status(401).json({ success: false, message: "Invalid login credentials" });
}
});
app.get("/logout", (req, res) => {
req.session.destroy();
res.redirect("/login");
@ -66,6 +77,10 @@ const server = http.createServer(app);
const wss = new WebSocket.Server({ server });
const clients = new Set();
let alarmCheckInterval;
let lastAlarmPrices = {};
const DATA_FILE = path.join(__dirname, "..", "data", "data.json");
if (!fs.existsSync(DATA_FILE)) {
fs.writeFileSync(DATA_FILE, JSON.stringify({ cryptos: ["BTC"], alarms: [], notifications: [] }, null, 2));
@ -187,7 +202,7 @@ app.put("/api/cryptos", (req, res) => {
res.json({ success: true, cryptos: data.cryptos });
});
/* ------------- Notizen  ------------- */
/* ------------- Notices ------------- */
app.post("/api/portfolio/note", (req, res) => {
const { id, note } = req.body;
@ -531,16 +546,16 @@ app.get("/api/trade_summary", (req, res) => {
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" });
return res.status(400).json({ error: "Invalid data" });
}
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" });
return res.status(400).json({ error: "Transaction not found" });
}
let tx = pf.transactions[txIndex];
if (sellAmount > tx.amount) {
return res.status(400).json({ error: "Verkaufsmenge überschreitet die vorhandene Menge" });
return res.status(400).json({ error: "Selling amount exceeds available amount" });
}
const profit = (parseFloat(sellPrice) - parseFloat(tx.buyPrice)) * sellAmount;
const percentProfit = tx.buyPrice > 0 ? (profit / (tx.buyPrice * sellAmount)) * 100 : 0;
@ -583,16 +598,16 @@ app.post("/api/portfolio/delete", (req, res) => {
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" });
return res.status(400).json({ error: "Invalid data" });
}
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" });
return res.status(400).json({ error: "Transaction not found" });
}
let tx = pf.transactions[txIndex];
if (sellAmount > tx.amount) {
return res.status(400).json({ error: "Verkaufsmenge überschreitet die vorhandene Menge" });
return res.status(400).json({ error: "Selling amount exceeds available amount" });
}
const profit = (parseFloat(sellPrice) - parseFloat(tx.buyPrice)) * sellAmount;
const percentProfit = tx.buyPrice > 0 ? (profit / (tx.buyPrice * sellAmount)) * 100 : 0;
@ -683,13 +698,246 @@ function writePortfolio(data) {
wss.on("connection", (ws) => {
console.log(`🔗 New WebSocket client connected. Total: ${clients.size + 1}`);
clients.add(ws);
console.log(`📡 WebSocket client added. Current clients: ${clients.size}`);
ws.on("close", () => {
clients.delete(ws);
console.log(`🔌 WebSocket client disconnected. Remaining clients: ${clients.size}`);
});
ws.on("error", (error) => {
console.error(`WebSocket error:`, error);
});
const welcomeMessage = {
message: "WebSocket connection established",
timestamp: new Date().toISOString()
};
ws.send(JSON.stringify(welcomeMessage));
console.log(`Welcome message sent to client`);
});
const PORT = process.env.PORT || 3099;
server.listen(PORT, () => {
console.log("Server läuft auf Port " + PORT);
console.log("Server running on port " + PORT);
startAlarmChecking();
});
async function checkAlarmsServerSide() {
try {
console.log("Server-side alarm checking started...");
console.log(`Connected WebSocket clients: ${clients.size}`);
const data = readData();
console.log(`Found alarms: ${data.alarms ? data.alarms.length : 0}`);
if (!data.alarms || data.alarms.length === 0) {
console.log("No active alarms found");
return;
}
data.alarms.forEach(alarm => {
if (!alarm.triggered || alarm.frequency === "Recurring") {
console.log(`📋 Active alarm: ${alarm.symbol} at ${alarm.price} (${alarm.direction}, ${alarm.frequency})`);
}
});
for (const alarm of data.alarms) {
if (alarm.triggered && alarm.frequency === "Once") {
console.log(`⏭️ One-time alarm ${alarm.symbol} already triggered, skipping...`);
continue;
}
console.log(`🔍 Checking alarm: ${alarm.symbol} at ${alarm.price} (${alarm.direction}, ${alarm.frequency})`);
try {
let currentPrice = null;
const binSup = await binanceSupported(alarm.symbol);
const okxSup = await okxSupported(alarm.symbol);
console.log(`📡 API support for ${alarm.symbol}: Binance=${binSup}, OKX=${okxSup}`);
if (binSup) {
try {
currentPrice = await getBinancePrice(alarm.symbol);
console.log(`Binance price for ${alarm.symbol}: ${currentPrice}`);
} catch (error) {
console.log(`Binance error for ${alarm.symbol}:`, error.message);
if (okxSup) {
currentPrice = await getOkxPrice(alarm.symbol);
console.log(`OKX price for ${alarm.symbol}: ${currentPrice}`);
}
}
} else if (okxSup) {
currentPrice = await getOkxPrice(alarm.symbol);
console.log(`OKX price for ${alarm.symbol}: ${currentPrice}`);
}
if (currentPrice === null) {
console.log(`No price available for ${alarm.symbol}`);
continue;
}
const alarmKey = `${alarm.symbol}_${alarm.price}_${alarm.direction}_${alarm.frequency}_${alarm.id}`;
const prevPrice = lastAlarmPrices[alarmKey] ?? null;
console.log(`Price comparison for ${alarm.symbol}: Previous=${prevPrice}, Current=${currentPrice}, Target=${alarm.price}`);
if (prevPrice === null) {
lastAlarmPrices[alarmKey] = currentPrice;
console.log(`First price for alarm ${alarmKey} saved: ${currentPrice}`);
continue;
}
let conditionMet = false;
if (alarm.direction === "Rising") {
conditionMet = prevPrice < alarm.price && currentPrice >= alarm.price;
} else if (alarm.direction === "Falling") {
conditionMet = prevPrice > alarm.price && currentPrice <= alarm.price;
} else if (alarm.direction === "Both") {
const crossingUp = prevPrice < alarm.price && currentPrice >= alarm.price;
const crossingDown = prevPrice > alarm.price && currentPrice <= alarm.price;
conditionMet = crossingUp || crossingDown;
}
console.log(`🎯 Condition for ${alarm.symbol}: ${conditionMet ? 'MET' : 'Not met'}`);
if (conditionMet) {
const message = `ALARM (${alarm.frequency}, ${alarm.direction}): ${alarm.symbol} reached ${alarm.price}!`;
console.log(`ALARM TRIGGERED: ${message}`);
const notification = {
message: message,
timestamp: new Date().toISOString()
};
console.log(`📡 Sending notification to ${clients.size} connected clients...`);
let sentCount = 0;
clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(notification));
sentCount++;
console.log(`Notification sent to client ${sentCount}`);
} else {
console.log(`Client not ready (Status: ${client.readyState})`);
}
});
console.log(`Total ${sentCount} notifications sent`);
data.notifications.unshift(notification);
writeData(data);
if (alarm.frequency === "Once") {
data.alarms = data.alarms.filter(a => a.id !== alarm.id);
console.log(`🗑️ One-time alarm ${alarm.symbol} deleted`);
} else if (alarm.frequency === "Recurring") {
alarm.triggered = false;
console.log(`Recurring alarm ${alarm.symbol} reset for next trigger`);
}
writeData(data);
}
lastAlarmPrices[alarmKey] = currentPrice;
} catch (error) {
console.error(`Error during alarm check for ${alarm.symbol}:`, error);
}
}
} catch (error) {
console.error("Error during server-side alarm checking:", error);
}
}
function startAlarmChecking() {
if (alarmCheckInterval) {
clearInterval(alarmCheckInterval);
}
console.log("Starting server-side alarm checking...");
checkAlarmsServerSide();
alarmCheckInterval = setInterval(checkAlarmsServerSide, 1000);
console.log("Server-side alarm checking started (every 1 second)");
}
app.post("/api/test-alarm", (req, res) => {
try {
console.log("Test alarm checking started...");
checkAlarmsServerSide();
res.json({ success: true, message: "Alarm checking started" });
} catch (error) {
console.error("Error during test alarm:", error);
res.status(500).json({ error: "Test alarm failed" });
}
});
app.post("/api/create-test-alarm", (req, res) => {
try {
const data = readData();
const testAlarm = {
id: Date.now(),
symbol: "BTC",
price: 50000,
frequency: "Once",
direction: "Rising",
triggered: false
};
data.alarms.push(testAlarm);
writeData(data);
console.log("Test alarm created:", testAlarm);
res.json({ success: true, alarm: testAlarm });
} catch (error) {
console.error("Error creating test alarm:", error);
res.status(500).json({ error: "Creating test alarm failed" });
}
});
app.post("/api/test-notification", (req, res) => {
try {
console.log("Test notification being sent...");
console.log(`Connected clients: ${clients.size}`);
const testNotification = {
message: "TEST NOTIFICATION: This message comes from the server!",
timestamp: new Date().toISOString()
};
let sentCount = 0;
clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(testNotification));
sentCount++;
console.log(`Test notification sent to client ${sentCount}`);
}
});
console.log(`Total ${sentCount} test notifications sent`);
res.json({ success: true, message: `${sentCount} test notifications sent` });
} catch (error) {
console.error("Error during test notification:", error);
res.status(500).json({ error: "Test notification failed" });
}
});

View File

@ -1,8 +1,24 @@
{
"version": "1.5.1",
"version": "1.6.0",
"changelog": [
"Adding Note Option in Portfolio Page",
"Fixing Editing Option in Trade Summary"
"Revamped Alarm Modal with grouped alarms per coin and cleaner layout",
"Improved alignment and responsiveness of alarm input fields",
"Compact alarm boxes with hover effects and subtle delete buttons",
"Alarm modal now retains focus and resets fields after adding an alarm",
"Enter key submits alarm from any input field",
"Prevents duplicate alarms for same coin/price",
"Alarm titles now displayed with center-aligned divider lines",
"Alarm logic improved: server-side checks even without browser",
"Recurring alarms now persist and trigger correctly",
"Alarms are sorted and grouped by symbol and price",
"Last price check is stored per alarm to avoid threshold skips",
"API improved: endpoints usable for external tools (e.g., Windows app)",
"Better error handling and logging for API actions",
"Windows App UI redesigned: modern English layout, centered elements",
"Tray icon with context menu (Open/Exit) and minimize/close behavior",
"All texts and notifications are now in English",
"Auto-reconnect and connection recovery implemented",
"Notification appears if disconnected for more than 2 minutes",
"Resolved issue where HodlEye_Notify could not receive alarms from the 'HodlEye-Crypto-Price-Tracker' Docker container when no browser session was active"
]
}