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

119
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. 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 ## 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** **Default Login Data**
**User:** admin **User:** admin
@ -26,19 +28,17 @@ Check out the live demo here: [HodlEye Demo](https://hodleye.gerald-hasani.com/)
- [Crypto News](#crypto-news) - [Crypto News](#crypto-news)
- [Economic Calendar](#economic-calendar) - [Economic Calendar](#economic-calendar)
- [TradingView Chart](#tradingview-chart) - [TradingView Chart](#tradingview-chart)
3. [Portfolio Management](#portfolio-management) 3. [Installation & Usage](#installation--usage)
4. [Installation & Usage](#installation--usage)
- [Requirements](#requirements) - [Requirements](#requirements)
- [Environment variables (.env)](#environment-variables-env)
- [Docker Build & Run](#docker-build--run) - [Docker Build & Run](#docker-build--run)
5. [Windows Notification App: HodlEye_Notify](#windows-notification-app-hodleye_notify) 4. [Windows Notification App: HodlEye_Notify](#windows-notification-app-hodleye_notify)
6. [Project Structure](#project-structure) 5. [Project Structure](#project-structure)
- [Frontend (index.html & magic.js)](#frontend-indexhtml--magicjs) - [Frontend (index.html & magic.js)](#frontend-indexhtml--magicjs)
- [News Feed Server (Node.js)](#news-feed-server-nodejs) - [News Feed Server (Node.js)](#news-feed-server-nodejs)
7. [Important Notes / Limitations](#important-notes--limitations) 6. [Important Notes / Limitations](#important-notes--limitations)
8. [Coming Soon](#coming-soon) 7. [Coming Soon](#coming-soon)
9. [Privacy & Data Disclaimer](#privacy--data-disclaimer) 8. [Privacy & Data Disclaimer](#privacy--data-disclaimer)
10. [License](#license) 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. - 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"> <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; &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 ## Installation & Usage
### Requirements ### 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. - [Docker](https://www.docker.com/) installed.
- (Optional) [Docker-Compose](https://docs.docker.com/compose/) if you want a more complex or multi-container setup. - (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 ### Docker Build & Run
1. **Clone this repository** 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 HodlEye-Crypto-Price-Tracker
├── .env
├── Dockerfile ├── Dockerfile
├── LICENSE.txt ├── LICENSE.txt
├── PRIVACY.md ├── PRIVACY.md
@ -264,11 +210,6 @@ HodlEye-Crypto-Price-Tracker
├── data ├── data
│ └── data.json │ └── data.json
├── public ├── public
│ ├── css
│ │ ├── login.css
│ │ ├── portfolio.css
│ │ ├── responsive.css
│ │ └── style.css
│ ├── font │ ├── font
│ │ └── BreeSerif-Regular.ttf │ │ └── BreeSerif-Regular.ttf
│ ├── images │ ├── images
@ -277,22 +218,23 @@ HodlEye-Crypto-Price-Tracker
│ │ ├── favicon.png │ │ ├── favicon.png
│ │ └── github-mark.svg │ │ └── github-mark.svg
│ ├── index.html │ ├── index.html
│ ├── js │ ├── magic.js
│ │ ├── magic.js │ ├── news.js
│ │ ├── news.js │ ├── tradingview.js
│ │ ├── portfolio.js │ ├── script.js
│ │ ├── script.js │ ├── responsive.css
│ │ ├── trade_summary.js
│ │ ├── tradingview.js
│ │ └── update.js
│ ├── login.html
│ ├── portfolio.html
│ ├── sound │ ├── sound
│ │ ├── cashing.mp3 │ │ ├── cashing.mp3
│ │ └── ping.mp3 │ │ └── ping.mp3
│ └── trade_summary.html │ ├── style.css
│ └── update.js
├── server ├── server
│ ├── newsfeed │ ├── newsfeed
│ │ ├── node_modules
│ │ ├── package-lock.json
│ │ ├── package.json
│ │ └── server.js
│ ├── node_modules
│ ├── package-lock.json │ ├── package-lock.json
│ ├── package.json │ ├── package.json
│ └── server.js │ └── server.js
@ -300,7 +242,6 @@ HodlEye-Crypto-Price-Tracker
│ ├── cashing.mp3 │ ├── cashing.mp3
│ └── ping.mp3 │ └── ping.mp3
└── update.json └── update.json
``` ```
&nbsp; &nbsp;
@ -365,13 +306,14 @@ _(Within Docker, its already bundled, so just expose `5001`.)_
--- ---
&nbsp; &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. - **Portfolio Management**: Track your crypto holdings in real-time with easy-to-read analytics.
- **Android**: Android app with synchronization option to HodlEye Docker (First early alpha already available internally) - **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** - **HodlEye Notify Alarm with various sound selections and HodlEye Alarms**
- **Windows HodlEye Notify Update**: Windows app bugfix and updates
Stay tuned for 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. - **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; &nbsp;

View File

@ -238,10 +238,9 @@ body.light .crypto-box {
#buyMeModal .modal-content { #buyMeModal .modal-content {
width: 400px;
width: 30%; max-width: 90%;
overflow-x: hidden; overflow-x: hidden;
text-align: center; text-align: center;
position: absolute; position: absolute;
top: 35%; top: 35%;
@ -251,9 +250,12 @@ body.light .crypto-box {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: 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 { #buyMeModal .close {
position: absolute; position: absolute;
top: 15px; top: 15px;
@ -314,6 +316,7 @@ body.light .crypto-box {
position: absolute; position: absolute;
top: 35%; top: 35%;
left: 50%; left: 50%;
max-width: 100%;
transform: translate(-50%, -45%); transform: translate(-50%, -45%);
flex-direction: column; flex-direction: column;
@ -347,6 +350,35 @@ body.light .crypto-box {
cursor: pointer; 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 { .alarm-list-container {
display: grid; display: block;
grid-template-columns: repeat(2, 2fr); max-height: 200px;
gap: 10px;
max-height: 300px;
overflow-y: auto; overflow-y: auto;
margin: 1em 0; 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 { .alarm-item {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
background-color: #333; background-color: #333;
padding: 6px; padding: 3px 8px;
border-radius: 3px; 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 { body.light .alarm-item {
@ -480,12 +571,20 @@ body.light .alarm-item {
border: none; border: none;
color: white; color: white;
cursor: pointer; cursor: pointer;
border-radius: 5px; border-radius: 4px;
padding: 5px 8px; 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 { .alarm-delete-btn:hover {
background-color: #005f73; background-color: #005f7a;
} }
@font-face { @font-face {
@ -741,6 +840,279 @@ body.light .alarm-item {
padding: 8px; padding: 8px;
} }
#alarmModal .modal-content button:last-child { .coffee-modal-header {
margin-top: 20px; 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> </button>
<div class="version-info"> <div class="version-info">
<span id="currentVersion">Version 1.5.1</span> <span id="currentVersion">Version 1.6.0</span>
<span <span
id="updateAvailable" id="updateAvailable"
style="display: none; color: red; cursor: pointer" style="display: none; color: red; cursor: pointer"
@ -201,33 +201,50 @@
<div id="buyMeModal" class="modal"> <div id="buyMeModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<span class="close" onclick="closeBuyMeModal()">&times;</span> <span class="close" onclick="closeBuyMeModal()">&times;</span>
<div class="coffee-modal-header">
<div class="coffee-icon-large"></div>
<h2>Buy me a Coffee</h2> <h2>Buy me a Coffee</h2>
<p><strong>Send me:</strong> USDT / Ethereum</p> <p class="coffee-subtitle">Support the development of HodlEye</p>
<p> </div>
<strong>Chain:</strong>
<span <div class="coffee-content">
style="text-decoration: underline; cursor: pointer" <div class="payment-methods">
onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')" <div class="payment-method">
> <div class="payment-icon">💎</div>
ETH <h3>USDT / Ethereum</h3>
</span> <p class="chain-selector">
/ <span class="chain-option active" onclick="selectChain('ETH')">ETH</span>
<span <span class="chain-option" onclick="selectChain('BSC')">BSC</span>
style="text-decoration: underline; cursor: pointer"
onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')"
>
BSC
</span>
</p> </p>
<p> </div>
<span </div>
style="text-decoration: underline; cursor: pointer"
onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')" <div class="wallet-address-section">
> <div class="qr-code-placeholder">
0x26c2E3F6C854Af006520ec2ce433982866bB7632 <div class="qr-icon">📱</div>
</span> <p>QR Code</p>
</p> </div>
<p>(When clicked, the address is copied to the clipboard.)</p>
<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>
</div> </div>
@ -254,8 +271,8 @@
<span class="close" onclick="closeCryptoNewsModal()">&times;</span> <span class="close" onclick="closeCryptoNewsModal()">&times;</span>
<h2>Crypto News</h2> <h2>Crypto News</h2>
<input type="text" id="search" placeholder="Suche nach Artikeln..." /> <input type="text" id="search" placeholder="Search for articles..." />
<div id="news-feed">Lade News...</div> <div id="news-feed">Loading news...</div>
</div> </div>
</div> </div>
@ -292,7 +309,7 @@
<div id="updateModal" class="modal"> <div id="updateModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<span class="close" onclick="closeUpdateModal()">&times;</span> <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> <p id="updateChanges"></p>
<button onclick="performUpdate()">Update</button> <button onclick="performUpdate()">Update</button>

View File

@ -27,7 +27,7 @@ async function loadCryptosFromServer() {
cryptoList = await resp.json(); cryptoList = await resp.json();
renderCryptoGrid(); renderCryptoGrid();
} catch (err) { } 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 { try {
const resp = await fetch("/api/alarms"); const resp = await fetch("/api/alarms");
alarms = await resp.json(); 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(); renderAlarmList();
} catch (err) { } 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 frequency = document.getElementById("alarmFrequency").value;
const direction = document.getElementById("alarmDirection").value; const direction = document.getElementById("alarmDirection").value;
if (!symbol || isNaN(price)) return; if (!symbol || isNaN(price)) {
if (alarms.some(a => a.symbol === symbol && parseFloat(a.price) === price)) { showErrorMessage("Please enter symbol and price!");
showErrorMessage("Alarm for this symbol and price already exists.");
return; 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 { try {
await fetch("/api/alarms", { const resp = await fetch("/api/alarms", {
method: "POST", method: "POST",
headers: {"Content-Type": "application/json"}, headers: { "Content-Type": "application/json" },
body: JSON.stringify({symbol, price, frequency, direction}), 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) { } catch (err) {
showErrorMessage("Error adding alarm: " + err.message); showErrorMessage("Error adding alarm: " + err.message);
} }
@ -117,7 +145,7 @@ async function loadNotificationsFromServer() {
notifications = await resp.json(); notifications = await resp.json();
renderNotifications(); renderNotifications();
} catch (err) { } 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(); loadNotificationsFromServer();
} catch (err) { } 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"}); await fetch("/api/notifications", {method: "DELETE"});
loadNotificationsFromServer(); loadNotificationsFromServer();
} catch (err) { } catch (err) {
console.error("Fehler beim Löschen der Notifications:", err); console.error("Error deleting notifications:", err);
} }
} }
async function init() { async function init() {
document.getElementById("alarmSound").src = "sound/" + userOptions.soundFile; try {
updateTheme(userOptions.darkMode);
await loadCryptosFromServer(); await loadCryptosFromServer();
await loadAlarmsFromServer(); await loadAlarmsFromServer();
await loadNotificationsFromServer(); 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(() => { setInterval(() => {
cryptoList.forEach((symbol, index) => { cryptoList.forEach((symbol, index) => {
const elementId = "crypto-" + index; const elementId = "crypto-" + index;
fetchCryptoData(symbol, elementId).catch(() => setNotSupported(elementId)); fetchCryptoData(symbol, elementId).catch(() => setNotSupported(elementId));
}); });
}, 1000); }, 1000);
} catch (err) {
renderNotifications(); 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() { function renderCryptoGrid() {
const grid = document.getElementById("cryptoGrid"); const grid = document.getElementById("cryptoGrid");
grid.innerHTML = ""; grid.innerHTML = "";
@ -254,16 +320,26 @@ function renderAlarmList() {
const container = document.getElementById("alarmListContainer"); const container = document.getElementById("alarmListContainer");
container.innerHTML = ""; container.innerHTML = "";
let grouped = {}; let grouped = {};
alarms.forEach((alarm) => { alarms.forEach((alarm) => {
if (!grouped[alarm.symbol]) { if (!grouped[alarm.symbol]) {
grouped[alarm.symbol] = []; grouped[alarm.symbol] = [];
} }
grouped[alarm.symbol].push(alarm); grouped[alarm.symbol].push(alarm);
}); });
Object.keys(grouped).forEach((symbol) => { 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"); const groupHeader = document.createElement("h3");
groupHeader.textContent = symbol; groupHeader.textContent = symbol;
container.appendChild(groupHeader); groupDiv.appendChild(groupHeader);
const groupList = document.createElement("div");
groupList.className = "alarm-group-list";
grouped[symbol].forEach((alarm) => { grouped[symbol].forEach((alarm) => {
const item = document.createElement("div"); const item = document.createElement("div");
item.className = "alarm-item"; item.className = "alarm-item";
@ -275,8 +351,10 @@ function renderAlarmList() {
delBtn.className = "alarm-delete-btn"; delBtn.className = "alarm-delete-btn";
delBtn.onclick = () => deleteAlarm(alarm.id); delBtn.onclick = () => deleteAlarm(alarm.id);
item.appendChild(delBtn); item.appendChild(delBtn);
container.appendChild(item); groupList.appendChild(item);
}); });
groupDiv.appendChild(groupList);
container.appendChild(groupDiv);
}); });
} }
@ -315,11 +393,24 @@ function openAlarmModal() {
option.textContent = symbol; option.textContent = symbol;
dropdown.appendChild(option); dropdown.appendChild(option);
}); });
setupEnterKeyHandlers();
document.getElementById("alarmPrice").focus();
renderAlarmList(); renderAlarmList();
} }
function closeAlarmModal() { function closeAlarmModal() {
document.getElementById("alarmModal").style.display = "none"; 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() { function openOptionsModal() {
@ -480,7 +571,7 @@ async function saveCryptoList() {
body: JSON.stringify({cryptoList: cryptoList}), body: JSON.stringify({cryptoList: cryptoList}),
}); });
} catch (err) { } 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) { function checkAlarms(symbol, currentPrice) {
alarms.forEach((alarm) => { alarms.forEach((alarm) => {
if (alarm.symbol !== symbol) return; if (alarm.symbol !== symbol) return;
if (alarm.triggered) return; if (alarm.triggered && alarm.frequency === "Once") return;
const alarmPrice = parseFloat(alarm.price); const alarmPrice = parseFloat(alarm.price);
const prevPrice = lastPrices[symbol] || null; const prevPrice = lastPrices[symbol] || null;
@ -667,10 +758,12 @@ function checkAlarms(symbol, currentPrice) {
if (conditionMet) { if (conditionMet) {
const msg = `⚠️ ALARM (${alarm.frequency}, ${alarm.direction}): ${symbol} reached ${alarmPrice}!`; const msg = `⚠️ ALARM (${alarm.frequency}, ${alarm.direction}): ${symbol} reached ${alarmPrice}!`;
showAlarmPopup(msg); showAlarmPopup(msg);
if (alarm.frequency === "Once") { if (alarm.frequency === "Once") {
deleteAlarm(alarm.id); deleteAlarm(alarm.id);
} else { } else if (alarm.frequency === "Recurring") {
alarm.triggered = true; alarm.triggered = false;
updateAlarmOnServer(alarm);
} }
} }
}); });
@ -717,8 +810,8 @@ function copyToClipboard(address) {
alert("Address is copied to the clipboard."); alert("Address is copied to the clipboard.");
}) })
.catch((err) => { .catch((err) => {
console.error("Clipboard API Fehler:", err); console.error("Clipboard API error:", err);
alert("Fehler beim Kopieren der Adresse."); alert("Error copying address.");
}); });
} else { } else {
let textArea = document.createElement("textarea"); let textArea = document.createElement("textarea");
@ -740,8 +833,8 @@ function copyToClipboard(address) {
document.execCommand("copy"); document.execCommand("copy");
alert("Address is copied to the clipboard."); alert("Address is copied to the clipboard.");
} catch (err) { } catch (err) {
console.error("Fallback Copy Fehler:", err); console.error("Fallback copy error:", err);
alert("Fehler beim Kopieren der Adresse."); alert("Error copying address.");
} }
document.body.removeChild(textArea); document.body.removeChild(textArea);
} }
@ -788,8 +881,8 @@ function copyToClipboard(address) {
alert("Address is copied to the clipboard."); alert("Address is copied to the clipboard.");
}) })
.catch((err) => { .catch((err) => {
console.error("Clipboard API Fehler:", err); console.error("Clipboard API error:", err);
alert("Fehler beim Kopieren der Adresse."); alert("Error copying address.");
}); });
} else { } else {
let textArea = document.createElement("textarea"); let textArea = document.createElement("textarea");
@ -811,9 +904,81 @@ function copyToClipboard(address) {
document.execCommand("copy"); document.execCommand("copy");
alert("Address is copied to the clipboard."); alert("Address is copied to the clipboard.");
} catch (err) { } catch (err) {
console.error("Fallback Copy Fehler:", err); console.error("Fallback copy error:", err);
alert("Fehler beim Kopieren der Adresse."); alert("Error copying address.");
} }
document.body.removeChild(textArea); 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"); const searchInput = document.getElementById("search");
let allArticles = []; let allArticles = [];
function refreshNewsFeed() { function refreshNewsFeed() {
fetch(apiUrl) fetch(apiUrl)
.then(response => response.json()) .then(response => response.json())
@ -18,7 +17,6 @@ document.addEventListener("DOMContentLoaded", function () {
}); });
} }
function displayArticles(items) { function displayArticles(items) {
newsFeed.innerHTML = ""; newsFeed.innerHTML = "";
items.forEach(item => { items.forEach(item => {
@ -38,7 +36,6 @@ document.addEventListener("DOMContentLoaded", function () {
}); });
} }
searchInput.addEventListener("input", function () { searchInput.addEventListener("input", function () {
const searchTerm = searchInput.value.toLowerCase(); const searchTerm = searchInput.value.toLowerCase();
const filteredArticles = allArticles.filter(item => const filteredArticles = allArticles.filter(item =>
@ -48,23 +45,21 @@ document.addEventListener("DOMContentLoaded", function () {
displayArticles(filteredArticles); displayArticles(filteredArticles);
}); });
function getTimeAgo(date) { function getTimeAgo(date) {
const now = new Date(); const now = new Date();
const seconds = Math.floor((now - new Date(date)) / 1000); 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); 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); 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); const days = Math.floor(hours / 24);
if (days < 7) return `vor ${days} Tagen`; if (days < 7) return `${days} days ago`;
if (days < 30) return `vor ${Math.floor(days / 7)} Wochen`; if (days < 30) return `${Math.floor(days / 7)} weeks ago`;
if (days < 365) return `vor ${Math.floor(days / 30)} Monaten`; if (days < 365) return `${Math.floor(days / 30)} months ago`;
return `vor ${Math.floor(days / 365)} Jahren`; return `${Math.floor(days / 365)} years ago`;
} }
function formatSourceName(source) { function formatSourceName(source) {
const sourceMap = { const sourceMap = {
"crypto_news": "Crypto News", "crypto_news": "Crypto News",
@ -77,7 +72,6 @@ document.addEventListener("DOMContentLoaded", function () {
return sourceMap[source] || source; return sourceMap[source] || source;
} }
refreshNewsFeed(); refreshNewsFeed();
setInterval(refreshNewsFeed, 180000); setInterval(refreshNewsFeed, 180000);
}); });

View File

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

View File

@ -1,4 +1,3 @@
document.querySelectorAll('.modal').forEach(modal => { document.querySelectorAll('.modal').forEach(modal => {
modal.addEventListener('click', function(event) { modal.addEventListener('click', function(event) {
@ -19,5 +18,5 @@ document.querySelectorAll('.modal').forEach(modal => {
if (response.redirected) { if (response.redirected) {
window.location.href = response.url; 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); renderTradeSummary(allTrades);
updateBottomBar(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) { function renderTradeSummary(trades) {

View File

@ -1,6 +1,6 @@
function openTradingViewModal(exchange, symbol) { function openTradingViewModal(exchange, symbol) {
const tradingViewSymbol = `${exchange}:${symbol}USDT`; const tradingViewSymbol = `${exchange}:${symbol}USDT`;
console.log("TradingView-Symbol =", tradingViewSymbol); console.log("TradingView symbol =", tradingViewSymbol);
const modal = document.getElementById("tradingViewModal"); const modal = document.getElementById("tradingViewModal");
const container = document.getElementById("tradingViewModalContent"); const container = document.getElementById("tradingViewModalContent");
@ -45,9 +45,9 @@ function openTradingViewModal(exchange, symbol) {
const widgetContainer = container.querySelector(".tradingview-widget-container"); const widgetContainer = container.querySelector(".tradingview-widget-container");
if (widgetContainer) { if (widgetContainer) {
widgetContainer.appendChild(script); widgetContainer.appendChild(script);
console.log("TradingView-Widget-Skript korrekt dem Container hinzugefügt."); console.log("TradingView widget script correctly added to container.");
} else { } 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"); const container = document.getElementById("tradingViewModalContent");
container.innerHTML = ""; 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() { function getUpdateUrl() {
return "/api/update?t=" + new Date().getTime(); return "/api/update?t=" + new Date().getTime();
@ -8,9 +8,7 @@ document.addEventListener("DOMContentLoaded", () => {
document.getElementById("currentVersion").textContent = `Version ${CURRENT_VERSION}`; document.getElementById("currentVersion").textContent = `Version ${CURRENT_VERSION}`;
checkForUpdates(); checkForUpdates();
setInterval(checkForUpdates, 86400000); setInterval(checkForUpdates, 86400000);
}); });
function checkForUpdates() { function checkForUpdates() {
@ -19,18 +17,15 @@ function checkForUpdates() {
.then((data) => { .then((data) => {
let skippedVersion = localStorage.getItem("skippedVersion") || null; let skippedVersion = localStorage.getItem("skippedVersion") || null;
if (skippedVersion && data.version !== skippedVersion) { if (skippedVersion && data.version !== skippedVersion) {
localStorage.removeItem("skippedVersion"); localStorage.removeItem("skippedVersion");
skippedVersion = null; skippedVersion = null;
} }
if (compareVersions(data.version, CURRENT_VERSION) > 0) { if (compareVersions(data.version, CURRENT_VERSION) > 0) {
const updateAvailableEl = document.getElementById("updateAvailable"); const updateAvailableEl = document.getElementById("updateAvailable");
updateAvailableEl.style.display = "inline"; updateAvailableEl.style.display = "inline";
updateAvailableEl.onclick = () => { updateAvailableEl.onclick = () => {
fetch(getUpdateUrl()) fetch(getUpdateUrl())
.then((response) => response.json()) .then((response) => response.json())
@ -43,13 +38,13 @@ function checkForUpdates() {
openUpdateModal(data); openUpdateModal(data);
}) })
.catch((error) => .catch((error) =>
console.error("Fehler beim erneuten Abrufen des Updates:", error) console.error("Error fetching update again:", error)
); );
}; };
} }
}) })
.catch((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) { function openUpdateModal(data) {
document.getElementById("updateVersion").textContent = data.version; document.getElementById("updateVersion").textContent = data.version;
let changelogContainer = document.getElementById("updateChanges"); let changelogContainer = document.getElementById("updateChanges");
changelogContainer.innerHTML = ""; changelogContainer.innerHTML = "";
if (Array.isArray(data.changelog)) { if (Array.isArray(data.changelog)) {
let ul = document.createElement("ul"); let ul = document.createElement("ul");
data.changelog.forEach(item => { data.changelog.forEach(item => {
@ -83,14 +75,11 @@ function openUpdateModal(data) {
}); });
changelogContainer.appendChild(ul); changelogContainer.appendChild(ul);
} else { } else {
changelogContainer.textContent = data.changelog || "No changelog available.";
changelogContainer.textContent = data.changelog || "Kein Changelog vorhanden.";
} }
window._updateData = data; window._updateData = data;
document.getElementById("updateModal").style.display = "block"; document.getElementById("updateModal").style.display = "block";
} }
@ -99,13 +88,11 @@ function closeUpdateModal() {
} }
function performUpdate() { function performUpdate() {
window.open("https://github.com/Gerald-Ha/HodlEye-Crypto-Price-Tracker", "_blank"); window.open("https://github.com/Gerald-Ha/HodlEye-Crypto-Price-Tracker", "_blank");
closeUpdateModal(); closeUpdateModal();
} }
function skipUpdate() { function skipUpdate() {
if (window._updateData && window._updateData.version) { if (window._updateData && window._updateData.version) {
localStorage.setItem("skippedVersion", window._updateData.version); localStorage.setItem("skippedVersion", window._updateData.version);
} }

View File

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

View File

@ -17,16 +17,27 @@ function writeData(data) {
router.get("/", (req, res) => { router.get("/", (req, res) => {
try { try {
const data = readData(); 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) { } catch (err) {
res.status(500).json({ error: "Fehler beim Lesen der Daten" }); res.status(500).json({ error: "Error reading data" });
} }
}); });
router.post("/", (req, res) => { router.post("/", (req, res) => {
const { symbol, price, frequency, direction } = req.body; const { symbol, price, frequency, direction } = req.body;
if (!symbol || !price) { 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 { try {
const data = readData(); const data = readData();
@ -42,7 +53,7 @@ router.post("/", (req, res) => {
writeData(data); writeData(data);
res.json(newAlarm); res.json(newAlarm);
} catch (err) { } 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); writeData(data);
res.json({ success: true, alarms: data.alarms }); res.json({ success: true, alarms: data.alarms });
} catch (err) { } 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 * Metadata
* Version: 1.5.1 * Version: 1.6.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
@ -28,7 +28,7 @@ app.use(session({
})); }));
app.use((req, res, next) => { 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"]; const staticFileExtensions = [".css", ".js", ".png", ".jpg", ".jpeg", ".svg"];
if (req.session.loggedIn || publicPaths.includes(req.path) || staticFileExtensions.some(ext => req.path.endsWith(ext))) { if (req.session.loggedIn || publicPaths.includes(req.path) || staticFileExtensions.some(ext => req.path.endsWith(ext))) {
return next(); 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) => { app.get("/logout", (req, res) => {
req.session.destroy(); req.session.destroy();
res.redirect("/login"); res.redirect("/login");
@ -66,6 +77,10 @@ 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();
let alarmCheckInterval;
let lastAlarmPrices = {};
const DATA_FILE = path.join(__dirname, "..", "data", "data.json"); const DATA_FILE = path.join(__dirname, "..", "data", "data.json");
if (!fs.existsSync(DATA_FILE)) { if (!fs.existsSync(DATA_FILE)) {
fs.writeFileSync(DATA_FILE, JSON.stringify({ cryptos: ["BTC"], alarms: [], notifications: [] }, null, 2)); 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 }); res.json({ success: true, cryptos: data.cryptos });
}); });
/* ------------- Notizen  ------------- */ /* ------------- Notices ------------- */
app.post("/api/portfolio/note", (req, res) => { app.post("/api/portfolio/note", (req, res) => {
const { id, note } = req.body; const { id, note } = req.body;
@ -531,16 +546,16 @@ app.get("/api/trade_summary", (req, res) => {
app.post("/api/portfolio/sell", (req, res) => { app.post("/api/portfolio/sell", (req, res) => {
const { id, sellAmount, sellPrice, sellDate } = req.body; const { id, sellAmount, sellPrice, sellDate } = req.body;
if (!id || isNaN(sellAmount) || isNaN(sellPrice) || !sellDate) { 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(); const pf = readPortfolio();
let txIndex = pf.transactions.findIndex(x => String(x.id) === String(id)); let txIndex = pf.transactions.findIndex(x => String(x.id) === String(id));
if (txIndex === -1) { 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]; let tx = pf.transactions[txIndex];
if (sellAmount > tx.amount) { 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 profit = (parseFloat(sellPrice) - parseFloat(tx.buyPrice)) * sellAmount;
const percentProfit = tx.buyPrice > 0 ? (profit / (tx.buyPrice * sellAmount)) * 100 : 0; 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) => { app.post("/api/portfolio/sell", (req, res) => {
const { id, sellAmount, sellPrice, sellDate } = req.body; const { id, sellAmount, sellPrice, sellDate } = req.body;
if (!id || isNaN(sellAmount) || isNaN(sellPrice) || !sellDate) { 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(); const pf = readPortfolio();
let txIndex = pf.transactions.findIndex(x => String(x.id) === String(id)); let txIndex = pf.transactions.findIndex(x => String(x.id) === String(id));
if (txIndex === -1) { 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]; let tx = pf.transactions[txIndex];
if (sellAmount > tx.amount) { 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 profit = (parseFloat(sellPrice) - parseFloat(tx.buyPrice)) * sellAmount;
const percentProfit = tx.buyPrice > 0 ? (profit / (tx.buyPrice * sellAmount)) * 100 : 0; const percentProfit = tx.buyPrice > 0 ? (profit / (tx.buyPrice * sellAmount)) * 100 : 0;
@ -683,13 +698,246 @@ function writePortfolio(data) {
wss.on("connection", (ws) => { wss.on("connection", (ws) => {
console.log(`🔗 New WebSocket client connected. Total: ${clients.size + 1}`);
clients.add(ws); clients.add(ws);
console.log(`📡 WebSocket client added. Current clients: ${clients.size}`);
ws.on("close", () => { ws.on("close", () => {
clients.delete(ws); 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; const PORT = process.env.PORT || 3099;
server.listen(PORT, () => { 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": [ "changelog": [
"Adding Note Option in Portfolio Page", "Revamped Alarm Modal with grouped alarms per coin and cleaner layout",
"Fixing Editing Option in Trade Summary" "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"
] ]
} }