Compare commits

...

10 Commits

Author SHA1 Message Date
Gerald-H
2f29f21519
Update README.md 2025-03-09 03:44:07 +01:00
92917dbeaa Update Fix 2025-03-09 03:18:00 +01:00
55d15cf147 fix 2025-03-09 02:35:50 +01:00
5554024131 fix 2025-03-09 02:28:42 +01:00
e297acdcdc fix 2025-03-09 02:25:28 +01:00
9dd80f704c fix 2025-03-09 02:17:34 +01:00
06cfc8244a Merge remote-tracking branch 'origin/main' 2025-03-09 02:13:33 +01:00
f5c7e607df url fix 2025-03-09 02:13:23 +01:00
Gerald-H
23500f0c20
Update update.json 2025-03-09 02:09:23 +01:00
f751559878 Release 1.0.6
- Responsive design addet
- Crypto News Auto Update without Browser Refresh every 3 minutes
- Crypto Price Refresh Reduce from 5 Second to 1 Second
- Docker command updated to saved alarms and registered cryptocurrencies that are still there after a Docker image update
--> docker run -p 3099:3099 -p 5001:5001 -v hodleye_data:/app/data --name hodleye-container hodleye-crypto-tracker

- Bugfix "Edit List" works now
- Addet Update Notification every 5 Days
- re-Design from Crypto-box
- improving the logic of the price display for small cryptocurrencies
2025-03-09 02:07:54 +01:00
13 changed files with 1360 additions and 926 deletions

View File

@ -1,4 +1,4 @@
MIT License
Copyright (c) 2025 Gerald-H

108
README.md
View File

