mirror of
https://github.com/Gerald-Ha/HodlEye-Crypto-Price-Tracker.git
synced 2025-06-25 09:11:45 +00:00
Compare commits
No commits in common. "e2d93733a4b87e6297646f9bef28274246176a0f" and "819f36f26feaf61f3176c121c32395dfdc800f86" have entirely different histories.
e2d93733a4
...
819f36f26f
123
README.md
123
README.md
@ -1,4 +1,4 @@
|
||||
# HodlEye Crypto Price Tracker
|
||||
# HodlEye Crypto Portfolio & 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,9 +6,7 @@ A lightweight Docker-based web tool to monitor cryptocurrency prices (via Binanc
|
||||
|
||||
## Demo
|
||||
|
||||
Visit [HodlEye Demo](https://hodleye.gerald-hasani.com)
|
||||
|
||||
|
||||
Check out the live demo here: [HodlEye Demo](https://hodleye.gerald-hasani.com/)
|
||||
|
||||
**Default Login Data**
|
||||
**User:** admin
|
||||
@ -27,18 +25,20 @@ Visit [HodlEye Demo](https://hodleye.gerald-hasani.com)
|
||||
- [Alarm Functionality](#alarm-functionality)
|
||||
- [Crypto News](#crypto-news)
|
||||
- [Economic Calendar](#economic-calendar)
|
||||
- [TradingView Chart](#tradingview-chart)
|
||||
3. [Installation & Usage](#installation--usage)
|
||||
- [TradingView Chart](#tradingview-chart)
|
||||
3. [Portfolio Management](#portfolio-management)
|
||||
4. [Installation & Usage](#installation--usage)
|
||||
- [Requirements](#requirements)
|
||||
- [Environment variables (.env)](#environment-variables-env)
|
||||
- [Docker Build & Run](#docker-build--run)
|
||||
4. [Windows Notification App: HodlEye_Notify](#windows-notification-app-hodleye_notify)
|
||||
5. [Project Structure](#project-structure)
|
||||
5. [Windows Notification App: HodlEye_Notify](#windows-notification-app-hodleye_notify)
|
||||
6. [Project Structure](#project-structure)
|
||||
- [Frontend (index.html & magic.js)](#frontend-indexhtml--magicjs)
|
||||
- [News Feed Server (Node.js)](#news-feed-server-nodejs)
|
||||
6. [Important Notes / Limitations](#important-notes--limitations)
|
||||
7. [Coming Soon](#coming-soon)
|
||||
8. [Privacy & Data Disclaimer](#privacy--data-disclaimer)
|
||||
9. [License](#license)
|
||||
7. [Important Notes / Limitations](#important-notes--limitations)
|
||||
8. [Coming Soon](#coming-soon)
|
||||
9. [Privacy & Data Disclaimer](#privacy--data-disclaimer)
|
||||
10. [License](#license)
|
||||
|
||||
---
|
||||
|
||||
@ -115,8 +115,39 @@ 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">
|
||||
|
||||
---
|
||||
|
||||
|
||||
### 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">
|
||||
|
||||
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## Installation & Usage
|
||||
|
||||
### Requirements
|
||||
@ -124,6 +155,28 @@ The tool refreshes prices every **1 seconds**, which may introduce a slight dela
|
||||
- [Docker](https://www.docker.com/) installed.
|
||||
- (Optional) [Docker-Compose](https://docs.docker.com/compose/) if you want a more complex or multi-container setup.
|
||||
|
||||
|
||||
|
||||
---
|
||||
### 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.
|
||||
|
||||
|
||||
|
||||
---
|
||||
### Docker Build & Run
|
||||
|
||||
1. **Clone this repository**
|
||||
@ -203,6 +256,7 @@ Below is an example directory tree (based on your structure). Yours may vary sli
|
||||
|
||||
```
|
||||
HodlEye-Crypto-Price-Tracker
|
||||
├── .env
|
||||
├── Dockerfile
|
||||
├── LICENSE.txt
|
||||
├── PRIVACY.md
|
||||
@ -210,6 +264,11 @@ HodlEye-Crypto-Price-Tracker
|
||||
├── data
|
||||
│ └── data.json
|
||||
├── public
|
||||
│ ├── css
|
||||
│ │ ├── login.css
|
||||
│ │ ├── portfolio.css
|
||||
│ │ ├── responsive.css
|
||||
│ │ └── style.css
|
||||
│ ├── font
|
||||
│ │ └── BreeSerif-Regular.ttf
|
||||
│ ├── images
|
||||
@ -218,23 +277,22 @@ HodlEye-Crypto-Price-Tracker
|
||||
│ │ ├── favicon.png
|
||||
│ │ └── github-mark.svg
|
||||
│ ├── index.html
|
||||
│ ├── magic.js
|
||||
│ ├── news.js
|
||||
│ ├── tradingview.js
|
||||
│ ├── script.js
|
||||
│ ├── responsive.css
|
||||
│ ├── js
|
||||
│ │ ├── magic.js
|
||||
│ │ ├── news.js
|
||||
│ │ ├── portfolio.js
|
||||
│ │ ├── script.js
|
||||
│ │ ├── trade_summary.js
|
||||
│ │ ├── tradingview.js
|
||||
│ │ └── update.js
|
||||
│ ├── login.html
|
||||
│ ├── portfolio.html
|
||||
│ ├── sound
|
||||
│ │ ├── cashing.mp3
|
||||
│ │ └── ping.mp3
|
||||
│ ├── style.css
|
||||
│ └── update.js
|
||||
│ └── trade_summary.html
|
||||
├── server
|
||||
│ ├── newsfeed
|
||||
│ │ ├── node_modules
|
||||
│ │ ├── package-lock.json
|
||||
│ │ ├── package.json
|
||||
│ │ └── server.js
|
||||
│ ├── node_modules
|
||||
│ ├── package-lock.json
|
||||
│ ├── package.json
|
||||
│ └── server.js
|
||||
@ -242,6 +300,7 @@ HodlEye-Crypto-Price-Tracker
|
||||
│ ├── cashing.mp3
|
||||
│ └── ping.mp3
|
||||
└── update.json
|
||||
|
||||
```
|
||||
|
||||
|
||||
@ -306,14 +365,13 @@ _(Within Docker, it’s already bundled, so just expose `5001`.)_
|
||||
---
|
||||
|
||||
|
||||
## Coming Soon
|
||||
## Upcoming planned changes with the next versions
|
||||
|
||||
Exciting new features and improvements are on the way! Here are some planned updates:
|
||||
|
||||
- **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**
|
||||
- **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)
|
||||
- **HodlEye Notify Alarm with various sound selections and HodlEye Alarms**
|
||||
- **Windows HodlEye Notify Update**: Windows app bugfix and updates
|
||||
|
||||
|
||||
Stay tuned for updates!
|
||||
@ -329,9 +387,12 @@ 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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 🛡️ License
|
||||
Custom Non-Commercial License. See `LICENSE` file for details.
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
@ -238,9 +238,10 @@ body.light .crypto-box {
|
||||
|
||||
|
||||
#buyMeModal .modal-content {
|
||||
width: 400px;
|
||||
max-width: 90%;
|
||||
|
||||
width: 30%;
|
||||
overflow-x: hidden;
|
||||
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
top: 35%;
|
||||
@ -250,12 +251,9 @@ 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;
|
||||
@ -316,7 +314,6 @@ body.light .crypto-box {
|
||||
position: absolute;
|
||||
top: 35%;
|
||||
left: 50%;
|
||||
max-width: 100%;
|
||||
transform: translate(-50%, -45%);
|
||||
|
||||
flex-direction: column;
|
||||
@ -350,35 +347,6 @@ 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@ -485,80 +453,21 @@ body.light .api-label {
|
||||
|
||||
|
||||
.alarm-list-container {
|
||||
display: block;
|
||||
max-height: 200px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 2fr);
|
||||
gap: 10px;
|
||||
max-height: 300px;
|
||||
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: 3px 8px;
|
||||
padding: 6px;
|
||||
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 {
|
||||
@ -571,20 +480,12 @@ body.light .alarm-item {
|
||||
border: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
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;
|
||||
border-radius: 5px;
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
.alarm-delete-btn:hover {
|
||||
background-color: #005f7a;
|
||||
background-color: #005f73;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@ -840,279 +741,6 @@ body.light .alarm-item {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
#alarmModal .modal-content button:last-child {
|
||||
margin-top: 20px;
|
||||
}
|
@ -68,7 +68,7 @@
|
||||
</button>
|
||||
|
||||
<div class="version-info">
|
||||
<span id="currentVersion">Version 1.6.0</span>
|
||||
<span id="currentVersion">Version 1.5.1</span>
|
||||
<span
|
||||
id="updateAvailable"
|
||||
style="display: none; color: red; cursor: pointer"
|
||||
@ -201,50 +201,33 @@
|
||||
<div id="buyMeModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeBuyMeModal()">×</span>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@ -271,8 +254,8 @@
|
||||
<span class="close" onclick="closeCryptoNewsModal()">×</span>
|
||||
<h2>Crypto News</h2>
|
||||
|
||||
<input type="text" id="search" placeholder="Search for articles..." />
|
||||
<div id="news-feed">Loading news...</div>
|
||||
<input type="text" id="search" placeholder="Suche nach Artikeln..." />
|
||||
<div id="news-feed">Lade News...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -309,7 +292,7 @@
|
||||
<div id="updateModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closeUpdateModal()">×</span>
|
||||
<h2 id="updateTitle">Update to <span id="updateVersion"></span></h2>
|
||||
<h2 id="updateTitle">Update auf <span id="updateVersion"></span></h2>
|
||||
<p id="updateChanges"></p>
|
||||
|
||||
<button onclick="performUpdate()">Update</button>
|
||||
|
@ -27,7 +27,7 @@ async function loadCryptosFromServer() {
|
||||
cryptoList = await resp.json();
|
||||
renderCryptoGrid();
|
||||
} catch (err) {
|
||||
console.error("Error loading crypto list:", err);
|
||||
console.error("Fehler beim Laden der Kryptoliste:", err);
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,20 +72,9 @@ 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("Error loading alarms:", err);
|
||||
console.error("Fehler beim Laden der Alarme:", err);
|
||||
}
|
||||
}
|
||||
|
||||
@ -95,35 +84,18 @@ async function addAlarm() {
|
||||
const frequency = document.getElementById("alarmFrequency").value;
|
||||
const direction = document.getElementById("alarmDirection").value;
|
||||
|
||||
if (!symbol || isNaN(price)) {
|
||||
showErrorMessage("Please enter symbol and price!");
|
||||
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.");
|
||||
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 {
|
||||
const resp = await fetch("/api/alarms", {
|
||||
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}),
|
||||
});
|
||||
await resp.json();
|
||||
|
||||
|
||||
await loadAlarmsFromServer();
|
||||
|
||||
|
||||
document.getElementById("alarmPrice").value = "";
|
||||
document.getElementById("alarmPrice").focus();
|
||||
|
||||
|
||||
console.log("✅ Alarm successfully added");
|
||||
loadAlarmsFromServer();
|
||||
} catch (err) {
|
||||
showErrorMessage("Error adding alarm: " + err.message);
|
||||
}
|
||||
@ -145,7 +117,7 @@ async function loadNotificationsFromServer() {
|
||||
notifications = await resp.json();
|
||||
renderNotifications();
|
||||
} catch (err) {
|
||||
console.error("Error loading notifications:", err);
|
||||
console.error("Fehler beim Laden der Notifications:", err);
|
||||
}
|
||||
}
|
||||
|
||||
@ -158,7 +130,7 @@ async function addNotification(msg) {
|
||||
});
|
||||
loadNotificationsFromServer();
|
||||
} catch (err) {
|
||||
console.error("Error adding notification:", err);
|
||||
console.error("Fehler beim Hinzufügen einer Notification:", err);
|
||||
}
|
||||
}
|
||||
|
||||
@ -167,67 +139,29 @@ async function clearNotifications() {
|
||||
await fetch("/api/notifications", {method: "DELETE"});
|
||||
loadNotificationsFromServer();
|
||||
} catch (err) {
|
||||
console.error("Error deleting notifications:", err);
|
||||
console.error("Fehler beim Löschen der Notifications:", err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function init() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
document.getElementById("alarmSound").src = "sound/" + userOptions.soundFile;
|
||||
updateTheme(userOptions.darkMode);
|
||||
await loadCryptosFromServer();
|
||||
await loadAlarmsFromServer();
|
||||
await loadNotificationsFromServer();
|
||||
|
||||
|
||||
async function sortExistingAlarms() {
|
||||
try {
|
||||
const resp = await fetch("/api/alarms/sort", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" }
|
||||
setInterval(() => {
|
||||
cryptoList.forEach((symbol, index) => {
|
||||
const elementId = "crypto-" + index;
|
||||
fetchCryptoData(symbol, elementId).catch(() => setNotSupported(elementId));
|
||||
});
|
||||
const result = await resp.json();
|
||||
if (result.success) {
|
||||
console.log("✅ Existing alarms sorted");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error sorting existing alarms:", err);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
renderNotifications();
|
||||
}
|
||||
|
||||
|
||||
function renderCryptoGrid() {
|
||||
const grid = document.getElementById("cryptoGrid");
|
||||
grid.innerHTML = "";
|
||||
@ -320,26 +254,16 @@ 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;
|
||||
groupDiv.appendChild(groupHeader);
|
||||
const groupList = document.createElement("div");
|
||||
groupList.className = "alarm-group-list";
|
||||
container.appendChild(groupHeader);
|
||||
grouped[symbol].forEach((alarm) => {
|
||||
const item = document.createElement("div");
|
||||
item.className = "alarm-item";
|
||||
@ -351,10 +275,8 @@ function renderAlarmList() {
|
||||
delBtn.className = "alarm-delete-btn";
|
||||
delBtn.onclick = () => deleteAlarm(alarm.id);
|
||||
item.appendChild(delBtn);
|
||||
groupList.appendChild(item);
|
||||
container.appendChild(item);
|
||||
});
|
||||
groupDiv.appendChild(groupList);
|
||||
container.appendChild(groupDiv);
|
||||
});
|
||||
}
|
||||
|
||||
@ -393,24 +315,11 @@ 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() {
|
||||
@ -571,7 +480,7 @@ async function saveCryptoList() {
|
||||
body: JSON.stringify({cryptoList: cryptoList}),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error saving crypto list:", err);
|
||||
console.error("Fehler beim Speichern der Kryptoliste:", err);
|
||||
}
|
||||
}
|
||||
|
||||
@ -738,7 +647,7 @@ function updateCryptoBox({symbol, elementId, apiUsed, dailyOpen, hourlyOpen, las
|
||||
function checkAlarms(symbol, currentPrice) {
|
||||
alarms.forEach((alarm) => {
|
||||
if (alarm.symbol !== symbol) return;
|
||||
if (alarm.triggered && alarm.frequency === "Once") return;
|
||||
if (alarm.triggered) return;
|
||||
|
||||
const alarmPrice = parseFloat(alarm.price);
|
||||
const prevPrice = lastPrices[symbol] || null;
|
||||
@ -758,12 +667,10 @@ 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 if (alarm.frequency === "Recurring") {
|
||||
alarm.triggered = false;
|
||||
updateAlarmOnServer(alarm);
|
||||
} else {
|
||||
alarm.triggered = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -810,8 +717,8 @@ function copyToClipboard(address) {
|
||||
alert("Address is copied to the clipboard.");
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Clipboard API error:", err);
|
||||
alert("Error copying address.");
|
||||
console.error("Clipboard API Fehler:", err);
|
||||
alert("Fehler beim Kopieren der Adresse.");
|
||||
});
|
||||
} else {
|
||||
let textArea = document.createElement("textarea");
|
||||
@ -833,8 +740,8 @@ function copyToClipboard(address) {
|
||||
document.execCommand("copy");
|
||||
alert("Address is copied to the clipboard.");
|
||||
} catch (err) {
|
||||
console.error("Fallback copy error:", err);
|
||||
alert("Error copying address.");
|
||||
console.error("Fallback Copy Fehler:", err);
|
||||
alert("Fehler beim Kopieren der Adresse.");
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
@ -881,8 +788,8 @@ function copyToClipboard(address) {
|
||||
alert("Address is copied to the clipboard.");
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Clipboard API error:", err);
|
||||
alert("Error copying address.");
|
||||
console.error("Clipboard API Fehler:", err);
|
||||
alert("Fehler beim Kopieren der Adresse.");
|
||||
});
|
||||
} else {
|
||||
let textArea = document.createElement("textarea");
|
||||
@ -904,81 +811,9 @@ function copyToClipboard(address) {
|
||||
document.execCommand("copy");
|
||||
alert("Address is copied to the clipboard.");
|
||||
} catch (err) {
|
||||
console.error("Fallback copy error:", err);
|
||||
alert("Error copying address.");
|
||||
console.error("Fallback Copy Fehler:", err);
|
||||
alert("Fehler beim Kopieren der Adresse.");
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
const searchInput = document.getElementById("search");
|
||||
let allArticles = [];
|
||||
|
||||
|
||||
function refreshNewsFeed() {
|
||||
fetch(apiUrl)
|
||||
.then(response => response.json())
|
||||
@ -17,6 +18,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function displayArticles(items) {
|
||||
newsFeed.innerHTML = "";
|
||||
items.forEach(item => {
|
||||
@ -36,6 +38,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
searchInput.addEventListener("input", function () {
|
||||
const searchTerm = searchInput.value.toLowerCase();
|
||||
const filteredArticles = allArticles.filter(item =>
|
||||
@ -45,21 +48,23 @@ 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 `${seconds} seconds ago`;
|
||||
if (seconds < 60) return `vor ${seconds} Sekunden`;
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60) return `${minutes} minutes ago`;
|
||||
if (minutes < 60) return `vor ${minutes} Minuten`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return `${hours} hours ago`;
|
||||
if (hours < 24) return `vor ${hours} Stunden`;
|
||||
const days = Math.floor(hours / 24);
|
||||
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`;
|
||||
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`;
|
||||
}
|
||||
|
||||
|
||||
function formatSourceName(source) {
|
||||
const sourceMap = {
|
||||
"crypto_news": "Crypto News",
|
||||
@ -72,6 +77,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
return sourceMap[source] || source;
|
||||
}
|
||||
|
||||
|
||||
refreshNewsFeed();
|
||||
setInterval(refreshNewsFeed, 180000);
|
||||
});
|
||||
|
@ -256,10 +256,10 @@ function saveNote() {
|
||||
alert(j.error);
|
||||
} else {
|
||||
closeNoteModal();
|
||||
updatePrices();
|
||||
updatePrices(); //
|
||||
}
|
||||
})
|
||||
.catch(() => alert("Network error"));
|
||||
.catch(() => alert("Error saving note"));
|
||||
}
|
||||
|
||||
|
||||
@ -290,7 +290,7 @@ function confirmBuy() {
|
||||
const buyDate = document.getElementById("buyDate").value;
|
||||
|
||||
if (!symbol || isNaN(amount) || isNaN(buyPrice)) {
|
||||
alert("Please enter valid values");
|
||||
alert("Bitte gültige Werte eingeben");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -299,7 +299,7 @@ function confirmBuy() {
|
||||
.then(r => r.json())
|
||||
.then(j => {
|
||||
if (j.error) {
|
||||
alert("This coin is not supported");
|
||||
alert("Dieser Coin wird nicht unterstützt");
|
||||
} else {
|
||||
fetch("/api/portfolio/buy", {
|
||||
method: "POST",
|
||||
@ -324,7 +324,7 @@ function confirmBuy() {
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => alert("Network error"));
|
||||
.catch(() => alert("Netzwerkfehler"));
|
||||
}
|
||||
|
||||
function confirmSell() {
|
||||
@ -337,7 +337,7 @@ function confirmSell() {
|
||||
const sellDate = document.getElementById("sellDate").value;
|
||||
|
||||
if (isNaN(sellAmount) || isNaN(sellPrice) || !sellDate) {
|
||||
alert("Please enter valid values");
|
||||
alert("Bitte gültige Werte eingeben");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -358,7 +358,7 @@ function confirmSell() {
|
||||
}
|
||||
|
||||
if (!selectedTransaction) {
|
||||
alert("Invalid");
|
||||
alert("Ungültig");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -382,7 +382,7 @@ function confirmSell() {
|
||||
closeAddTransactionModal();
|
||||
}
|
||||
})
|
||||
.catch(() => alert("Error during sell"));
|
||||
.catch(() => alert("Fehler beim 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("No valid transaction found");
|
||||
alert("Keine gültige Transaktion gefunden");
|
||||
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("No valid transaction found");
|
||||
alert("Keine gültige Transaktion gefunden");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -434,7 +434,7 @@ function saveEditedTransaction() {
|
||||
const newDate = document.getElementById("editDate").value;
|
||||
|
||||
if (isNaN(newAmount) || isNaN(newBuyPrice)) {
|
||||
alert("Please enter valid values");
|
||||
alert("Bitte gültige Werte eingeben");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -459,7 +459,7 @@ function saveEditedTransaction() {
|
||||
updatePrices();
|
||||
}
|
||||
})
|
||||
.catch(() => alert("Error during edit"));
|
||||
.catch(() => alert("Fehler bei Edit"));
|
||||
}
|
||||
|
||||
function deleteTransaction() {
|
||||
|
@ -1,3 +1,4 @@
|
||||
|
||||
document.querySelectorAll('.modal').forEach(modal => {
|
||||
modal.addEventListener('click', function(event) {
|
||||
|
||||
@ -18,5 +19,5 @@ document.querySelectorAll('.modal').forEach(modal => {
|
||||
if (response.redirected) {
|
||||
window.location.href = response.url;
|
||||
}
|
||||
}).catch(error => console.error("Logout error:", error));
|
||||
}).catch(error => console.error("Logout-Fehler:", error));
|
||||
}
|
@ -19,7 +19,7 @@ function loadTradeSummary() {
|
||||
renderTradeSummary(allTrades);
|
||||
updateBottomBar(allTrades);
|
||||
})
|
||||
.catch(err => console.error("Error loading trade summary", err));
|
||||
.catch(err => console.error("Fehler beim Laden der Trade Summary", err));
|
||||
}
|
||||
|
||||
function renderTradeSummary(trades) {
|
||||
|
@ -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 script correctly added to container.");
|
||||
console.log("TradingView-Widget-Skript korrekt dem Container hinzugefügt.");
|
||||
} else {
|
||||
console.error("Error: Widget container not found.");
|
||||
console.error("Fehler: Widget-Container nicht gefunden.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,5 +59,5 @@ function closeTradingViewModal() {
|
||||
|
||||
const container = document.getElementById("tradingViewModalContent");
|
||||
container.innerHTML = "";
|
||||
console.log("TradingView modal closed and content cleared.");
|
||||
console.log("TradingView-Modal geschlossen und Inhalt bereinigt.");
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
const CURRENT_VERSION = "1.6.0";
|
||||
const CURRENT_VERSION = "1.5.1";
|
||||
|
||||
function getUpdateUrl() {
|
||||
return "/api/update?t=" + new Date().getTime();
|
||||
@ -8,7 +8,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
document.getElementById("currentVersion").textContent = `Version ${CURRENT_VERSION}`;
|
||||
checkForUpdates();
|
||||
|
||||
|
||||
setInterval(checkForUpdates, 86400000);
|
||||
|
||||
});
|
||||
|
||||
function checkForUpdates() {
|
||||
@ -17,15 +19,18 @@ 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())
|
||||
@ -38,13 +43,13 @@ function checkForUpdates() {
|
||||
openUpdateModal(data);
|
||||
})
|
||||
.catch((error) =>
|
||||
console.error("Error fetching update again:", error)
|
||||
console.error("Fehler beim erneuten Abrufen des Updates:", error)
|
||||
);
|
||||
};
|
||||
}
|
||||
})
|
||||
.catch((error) =>
|
||||
console.error("Error fetching update:", error)
|
||||
console.error("Fehler beim Abrufen des Updates:", error)
|
||||
);
|
||||
}
|
||||
|
||||
@ -61,11 +66,14 @@ 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 => {
|
||||
@ -75,11 +83,14 @@ function openUpdateModal(data) {
|
||||
});
|
||||
changelogContainer.appendChild(ul);
|
||||
} else {
|
||||
changelogContainer.textContent = data.changelog || "No changelog available.";
|
||||
|
||||
changelogContainer.textContent = data.changelog || "Kein Changelog vorhanden.";
|
||||
}
|
||||
|
||||
|
||||
window._updateData = data;
|
||||
|
||||
|
||||
document.getElementById("updateModal").style.display = "block";
|
||||
}
|
||||
|
||||
@ -88,11 +99,13 @@ 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);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="de">
|
||||
<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">
|
||||
From <input type="date" id="filterFromDate">
|
||||
To <input type="date" id="filterToDate">
|
||||
Von <input type="date" id="filterFromDate">
|
||||
Bis <input type="date" id="filterToDate">
|
||||
</label>
|
||||
</div>
|
||||
<div style="margin-top: 1rem;">
|
||||
|
@ -17,27 +17,16 @@ function writeData(data) {
|
||||
router.get("/", (req, res) => {
|
||||
try {
|
||||
const data = readData();
|
||||
|
||||
|
||||
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);
|
||||
res.json(data.alarms);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: "Error reading data" });
|
||||
res.status(500).json({ error: "Fehler beim Lesen der Daten" });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/", (req, res) => {
|
||||
const { symbol, price, frequency, direction } = req.body;
|
||||
if (!symbol || !price) {
|
||||
return res.status(400).json({ error: "Symbol and price are required." });
|
||||
return res.status(400).json({ error: "Symbol und Preis sind erforderlich." });
|
||||
}
|
||||
try {
|
||||
const data = readData();
|
||||
@ -53,7 +42,7 @@ router.post("/", (req, res) => {
|
||||
writeData(data);
|
||||
res.json(newAlarm);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: "Error writing data" });
|
||||
res.status(500).json({ error: "Fehler beim Schreiben der Daten" });
|
||||
}
|
||||
});
|
||||
|
||||
@ -65,49 +54,7 @@ router.delete("/:id", (req, res) => {
|
||||
writeData(data);
|
||||
res.json({ success: true, alarms: data.alarms });
|
||||
} catch (err) {
|
||||
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" });
|
||||
res.status(500).json({ error: "Fehler beim Löschen des Alarms" });
|
||||
}
|
||||
});
|
||||
|
||||
|
268
server/server.js
268
server/server.js
@ -10,7 +10,7 @@ const fetch = require("node-fetch");
|
||||
|
||||
/*
|
||||
* Metadata
|
||||
* Version: 1.6.0
|
||||
* Version: 1.5.1
|
||||
* 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", "/api/test-alarm", "/api/create-test-alarm", "/api/test-notification"];
|
||||
const publicPaths = ["/login", "/login.html"];
|
||||
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,17 +53,6 @@ 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");
|
||||
@ -77,10 +66,6 @@ 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));
|
||||
@ -202,7 +187,7 @@ app.put("/api/cryptos", (req, res) => {
|
||||
res.json({ success: true, cryptos: data.cryptos });
|
||||
});
|
||||
|
||||
/* ------------- Notices ------------- */
|
||||
/* ------------- Notizen ------------- */
|
||||
|
||||
app.post("/api/portfolio/note", (req, res) => {
|
||||
const { id, note } = req.body;
|
||||
@ -546,16 +531,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: "Invalid data" });
|
||||
return res.status(400).json({ error: "Ungültige Daten" });
|
||||
}
|
||||
const pf = readPortfolio();
|
||||
let txIndex = pf.transactions.findIndex(x => String(x.id) === String(id));
|
||||
if (txIndex === -1) {
|
||||
return res.status(400).json({ error: "Transaction not found" });
|
||||
return res.status(400).json({ error: "Transaktion nicht gefunden" });
|
||||
}
|
||||
let tx = pf.transactions[txIndex];
|
||||
if (sellAmount > tx.amount) {
|
||||
return res.status(400).json({ error: "Selling amount exceeds available amount" });
|
||||
return res.status(400).json({ error: "Verkaufsmenge überschreitet die vorhandene Menge" });
|
||||
}
|
||||
const profit = (parseFloat(sellPrice) - parseFloat(tx.buyPrice)) * sellAmount;
|
||||
const percentProfit = tx.buyPrice > 0 ? (profit / (tx.buyPrice * sellAmount)) * 100 : 0;
|
||||
@ -598,16 +583,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: "Invalid data" });
|
||||
return res.status(400).json({ error: "Ungültige Daten" });
|
||||
}
|
||||
const pf = readPortfolio();
|
||||
let txIndex = pf.transactions.findIndex(x => String(x.id) === String(id));
|
||||
if (txIndex === -1) {
|
||||
return res.status(400).json({ error: "Transaction not found" });
|
||||
return res.status(400).json({ error: "Transaktion nicht gefunden" });
|
||||
}
|
||||
let tx = pf.transactions[txIndex];
|
||||
if (sellAmount > tx.amount) {
|
||||
return res.status(400).json({ error: "Selling amount exceeds available amount" });
|
||||
return res.status(400).json({ error: "Verkaufsmenge überschreitet die vorhandene Menge" });
|
||||
}
|
||||
const profit = (parseFloat(sellPrice) - parseFloat(tx.buyPrice)) * sellAmount;
|
||||
const percentProfit = tx.buyPrice > 0 ? (profit / (tx.buyPrice * sellAmount)) * 100 : 0;
|
||||
@ -698,246 +683,13 @@ 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 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" });
|
||||
}
|
||||
console.log("Server läuft auf Port " + PORT);
|
||||
});
|
||||
|
24
update.json
24
update.json
@ -1,24 +1,8 @@
|
||||
{
|
||||
"version": "1.6.0",
|
||||
"version": "1.5.1",
|
||||
"changelog": [
|
||||
"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"
|
||||
"Adding Note Option in Portfolio Page",
|
||||
"Fixing Editing Option in Trade Summary"
|
||||
|
||||
]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user