@ -4,8 +4,6 @@ A lightweight Docker-based web tool to monitor cryptocurrency prices (via Binanc
<img src="https://github.com/user-attachments/assets/d87ca663-97be-4c22-a0ab-46505fe9c99f" width="800" height="auto">
---
## Table of Contents
@ -37,28 +35,35 @@ A lightweight Docker-based web tool to monitor cryptocurrency prices (via Binanc
- **Unlimited Alarms**: No cap on the number of alarms you can set.
- **Unlimited Crypto Tracking**: Easily add as many coins as you want.
- **Real-Time Price Updates (every 5 seconds)**: Uses Binance and OKX data.
- **Real-Time Price Updates (every 1 seconds)**: Uses Binance and OKX data.
- **Crypto News & Economic Calendar**: Stay updated on the latest events affecting the market.
The tool refreshes prices every **5 seconds**, which may introduce a slight delay in alarm triggers if the price quickly touches the threshold in between intervals.
The tool refreshes prices every **1 seconds**, which may introduce a slight delay in alarm triggers if the price quickly touches the threshold in between intervals.
---
&nbsp;
## Features
### Unlimited Alarms & Tracking
- You can set **as many alarms as you like** — no daily or total limit.
- Track **any number of cryptocurrencies** in the list simultaneously.
<img src="https://github.com/user-attachments/assets/276de04d-dcf6-411a-b550-cbb5104c1579" width="auto" height="400">
&nbsp;
### Price Updates
- **Binance** and **OKX** are integrated as the primary data sources.
- By default, HodlEye tries Binance first; if that fails or is forced off, it falls back to OKX.
- It automatically fetches the current price, 24h open, 1h open, and calculates the 24h and 1h percentage changes every **5 seconds**.
- It automatically fetches the current price, 24h open, 1h open, and calculates the 24h and 1h percentage changes every **1 seconds**.
&nbsp;
### Alarm Functionality
- Set alarms for each coin (e.g., `BTC/USDT`), choosing:
- **Alarm Price** (threshold)
- **Direction** (Rising, Falling, or Both)
@ -66,55 +71,62 @@ The tool refreshes prices every **5 seconds**, which may introduce a slight dela
- When triggered, a popup and sound notification appear, with optional desktop notifications.
- **Once** alarms are marked locally in the browser (not removed from the server) so they do not trigger again unless reloaded or manually reset.
&nbsp;
### Crypto News
- News from multiple RSS sources:
- `https://crypto.news/feed/`
- `https://cointelegraph.com/rss`
- `https://thedefiant.io/api/feed`
- `https://newsbtc.com/feed`
- `https://news.bitcoin.com/feed` *(may be inaccessible in certain regions)*
- `https://news.bitcoin.com/feed` _(may be inaccessible in certain regions)_
- `https://bitcoinmagazine.com/feed`
- `https://cryptopanic.com/news/rss/`
- `https://decrypt.co/feed`
- Quickly view and filter recent articles within the built-in News modal.
<img src="https://github.com/user-attachments/assets/f0727b39-a075-4d50-9600-f53c803d4a1b" width="auto" height="400">
&nbsp;
### Economic Calendar
- The **Economic Calendar** button opens a modal with an [Investing.com](https://www.investing.com/) iframe, showing major economic events such as central bank decisions and market-impacting announcements.
<img src="https://github.com/user-attachments/assets/e254301e-9aaa-48d8-84e7-6faa598ca8be" width="600" height="auto">
---
&nbsp;
## Installation & Usage
### Requirements
- [Docker](https://www.docker.com/) installed.
- (Optional) [Docker-Compose](https://docs.docker.com/compose/) if you want a more complex or multi-container setup.
### Docker Build & Run
1. **Clone this repository**
```bash
git clone https://github.com/YourGitHubName/HodlEye.git
git clone https://github.com/Gerald-Ha/HodlEye-Crypto-Price-Tracker.git
cd HodlEye
```
2. **Build the Docker image**
```bash
docker buildx build -t hodleye-crypto-tracker .
```
*(Make sure youre in the same directory as the Dockerfile.)*
_(Make sure youre in the same directory as the Dockerfile.)_
3. **Run the container**
```bash
docker run -d -p 3099:3099 -p 5001:5001 --name hodleye-container hodleye-crypto-tracker
docker run -p 3099:3099 -p 5001:5001 -v hodleye_data:/app/data --name hodleye-container hodleye-crypto-tracker
```
- Port `3099` serves the main web interface.
- Port `5001` is used by the Node.js server that fetches news RSS feeds.
@ -123,37 +135,48 @@ The tool refreshes prices every **5 seconds**, which may introduce a slight dela
- **News Feed Endpoint**: [http://localhost:5001/api/news](http://localhost:5001/api/news)
---
&nbsp;
## Windows Notification App: HodlEye_Notify
<img src="https://github.com/user-attachments/assets/a3356708-1b3a-4fb8-9a71-d1af88f29c5f" width="auto" height="150">
<img src="https://github.com/user-attachments/assets/cb0eb2a8-45fb-4a80-bb81-56957e838153" width="auto" height="150">
If you prefer not to keep the HodlEye Crypto Price Tracker web interface open in your browser all the time, you can use a lightweight Windows application called **HodlEye_Notify**. This tool connects directly to the same endpoint as the Docker container and will display notifications on your desktop whenever an alarm is triggered.
1. **Setup**
- Enter the IP address and port of your HodlEye Docker container (for example, `http://192.168.1.112:3099/`) in the HodlEye_Notify window.
- Click **Connect** to establish a WebSocket connection.
- Once connected, youll see the status change to “Connected.”
&nbsp;
1. **Setup**
2. **Autostart**
- Add HodlEye_Notify to your Windows **Startup** folder so it automatically launches when Windows starts. This way, youll continuously receive notifications without needing to reopen the program manually.
- Enter the IP address and port of your HodlEye Docker container (for example, `http://192.168.1.112:3099/`) in the HodlEye_Notify window.
- Click **Connect** to establish a WebSocket connection.
- Once connected, youll see the status change to “Connected.”
3. **Testing Notifications**
- From the machine running the Docker container, you can trigger a test notification using the following `curl` command:
```bash
2. **Autostart**
- Add HodlEye_Notify to your Windows **Startup** folder so it automatically launches when Windows starts. This way, youll continuously receive notifications without needing to reopen the program manually.
3. **Testing Notifications**
- From the machine running the Docker container, you can trigger a test notification using the following `curl` command:
**Ubuntu**
```bash
curl -X POST http://192.168.1.112:3099/api/notifications \
-H "Content-Type: application/json" \
-d "{\"message\": \"⚠️ ALARM (Recurring, Both): BTC reached 92250\", \"timestamp\": \"2025-03-06T06:19:58.584Z\"}"
```
- If everything is configured correctly, you should receive a desktop notification from HodlEye_Notify indicating the alarm has triggered.
```
**Windows CMD**
```bash
curl -X POST http://192.168.1.112:3099/api/notifications -H "Content-Type: application/json" -d "{\"message\": \"⚠️ ALARM (Recurring, Both): BTC reached 92250\", \"timestamp\": \"2025-03-06T06:19:58.584Z\"}"
```
- If everything is configured correctly, you should receive a desktop notification from HodlEye_Notify indicating the alarm has triggered.
This application simplifies the process of staying informed about your alarms, letting you work on other tasks without leaving the HodlEye web interface open.
---
&nbsp;
## Project Structure
Below is an example directory tree (based on your structure). Yours may vary slightly:
@ -191,7 +214,9 @@ HodlEye-Crypto-Price-Tracker
└── ping.mp3
```
&nbsp;
### Frontend (`index.html` & `magic.js`)
- **`index.html`**
- Main interface containing modals and buttons (Add Crypto, Edit List, Alarms, Options, etc.).
- Includes buttons for:
@ -204,11 +229,13 @@ HodlEye-Crypto-Price-Tracker
- Core logic:
- Fetches cryptos (`/api/cryptos`)
- Loads alarms (`/api/alarms`) and notifications (`/api/notifications`)
- Pulls prices from Binance/OKX every 5 seconds
- Pulls prices from Binance/OKX every 1 seconds
- Checks and triggers alarms
- Handles UI rendering (prices, alarms, notifications, drag & drop reorder)
&nbsp;
### News Feed Server (Node.js)
- A minimal Node.js Express server (in `server.js` or similar) which:
- Retrieves the listed RSS feeds and parses them via `xml2js`
- Serves them in JSON format at `/api/news`
@ -221,20 +248,24 @@ HodlEye-Crypto-Price-Tracker
```
- Accessible at [http://localhost:5001/api/news](http://localhost:5001/api/news).
*(Within Docker, its already bundled, so just expose `5001`.)*
_(Within Docker, its already bundled, so just expose `5001`.)_
---
&nbsp;
## Important Notes / Limitations
1. **5-second polling**
- Theres a potential delay in alarms because price thresholds are only checked every 5 seconds. If a price briefly touches and moves away between polls, you might miss that exact trigger moment.
1. **1-second polling**
- Theres a potential delay in alarms because price thresholds are only checked every 1 seconds. If a price briefly touches and moves away between polls, you might miss that exact trigger moment.
2. **API availability**
- Binance/OKX may be temporarily down or might not support certain symbols.
- HodlEye tries Binance → fallback to OKX if needed.
3. **Unlimited Alarms (Once vs. Recurring)**
- **Once** alarms become locally “triggered” to avoid repeated alerts but are not server-side deactivated.
- **Recurring** triggers repeatedly every time the threshold is crossed.
@ -244,6 +275,7 @@ HodlEye-Crypto-Price-Tracker
---
&nbsp;
## Coming Soon
Exciting new features and improvements are on the way! Here are some planned updates:
@ -252,20 +284,24 @@ Exciting new features and improvements are on the way! Here are some planned upd
- **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**
- **Adding more Responsive design**
Stay tuned for updates!
---
---
&nbsp;
## Privacy & Data Disclaimer
- **No Data Collection by This Application**: HodlEye itself does not collect, store, or process any personal data or usage analytics.
- **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;

8
data/data.json Normal file
View File

@ -0,0 +1,8 @@
{
"cryptos": [
"BTC",
"ETH"
],
"alarms": [],
"notifications": []
}

View File

@ -0,0 +1 @@
<svg version="1.1" id="main_outline" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" style="enable-background:new 0 0 640 640;" xml:space="preserve" viewBox="5.67 143.05 628.65 387.55"> <g> <path id="teabag" style="fill:#FFFFFF" d="M395.9,484.2l-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5,21.2-17.9,33.8-11.8 c17.2,8.3,27.1,13,27.1,13l-0.1-109.2l16.7-0.1l0.1,117.1c0,0,57.4,24.2,83.1,40.1c3.7,2.3,10.2,6.8,12.9,14.4 c2.1,6.1,2,13.1-1,19.3l-61,126.9C423.6,484.9,408.4,490.3,395.9,484.2z"></path> <g> <g> <path style="fill:#609926" d="M622.7,149.8c-4.1-4.1-9.6-4-9.6-4s-117.2,6.6-177.9,8c-13.3,0.3-26.5,0.6-39.6,0.7c0,39.1,0,78.2,0,117.2 c-5.5-2.6-11.1-5.3-16.6-7.9c0-36.4-0.1-109.2-0.1-109.2c-29,0.4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5 c-9.8-0.6-22.5-2.1-39,1.5c-8.7,1.8-33.5,7.4-53.8,26.9C-4.9,212.4,6.6,276.2,8,285.8c1.7,11.7,6.9,44.2,31.7,72.5 c45.8,56.1,144.4,54.8,144.4,54.8s12.1,28.9,30.6,55.5c25,33.1,50.7,58.9,75.7,62c63,0,188.9-0.1,188.9-0.1s12,0.1,28.3-10.3 c14-8.5,26.5-23.4,26.5-23.4s12.9-13.8,30.9-45.3c5.5-9.7,10.1-19.1,14.1-28c0,0,55.2-117.1,55.2-231.1 C633.2,157.9,624.7,151.8,622.7,149.8z M125.6,353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6,321.8,60,295.4 c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5,38.5-30c13.8-3.7,31-3.1,31-3.1s7.1,59.4,15.7,94.2c7.2,29.2,24.8,77.7,24.8,77.7 S142.5,359.9,125.6,353.9z M425.9,461.5c0,0-6.1,14.5-19.6,15.4c-5.8,0.4-10.3-1.2-10.3-1.2s-0.3-0.1-5.3-2.1l-112.9-55 c0,0-10.9-5.7-12.8-15.6c-2.2-8.1,2.7-18.1,2.7-18.1L322,273c0,0,4.8-9.7,12.2-13c0.6-0.3,2.3-1,4.5-1.5c8.1-2.1,18,2.8,18,2.8 l110.7,53.7c0,0,12.6,5.7,15.3,16.2c1.9,7.4-0.5,14-1.8,17.2C474.6,363.8,425.9,461.5,425.9,461.5z"></path> <path style="fill:#609926" d="M326.8,380.1c-8.2,0.1-15.4,5.8-17.3,13.8c-1.9,8,2,16.3,9.1,20c7.7,4,17.5,1.8,22.7-5.4 c5.1-7.1,4.3-16.9-1.8-23.1l24-49.1c1.5,0.1,3.7,0.2,6.2-0.5c4.1-0.9,7.1-3.6,7.1-3.6c4.2,1.8,8.6,3.8,13.2,6.1 c4.8,2.4,9.3,4.9,13.4,7.3c0.9,0.5,1.8,1.1,2.8,1.9c1.6,1.3,3.4,3.1,4.7,5.5c1.9,5.5-1.9,14.9-1.9,14.9 c-2.3,7.6-18.4,40.6-18.4,40.6c-8.1-0.2-15.3,5-17.7,12.5c-2.6,8.1,1.1,17.3,8.9,21.3c7.8,4,17.4,1.7,22.5-5.3 c5-6.8,4.6-16.3-1.1-22.6c1.9-3.7,3.7-7.4,5.6-11.3c5-10.4,13.5-30.4,13.5-30.4c0.9-1.7,5.7-10.3,2.7-21.3 c-2.5-11.4-12.6-16.7-12.6-16.7c-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3c4.7-9.7,9.4-19.3,14.1-29 c-4.1-2-8.1-4-12.2-6.1c-4.8,9.8-9.7,19.7-14.5,29.5c-6.7-0.1-12.9,3.5-16.1,9.4c-3.4,6.3-2.7,14.1,1.9,19.8 C343.2,346.5,335,363.3,326.8,380.1z"></path> </g> </g> </g> </svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1 @@
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#24292f"/></svg>

After

Width:  |  Height:  |  Size: 963 B

View File

@ -1,258 +1,295 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>HodlEye Crypto Price Tracker</title>
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/png" href="images/favicon.png">
</head>
<body class="dark">
<link rel="stylesheet" href="style.css" />
<link rel="stylesheet" href="responsive.css" />
<link rel="icon" type="image/png" href="images/favicon.png" />
</head>
<body class="dark">
<div class="header">
<h1>HodlEye Crypto Price Tracker</h1>
<div class="button-grid-container">
<div class="grid-left">
<button onclick="openCryptoNews()">Crypto News</button>
<button onclick="openEconomicCalendar()">Economic Calendar</button>
</div>
<div class="grid-middle">
<button onclick="openAddCryptoModal()">Add Crypto</button>
<button onclick="toggleEditMode()" id="editButton">Edit List</button>
<button class="alarm-btn" onclick="openAlarmModal()">Alarms⏰</button>
<button class="options-btn" onclick="openOptionsModal()">Options</button>
</div>
<div class="grid-right">
</div>
<h1>HodlEye Crypto Price Tracker</h1>
<div class="button-grid-container">
<div class="grid-left">
<button onclick="openCryptoNews()">Crypto News</button>
<button onclick="openEconomicCalendar()">Economic Calendar</button>
</div>
<div class="grid-middle">
<button onclick="openAddCryptoModal()">Add Crypto</button>
<button onclick="toggleEditMode()" id="editButton">Edit List</button>
<button class="alarm-btn" onclick="openAlarmModal()">Alarms</button>
<button class="options-btn" onclick="openOptionsModal()">
Options
</button>
</div>
<div class="grid-right"></div>
</div>
</div>
<div class="main-container">
<div class="left-column">
<div class="grid-container" id="cryptoGrid"></div>
</div>
<div class="left-column">
<div class="grid-container" id="cryptoGrid">
</div>
<div class="right-column">
<div class="notify-area">
<div class="notify-heading">
<span>Notify</span>
<button onclick="clearNotifications()">Clear List</button>
</div>
<ul class="notify-list" id="notifyList"></ul>
</div>
<div class="buttons-and-version">
<button class="coffee-btn" onclick="openBuyMeModal()">
Buy me a Coffee
<img src="images/coffee.svg" alt="Coffee" class="coffee-icon" />
</button>
<div class="right-column">
<div class="notify-heading">
<span>Notify</span>
<button onclick="clearNotifications()">Clear List</button>
</div>
<ul class="notify-list" id="notifyList">
<button
class="my-button"
onclick="window.open('https://github.com/Gerald-Ha/HodlEye-Crypto-Price-Tracker', '_blank')"
>
Github
<img
src="images/github-mark.svg"
alt="Git Logo"
class="coffee-icon"
/>
</button>
</ul>
<button class="coffee-btn" onclick="openBuyMeModal()">
Buy me a Coffee <img src="images/coffee.svg" alt="Coffee" class="coffee-icon">
</button>
<button onclick="window.open('https://www.gerald-hasani.com', '_blank')">Developer</button>
<div class="version-info">
<span id="currentVersion">Version 1.0.6</span>
<span
id="updateAvailable"
style="display: none; color: red; cursor: pointer"
>
Update Available
</span>
</div>
</div>
</div>
</div>
<div id="alarmModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeAlarmModal()">&times;</span>
<h2>Price Alarms</h2>
<div class="modal-content">
<span class="close" onclick="closeAlarmModal()">&times;</span>
<h2>Price Alarms</h2>
<div class="alarm-list-container" id="alarmListContainer">
<div class="alarm-list-container" id="alarmListContainer"></div>
</div>
<label for="alarmSymbol">Symbol:</label>
<select id="alarmSymbol"></select
><br />
<label for="alarmSymbol">Symbol:</label>
<select id="alarmSymbol"></select><br>
<label for="alarmPrice">Alarm Price (USDT):</label>
<input type="number" id="alarmPrice" placeholder="Price" /><br />
<label for="alarmPrice">Alarm Price (USDT):</label>
<input type="number" id="alarmPrice" placeholder="Price"><br>
<label for="alarmFrequency">Frequency:</label>
<select id="alarmFrequency">
<option value="Once">Once</option>
<option value="Recurring">Recurring</option></select
><br />
<label for="alarmFrequency">Frequency:</label>
<select id="alarmFrequency">
<option value="Once">Once</option>
<option value="Recurring">Recurring</option>
</select><br>
<label for="alarmDirection">Direction:</label>
<select id="alarmDirection">
<option value="Rising">Rising</option>
<option value="Falling">Falling</option>
<option value="Both">Both</option></select
><br />
<label for="alarmDirection">Direction:</label>
<select id="alarmDirection">
<option value="Rising">Rising</option>
<option value="Falling">Falling</option>
<option value="Both">Both</option>
</select><br>
<button onclick="addAlarm()">Add Alarm</button>
</div>
<button onclick="addAlarm()">Add Alarm</button>
</div>
</div>
<div id="optionsModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeOptionsModal()">&times;</span>
<h2>Options</h2>
<div class="modal-content">
<span class="close" onclick="closeOptionsModal()">&times;</span>
<h2>Options</h2>
<label for="soundSelect">Alarm Sound:</label>
<select id="soundSelect">
<option value="ping.mp3">ping.mp3</option>
<option value="cashing.mp3">cashing.mp3</option>
</select>
<br><br>
<label for="soundSelect">Alarm Sound:</label>
<select id="soundSelect">
<option value="ping.mp3">ping.mp3</option>
<option value="cashing.mp3">cashing.mp3</option>
</select>
<br /><br />
<input type="checkbox" id="darkModeToggle">
<label for="darkModeToggle">Enable Dark Mode</label>
<br><br>
<input type="checkbox" id="darkModeToggle" />
<label for="darkModeToggle">Enable Dark Mode</label>
<br /><br />
<input type="checkbox" id="desktopNotifyToggle">
<label for="desktopNotifyToggle">Enable Desktop Notifications</label>
<br><br>
<input type="checkbox" id="desktopNotifyToggle" />
<label for="desktopNotifyToggle">Enable Desktop Notifications</label>
<br /><br />
<button onclick="openApiModal()">ADD/Edit OKX API</button>
<br><br>
<button onclick="openApiModal()">ADD/Edit OKX API</button>
<br /><br />
<button onclick="saveOptions()">Save</button>
</div>
<button onclick="saveOptions()">Save</button>
</div>
</div>
<div id="apiModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeApiModal()">&times;</span>
<h2>OKX API Settings</h2>
<p>Enter your OKX API data here (optional). Primary is Binance.
If Binance fails, the site tries OKX (or if you force OKX).</p>
<div class="modal-content">
<span class="close" onclick="closeApiModal()">&times;</span>
<h2>OKX API Settings</h2>
<p>
Enter your OKX API data here (optional). Primary is Binance. If
Binance fails, the site tries OKX (or if you force OKX).
</p>
<label for="okxApiKey">OKX API-Key:</label><br>
<input type="text" id="okxApiKey" placeholder="e.g. c42166aa-...">
<br>
<label for="okxApiKey">OKX API-Key:</label><br />
<input type="text" id="okxApiKey" placeholder="e.g. c42166aa-..." />
<br />
<label for="okxSecretKey">OKX Secret-Key:</label><br>
<input type="text" id="okxSecretKey" placeholder="e.g. ACD0B07F...">
<br>
<label for="okxSecretKey">OKX Secret-Key:</label><br />
<input type="text" id="okxSecretKey" placeholder="e.g. ACD0B07F..." />
<br />
<label for="okxPassphrase">OKX Passphrase (if needed):</label><br>
<input type="text" id="okxPassphrase" placeholder="(optional)">
<br><br>
<label for="okxPassphrase">OKX Passphrase (if needed):</label><br />
<input type="text" id="okxPassphrase" placeholder="(optional)" />
<br /><br />
<button onclick="saveApiSettings()">Save</button>
</div>
<button onclick="saveApiSettings()">Save</button>
</div>
</div>
<div id="addCryptoModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeAddCryptoModal()">&times;</span>
<h2>Add a New Cryptocurrency</h2>
<label for="newCryptoSymbol">Symbol (e.g. ETH):</label>
<input type="text" id="newCryptoSymbol" placeholder="BTC, ETH, ADA..."><br>
<br><br>
<button onclick="addNewCrypto()">Add</button>
</div>
<div class="modal-content">
<span class="close" onclick="closeAddCryptoModal()">&times;</span>
<h2>Add a New Cryptocurrency</h2>
<label for="newCryptoSymbol">Symbol (e.g. ETH):</label>
<input
type="text"
id="newCryptoSymbol"
placeholder="BTC, ETH, ADA..."
/><br />
<br /><br />
<button onclick="addNewCrypto()">Add</button>
</div>
</div>
<div id="apiSelectModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeApiSelectModal()">&times;</span>
<h2>API Selection for <span id="apiSelectSymbol"></span></h2>
<label for="apiSelectDropdown">Please choose:</label>
<select id="apiSelectDropdown">
<option value="auto">Auto (Binance -> OKX fallback)</option>
<option value="binance">Force Binance</option>
<option value="okx">Force OKX</option>
</select>
<br><br>
<button onclick="saveApiSelection()">Save</button>
</div>
<div class="modal-content">
<span class="close" onclick="closeApiSelectModal()">&times;</span>
<h2>API Selection for <span id="apiSelectSymbol"></span></h2>
<label for="apiSelectDropdown">Please choose:</label>
<select id="apiSelectDropdown">
<option value="auto">Auto (Binance -> OKX fallback)</option>
<option value="binance">Force Binance</option>
<option value="okx">Force OKX</option>
</select>
<br /><br />
<button onclick="saveApiSelection()">Save</button>
</div>
</div>
<div id="buyMeModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeBuyMeModal()">&times;</span>
<h2>Buy me a Coffee</h2>
<p><strong>Send me:</strong> USDT / Ethereum</p>
<p><strong>Chain:</strong>
<span style="text-decoration: underline; cursor: pointer;"
onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')">
ETH
</span> /
<span style="text-decoration: underline; cursor: pointer;"
onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')">
BSC
</span>
</p>
<p>
<span style="text-decoration: underline; cursor: pointer;"
onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')">
0x26c2E3F6C854Af006520ec2ce433982866bB7632
</span>
</p>
<p>(When clicked, the address is copied to the clipboard.)</p>
</div>
<div class="modal-content">
<span class="close" onclick="closeBuyMeModal()">&times;</span>
<h2>Buy me a Coffee</h2>
<p><strong>Send me:</strong> USDT / Ethereum</p>
<p>
<strong>Chain:</strong>
<span
style="text-decoration: underline; cursor: pointer"
onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')"
>
ETH
</span>
/
<span
style="text-decoration: underline; cursor: pointer"
onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')"
>
BSC
</span>
</p>
<p>
<span
style="text-decoration: underline; cursor: pointer"
onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')"
>
0x26c2E3F6C854Af006520ec2ce433982866bB7632
</span>
</p>
<p>(When clicked, the address is copied to the clipboard.)</p>
</div>
</div>
<div id="alarmOverlay" class="alert-overlay">
<div class="alert-box">
<div id="alarmMessage"></div>
<button onclick="closeAlarmPopup()">Close</button>
</div>
<div class="alert-box">
<div id="alarmMessage"></div>
<button onclick="closeAlarmPopup()">Close</button>
</div>
</div>
<div id="errorOverlay" class="error-overlay">
<div class="error-box">
<p id="errorMessage"></p>
<button onclick="closeErrorPopup()">Ok</button>
</div>
<div class="error-box">
<p id="errorMessage"></p>
<button onclick="closeErrorPopup()">Ok</button>
</div>
</div>
<audio id="alarmSound">
<source src="sound/ping.mp3" type="audio/mpeg">
<source src="sound/ping.mp3" type="audio/mpeg" />
</audio>
<div id="cryptoNewsModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeCryptoNewsModal()">&times;</span>
<h2>Crypto News</h2>
<input type="text" id="search" placeholder="Suche nach Artikeln...">
<input type="text" id="search" placeholder="Suche nach Artikeln..." />
<div id="news-feed">Lade News...</div>
</div>
</div>
<div id="economicCalendarModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeEconomicCalendarModal()">&times;</span>
<span class="close" onclick="closeEconomicCalendarModal()"
>&times;</span
>
<h2>Economic Calendar</h2>
<div class="iframe-container">
<div class="iframe-wrapper">
<iframe
src="https://sslecal2.investing.com?columns=exc_flags,exc_currency,exc_importance,exc_actual,exc_forecast,exc_previous&features=datepicker,timezone&countries=25,32,6,37,72,22,17,39,14,10,35,43,56,36,110,11,26,12,4,5&calType=week&timeZone=8&lang=1"
allowtransparency="true">
<div class="iframe-wrapper">
<iframe
src="https://sslecal2.investing.com?columns=exc_flags,exc_currency,exc_importance,exc_actual,exc_forecast,exc_previous&features=datepicker,timezone&countries=25,32,6,37,72,22,17,39,14,10,35,43,56,36,110,11,26,12,4,5&calType=week&timeZone=8&lang=1"
allowtransparency="true"
>
</iframe>
</div>
<div class="poweredBy">
<span>Real Time Economic Calendar provided by
<a href="https://www.investing.com/" rel="nofollow" target="_blank" class="underline_link">Investing.com</a>.
</span>
</div>
</div>
<div class="poweredBy">
<span
>Real Time Economic Calendar provided by
<a
href="https://www.investing.com/"
rel="nofollow"
target="_blank"
class="underline_link"
>Investing.com</a
>.
</span>
</div>
</div>
</div>
</div>
<div id="updateModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeUpdateModal()">&times;</span>
<h2 id="updateTitle">Update auf <span id="updateVersion"></span></h2>
<p id="updateChanges"></p>
<button onclick="performUpdate()">Update</button>
</div>
</div>
<script src="magic.js"></script>
<script src="news.js"></script>
</body>
<script src="update.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -4,16 +4,20 @@ document.addEventListener("DOMContentLoaded", function () {
const searchInput = document.getElementById("search");
let allArticles = [];
fetch(apiUrl)
.then(response => response.json())
.then(data => {
allArticles = data;
displayArticles(allArticles);
})
.catch(error => {
newsFeed.innerHTML = "Error loading the news.";
console.error("Error when retrieving the news:", error);
});
function refreshNewsFeed() {
fetch(apiUrl)
.then(response => response.json())
.then(data => {
allArticles = data;
displayArticles(allArticles);
})
.catch(error => {
newsFeed.innerHTML = "Error loading the news.";
console.error("Error when retrieving the news:", error);
});
}
function displayArticles(items) {
newsFeed.innerHTML = "";
@ -30,11 +34,11 @@ document.addEventListener("DOMContentLoaded", function () {
</div>
</div>
`;
newsFeed.appendChild(newsItem);
});
}
searchInput.addEventListener("input", function () {
const searchTerm = searchInput.value.toLowerCase();
const filteredArticles = allArticles.filter(item =>
@ -44,6 +48,7 @@ document.addEventListener("DOMContentLoaded", function () {
displayArticles(filteredArticles);
});
function getTimeAgo(date) {
const now = new Date();
const seconds = Math.floor((now - new Date(date)) / 1000);
@ -59,6 +64,7 @@ document.addEventListener("DOMContentLoaded", function () {
return `vor ${Math.floor(days / 365)} Jahren`;
}
function formatSourceName(source) {
const sourceMap = {
"crypto_news": "Crypto News",
@ -70,4 +76,8 @@ document.addEventListener("DOMContentLoaded", function () {
};
return sourceMap[source] || source;
}
refreshNewsFeed();
setInterval(refreshNewsFeed, 180000);
});

74
public/responsive.css Normal file
View File

@ -0,0 +1,74 @@
@media (max-width: 1614px) {
.grid-container {
grid-template-columns: repeat(4, 1fr);
}
}
@media (max-width: 1353px) {
.grid-container {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 1275px) {
.grid-container {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 1200px) {
.grid-container {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 992px) {
.grid-container {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.grid-container {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 480px) {
.grid-container {
grid-template-columns: 1fr;
}
}
.main-container {
display: flex;
}
.left-column {
width: 75%;
}
.right-column {
width: 320px;
border-left: 1px solid #444;
padding: 20px;
display: flex;
flex-direction: column;
}
@media (max-width: 1251px) {
.button-grid-container {
display: grid;
grid-template-columns: none;
align-items: center;
justify-items: center;
column-gap: 10px;
margin-bottom: 15px;
}
}

View File

@ -1,12 +1,13 @@
body {
font-family: Arial, sans-serif;
margin: 0 auto; /* Zentrierung */
margin: 0 auto;
padding: 0;
transition: background-color 0.4s, color 0.4s;
min-width: 1400px;
max-width: 1978px;
max-width: 1778px;
min-width: 808px;
}
h1 {
font-size: 28px;
}
@ -16,6 +17,7 @@ body.dark {
background-color: #121212;
color: #ffffff;
}
body.light {
background-color: #f2f2f2;
color: #000000;
@ -26,9 +28,11 @@ body.light {
text-align: center;
padding: 20px;
}
.button-container {
margin-bottom: 10px;
}
button {
padding: 10px;
margin: 5px;
@ -38,31 +42,68 @@ button {
border: none;
cursor: pointer;
border-radius: 5px;
}
button:hover {
background-color: #005f73;
}
.alarm-btn { background-color: #ff8b00; }
.alarm-btn:hover { background-color: #e38806; }
.options-btn { background-color: #555; }
.options-btn:hover { background-color: #777; }
.alarm-btn {
background-color: #ff8b00;
}
.alarm-btn:hover {
background-color: #e38806;
}
.options-btn {
background-color: #555;
}
.options-btn:hover {
background-color: #777;
}
.main-container {
display: flex;
}
.left-column {
width: 75%;
padding: 20px;
padding: 10px;
}
.right-column {
width: 320px;
display: grid;
grid-template-rows: 300px auto;
border-left: 1px solid #444;
width: 320px;
padding: 20px;
}
.notify-area {
min-height: 400px;
max-width: 280px;
border: 1px solid #444;
overflow-y: auto;
padding: 10px;
display: flex;
flex-direction: column;
}
.buttons-and-version {
display: flex;
flex-direction: column;
max-width: 260px;
padding-left: 6%;
}
.notify-heading {
display: flex;
@ -71,6 +112,7 @@ button:hover {
font-size: 20px;
margin-bottom: 10px;
}
.notify-list {
list-style: none;
padding-left: 0;
@ -83,12 +125,14 @@ button:hover {
.grid-container {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-columns: repeat(5, 1fr);
gap: 10px;
width: 90%;
width: 80%;
margin: auto;
padding: 10px;
}
.crypto-box {
background-color: #1e1e1e;
border: 2px solid #444;
@ -98,15 +142,22 @@ button:hover {
font-size: 16px;
position: relative;
user-select: none;
min-width: 177px;
}
body.light .crypto-box {
background-color: #ffffff;
border: 2px solid #ccc;
color: #000;
}
.change.up { color: #00ff00; }
.change.down { color: #ff0000; }
.change.up {
color: #00ff00;
}
.change.down {
color: #ff0000;
}
.delete-btn {
@ -125,6 +176,7 @@ body.light .crypto-box {
line-height: 24px;
padding: 0;
}
.delete-btn:hover {
background-color: #999;
color: #000;
@ -135,6 +187,7 @@ body.light .crypto-box {
opacity: 0.5;
transform: scale(0.97);
}
.crypto-box.drag-over {
border: 2px dashed #ccc !important;
}
@ -144,16 +197,19 @@ body.light .crypto-box {
display: none;
position: fixed;
z-index: 2;
left: 0; top: 0;
width: 100%; height: 100%;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
}
.modal-content {
background-color: #222;
background-color: #222222f2;
margin: 5% auto;
padding: 20px;
border-radius: 8px;
max-width: 40%;
text-align: center;
color: white;
@ -200,7 +256,7 @@ body.light .crypto-box {
#optionsModal .modal-content {
width: 20%;
width: 30%;
overflow-x: hidden;
}
@ -245,12 +301,12 @@ body.light .crypto-box {
#alarmModal .modal-content {
width: 30%;
min-width: 406px;
text-align: center;
position: absolute;
top: 35%;
left: 50%;
transform: translate(-50%, -50%);
transform: translate(-50%, -45%);
display: flex;
flex-direction: column;
align-items: center;
@ -265,13 +321,14 @@ body.light .crypto-box {
#alarmModal .modal-content select,
#alarmModal .modal-content input {
margin-bottom: 8px;
width: 100%;
width: 30%;
padding: 8px;
}
#alarmModal .modal-content select {
padding: 8px;
font-size: 13px;
width: 100%;
width: 30%;
}
#alarmModal .close {
@ -289,17 +346,23 @@ body.light .modal-content {
background-color: #ddd;
color: #000;
}
.close {
float: right;
font-size: 24px;
cursor: pointer;
}
label {
display: inline-block;
margin-top: 10px;
}
select, input[type="number"], input[type="text"] {
select,
input[type="number"],
input[type="text"] {
margin: 5px 0 10px 5px;
font-size: medium;
}
@ -307,10 +370,13 @@ select, input[type="number"], input[type="text"] {
display: none;
position: fixed;
z-index: 3;
left: 0; top: 0;
width: 100%; height: 100%;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
}
.alert-box {
background-color: #333;
color: #fff;
@ -321,13 +387,16 @@ select, input[type="number"], input[type="text"] {
text-align: center;
position: relative;
}
.alert-box button {
margin-top: 15px;
background-color: #555;
}
.alert-box button:hover {
background-color: #777;
}
body.light .alert-box {
background-color: #ccc;
color: #000;
@ -338,10 +407,13 @@ body.light .alert-box {
display: none;
position: fixed;
z-index: 4;
left: 0; top: 0;
width: 100%; height: 100%;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
}
.error-box {
background-color: #aa3333;
color: #fff;
@ -352,6 +424,7 @@ body.light .alert-box {
text-align: center;
position: relative;
}
body.light .error-box {
background-color: #f2aaaa;
color: #000;
@ -365,6 +438,7 @@ body.light .error-box {
text-decoration: underline;
cursor: pointer;
}
body.light .api-label {
color: #333;
}
@ -378,6 +452,7 @@ body.light .api-label {
overflow-y: auto;
margin: 1em 0;
}
.alarm-item {
display: flex;
align-items: center;
@ -386,10 +461,12 @@ body.light .api-label {
padding: 6px;
border-radius: 3px;
}
body.light .alarm-item {
background-color: #eee;
color: #000;
}
.alarm-delete-btn {
background-color: #008CBA;
border: none;
@ -398,6 +475,7 @@ body.light .alarm-item {
border-radius: 5px;
padding: 5px 8px;
}
.alarm-delete-btn:hover {
background-color: #005f73;
}
@ -420,9 +498,11 @@ body.light .alarm-item {
border: none;
cursor: pointer;
}
.coffee-btn:hover {
background-color: #711fe9;
}
.coffee-icon {
width: 25px;
height: 25px;
@ -443,21 +523,26 @@ body.light .alarm-item {
color: white;
text-align: center;
}
#news-feed {
text-align: left;
}
.news-item {
padding: 15px;
border-bottom: 1px solid #333;
transition: background 0.3s;
}
.news-item:last-child {
border-bottom: none;
}
.news-item:hover {
background: rgba(255, 255, 255, 0.1);
cursor: pointer;
}
.news-title {
font-size: 18px;
color: #1db954;
@ -465,6 +550,7 @@ body.light .alarm-item {
display: block;
margin-bottom: 5px;
}
.news-meta {
font-size: 14px;
color: gray;
@ -472,10 +558,12 @@ body.light .alarm-item {
justify-content: space-between;
align-items: center;
}
.news-source {
font-weight: bold;
color: #ffffff;
}
.time {
font-size: 14px;
color: gray;
@ -484,18 +572,14 @@ body.light .alarm-item {
.button-grid-container {
display: grid;
grid-template-columns: 1fr 2fr 1fr;
grid-template-columns: 1fr 1fr 1fr;
/* alle gleich breit */
align-items: center;
justify-items: center;
gap: 10px;
column-gap: 10px;
/* oder kleiner */
margin-bottom: 15px;
}
.grid-left, .grid-middle, .grid-right {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
}
#economicCalendarModal .iframe-container {
@ -510,6 +594,7 @@ body.light .alarm-item {
align-items: center;
justify-content: center;
}
#economicCalendarModal .iframe-wrapper {
position: relative;
width: 100%;
@ -520,12 +605,14 @@ body.light .alarm-item {
background: white;
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2);
}
#economicCalendarModal iframe {
width: calc(100% - 20px);
height: calc(100% - 20px);
border: none;
display: block;
}
#economicCalendarModal .poweredBy {
margin-top: 10px;
font-family: Arial, Helvetica, sans-serif;
@ -533,6 +620,7 @@ body.light .alarm-item {
color: #ffffff;
text-align: center;
}
#economicCalendarModal .poweredBy a {
color: #06529D;
font-weight: bold;
@ -540,3 +628,26 @@ body.light .alarm-item {
}
#updateModal .modal-content {}
.version-info {
text-align: center;
margin-top: 10px;
}
.change {
margin: 5px;
}
.my-button {
background-color: white;
color: black;
border: 1px solid black;
padding: 8px 16px;
cursor: pointer;
font-weight: 700;
}

112
public/update.js Normal file
View File

@ -0,0 +1,112 @@
const CURRENT_VERSION = "1.0.6";
function getUpdateUrl() {
return "/api/update?t=" + new Date().getTime();
}
document.addEventListener("DOMContentLoaded", () => {
document.getElementById("currentVersion").textContent = `Version ${CURRENT_VERSION}`;
checkForUpdates();
setInterval(checkForUpdates, 259200000);
});
function checkForUpdates() {
fetch(getUpdateUrl())
.then((response) => response.json())
.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())
.then((data) => {
let skippedVersion = localStorage.getItem("skippedVersion") || null;
if (skippedVersion && data.version !== skippedVersion) {
localStorage.removeItem("skippedVersion");
skippedVersion = null;
}
openUpdateModal(data);
})
.catch((error) =>
console.error("Fehler beim erneuten Abrufen des Updates:", error)
);
};
}
})
.catch((error) =>
console.error("Fehler beim Abrufen des Updates:", error)
);
}
function compareVersions(v1, v2) {
const v1parts = v1.split(".").map(Number);
const v2parts = v2.split(".").map(Number);
for (let i = 0; i < Math.max(v1parts.length, v2parts.length); i++) {
const num1 = v1parts[i] || 0;
const num2 = v2parts[i] || 0;
if (num1 > num2) return 1;
if (num1 < num2) return -1;
}
return 0;
}
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 => {
let li = document.createElement("li");
li.textContent = item;
ul.appendChild(li);
});
changelogContainer.appendChild(ul);
} else {
changelogContainer.textContent = data.changelog || "Kein Changelog vorhanden.";
}
window._updateData = data;
document.getElementById("updateModal").style.display = "block";
}
function closeUpdateModal() {
document.getElementById("updateModal").style.display = "none";
}
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);
}
closeUpdateModal();
}

View File

@ -5,6 +5,7 @@ const cors = require("cors");
const http = require("http");
const WebSocket = require("ws");
const app = express();
app.use(cors());
app.use(express.json());
@ -16,7 +17,7 @@ const clients = new Set();
/*
* Metadata
* Version: 1.0.5
* Version: 1.0.6
* Author/Dev: Gerald Hasani
* Name: HodlEye Crypto Price Tracker
* Email: contact@gerald-hasani.com
@ -25,12 +26,20 @@ const clients = new Set();
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));
fs.writeFileSync(
DATA_FILE,
JSON.stringify(
{
cryptos: ["BTC"],
alarms: [],
notifications: []
},
null,
2
)
);
}
function readData() {
@ -41,6 +50,23 @@ function writeData(data) {
fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2));
}
app.get("/api/update", (req, res) => {
const remoteUpdateUrl = "https://raw.githubusercontent.com/Gerald-Ha/HodlEye-Crypto-Price-Tracker/refs/heads/main/update.json";
fetch(remoteUpdateUrl)
.then(response => response.json())
.then(data => {
res.header("Cache-Control", "no-cache, no-store, must-revalidate");
res.header("Pragma", "no-cache");
res.header("Expires", "0");
res.json(data);
})
.catch(error => {
console.error("Fehler beim Abrufen der remote update.json:", error);
res.status(500).json({ error: "Could not fetch update data" });
});
});
app.get("/api/cryptos", (req, res) => {
const data = readData();
res.json(data.cryptos);
@ -51,10 +77,8 @@ app.post("/api/cryptos", (req, res) => {
if (!symbol) {
return res.status(400).json({ error: "Symbol is required." });
}
const data = readData();
const upperSymbol = symbol.toUpperCase();
if (!data.cryptos.includes(upperSymbol)) {
data.cryptos.push(upperSymbol);
writeData(data);
@ -70,6 +94,17 @@ app.delete("/api/cryptos/:symbol", (req, res) => {
res.json({ success: true, cryptos: data.cryptos });
});
app.put("/api/cryptos", (req, res) => {
const { cryptoList } = req.body;
if (!Array.isArray(cryptoList)) {
return res.status(400).json({ error: "cryptoList must be an array." });
}
const data = readData();
data.cryptos = cryptoList;
writeData(data);
res.json({ success: true, cryptos: data.cryptos });
});
app.get("/api/alarms", (req, res) => {
const data = readData();
res.json(data.alarms);
@ -80,7 +115,6 @@ app.post("/api/alarms", (req, res) => {
if (!symbol || !price) {
return res.status(400).json({ error: "symbol and price are required." });
}
const data = readData();
const newAlarm = {
id: Date.now(),
@ -113,7 +147,6 @@ app.post("/api/notifications", (req, res) => {
if (!message) {
return res.status(400).json({ error: "message is required." });
}
const data = readData();
const entry = {
message,
@ -122,7 +155,7 @@ app.post("/api/notifications", (req, res) => {
data.notifications.unshift(entry);
writeData(data);
// Nachricht an alle WebSocket-Clients senden
clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(entry));
@ -141,18 +174,15 @@ app.delete("/api/notifications", (req, res) => {
app.use(express.static(path.join(__dirname, "..", "public")));
// WebSocket-Server für Echtzeitbenachrichtigungen
wss.on("connection", (ws) => {
console.log("Client verbunden");
clients.add(ws);
ws.on("close", () => {
console.log("Client getrennt");
clients.delete(ws);
});
console.log("Client verbunden");
clients.add(ws);
ws.on("close", () => {
console.log("Client getrennt");
clients.delete(ws);
});
});
// Server starten
const PORT = process.env.PORT || 3099;
server.listen(PORT, () => {
console.log(`Server läuft auf Port ${PORT}`);

14
update.json Normal file
View File

@ -0,0 +1,14 @@
{
"version": "1.0.6",
"changelog": [
"Bug fixes and improvements",
"Responsive design added",
"Crypto News Auto Update without Browser Refresh every 3 minutes",
"Crypto Price Refresh Reduce from 5 Second to 1 Second",
"Docker command updated to save alarms and registered cryptocurrencies that are still there after a Docker image update (--> docker run -p 3099:3099 -p 5001:5001 -v hodleye_data:/app/data --name hodleye-container hodleye-crypto-tracker)",
"Bugfix 'Edit List' works now",
"Added Update Option and Notification every 5 Days",
"Re-Design from Crypto-box",
"Improving the logic of the price display for small cryptocurrencies"
]
}