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

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.
&nbsp;
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.”
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,97 +1,110 @@
<!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">
<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 class="grid-container" id="cryptoGrid"></div>
</div>
</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>
<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">
Buy me a Coffee
<img src="images/coffee.svg" alt="Coffee" class="coffee-icon" />
</button>
<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>
<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="alarm-list-container" id="alarmListContainer">
</div>
<div class="alarm-list-container" id="alarmListContainer"></div>
<label for="alarmSymbol">Symbol:</label>
<select id="alarmSymbol"></select><br>
<select id="alarmSymbol"></select
><br />
<label for="alarmPrice">Alarm Price (USDT):</label>
<input type="number" id="alarmPrice" placeholder="Price"><br>
<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>
<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>
<option value="Both">Both</option></select
><br />
<button onclick="addAlarm()">Add Alarm</button>
</div>
</div>
<div id="optionsModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeOptionsModal()">&times;</span>
@ -102,60 +115,63 @@
<option value="ping.mp3">ping.mp3</option>
<option value="cashing.mp3">cashing.mp3</option>
</select>
<br><br>
<br /><br />
<input type="checkbox" id="darkModeToggle">
<input type="checkbox" id="darkModeToggle" />
<label for="darkModeToggle">Enable Dark Mode</label>
<br><br>
<br /><br />
<input type="checkbox" id="desktopNotifyToggle">
<input type="checkbox" id="desktopNotifyToggle" />
<label for="desktopNotifyToggle">Enable Desktop Notifications</label>
<br><br>
<br /><br />
<button onclick="openApiModal()">ADD/Edit OKX API</button>
<br><br>
<br /><br />
<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>
<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>
</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>
<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>
@ -166,30 +182,37 @@
<option value="binance">Force Binance</option>
<option value="okx">Force OKX</option>
</select>
<br><br>
<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')">
<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')">
</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')">
<span
style="text-decoration: underline; cursor: pointer"
onclick="copyToClipboard('0x26c2E3F6C854Af006520ec2ce433982866bB7632')"
>
0x26c2E3F6C854Af006520ec2ce433982866bB7632
</span>
</p>
@ -197,7 +220,6 @@
</div>
</div>
<div id="alarmOverlay" class="alert-overlay">
<div class="alert-box">
<div id="alarmMessage"></div>
@ -205,7 +227,6 @@
</div>
</div>
<div id="errorOverlay" class="error-overlay">
<div class="error-box">
<p id="errorMessage"></p>
@ -213,46 +234,62 @@
</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">
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
>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>

View File

@ -1,31 +1,23 @@
let cryptoList = [];
let alarms = [];
let notifications = [];
let lastPrices = {};
let cryptoList = [];
let alarms = [];
let notifications = [];
let lastPrices = {};
let userOptions = JSON.parse(localStorage.getItem("userOptions")) || {
let userOptions = JSON.parse(localStorage.getItem("userOptions")) || {
soundFile: "ping.mp3",
darkMode: true,
enableDesktopNotifications: false,
okxApiKey: "",
okxSecretKey: "",
okxPassphrase: ""
};
okxPassphrase: "",
};
let apiPreference = JSON.parse(localStorage.getItem("apiPreference")) || {};
let apiPreference = JSON.parse(localStorage.getItem("apiPreference")) || {};
let editMode = false;
let currentApiSelectSymbol = null;
let editMode = false;
let currentApiSelectSymbol = null;
async function loadCryptosFromServer() {
async function loadCryptosFromServer() {
try {
const resp = await fetch("/api/cryptos");
cryptoList = await resp.json();
@ -33,25 +25,23 @@
} catch (err) {
console.error("Fehler beim Laden der Kryptoliste:", err);
}
}
}
async function addNewCrypto() {
async function addNewCrypto() {
const newCrypto = document.getElementById("newCryptoSymbol").value.trim().toUpperCase();
if (!newCrypto) return;
if (!(await isBinanceSupported(newCrypto)) && !(await isOkxSupported(newCrypto))) {
showErrorMessage("This cryptocurrency is not supported on Binance or OKX (USDT).");
closeAddCryptoModal();
return;
}
try {
const resp = await fetch("/api/cryptos", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ symbol: newCrypto })
headers: {"Content-Type": "application/json"},
body: JSON.stringify({symbol: newCrypto}),
});
const updatedList = await resp.json();
cryptoList = updatedList;
@ -60,22 +50,20 @@
showErrorMessage("Error adding new crypto: " + err.message);
}
closeAddCryptoModal();
}
}
async function deleteCrypto(index) {
async function deleteCrypto(index) {
const symbol = cryptoList[index];
if (!symbol) return;
try {
await fetch(`/api/cryptos/${symbol}`, { method: "DELETE" });
await fetch(`/api/cryptos/${symbol}`, {method: "DELETE"});
loadCryptosFromServer();
} catch (err) {
showErrorMessage("Error deleting crypto: " + err.message);
}
}
}
async function loadAlarmsFromServer() {
async function loadAlarmsFromServer() {
try {
const resp = await fetch("/api/alarms");
alarms = await resp.json();
@ -83,9 +71,9 @@
} catch (err) {
console.error("Fehler beim Laden der Alarme:", err);
}
}
}
async function addAlarm() {
async function addAlarm() {
const symbol = document.getElementById("alarmSymbol").value;
const price = parseFloat(document.getElementById("alarmPrice").value);
const frequency = document.getElementById("alarmFrequency").value;
@ -94,30 +82,27 @@
if (!symbol || isNaN(price)) return;
try {
await fetch("/api/alarms", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ symbol, price, frequency, direction })
headers: {"Content-Type": "application/json"},
body: JSON.stringify({symbol, price, frequency, direction}),
});
loadAlarmsFromServer();
} catch (err) {
showErrorMessage("Error adding alarm: " + err.message);
}
}
}
async function deleteAlarm(alarmId) {
async function deleteAlarm(alarmId) {
try {
await fetch(`/api/alarms/${alarmId}`, { method: "DELETE" });
await fetch(`/api/alarms/${alarmId}`, {method: "DELETE"});
loadAlarmsFromServer();
} catch (err) {
showErrorMessage("Error deleting alarm: " + err.message);
}
}
}
async function loadNotificationsFromServer() {
async function loadNotificationsFromServer() {
try {
const resp = await fetch("/api/notifications");
notifications = await resp.json();
@ -125,64 +110,53 @@
} catch (err) {
console.error("Fehler beim Laden der Notifications:", err);
}
}
async function addNotification(msg) {
}
async function addNotification(msg) {
try {
await fetch("/api/notifications", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: msg })
headers: {"Content-Type": "application/json"},
body: JSON.stringify({message: msg}),
});
loadNotificationsFromServer();
} catch (err) {
console.error("Fehler beim Hinzufügen einer Notification:", err);
}
}
async function clearNotifications() {
}
async function clearNotifications() {
try {
await fetch("/api/notifications", { method: "DELETE" });
await fetch("/api/notifications", {method: "DELETE"});
loadNotificationsFromServer();
} catch (err) {
console.error("Fehler beim Löschen der Notifications:", err);
}
}
async function init() {
}
async function init() {
document.getElementById("alarmSound").src = "sound/" + userOptions.soundFile;
updateTheme(userOptions.darkMode);
await loadCryptosFromServer();
await loadAlarmsFromServer();
await loadNotificationsFromServer();
setInterval(() => {
cryptoList.forEach((symbol, index) => {
const elementId = "crypto-" + index;
fetchCryptoData(symbol, elementId)
.catch(() => setNotSupported(elementId));
fetchCryptoData(symbol, elementId).catch(() => setNotSupported(elementId));
});
}, 5000);
}, 1000);
renderNotifications();
}
}
function renderCryptoGrid() {
function renderCryptoGrid() {
const grid = document.getElementById("cryptoGrid");
grid.innerHTML = "";
cryptoList.forEach((symbol, index) => {
const boxId = "crypto-" + index;
const box = document.createElement("div");
box.className = "crypto-box";
box.id = boxId;
@ -216,7 +190,6 @@
change1h.textContent = "1h Change: -";
box.appendChild(change1h);
const apiLabel = document.createElement("div");
apiLabel.id = "api-" + boxId;
apiLabel.className = "api-label";
@ -226,7 +199,6 @@
});
box.appendChild(apiLabel);
if (editMode) {
box.draggable = true;
box.setAttribute("data-index", index);
@ -248,17 +220,15 @@
}
grid.appendChild(box);
fetchCryptoData(symbol, boxId).catch(() => setNotSupported(boxId));
});
}
}
function renderAlarmList() {
function renderAlarmList() {
const container = document.getElementById("alarmListContainer");
container.innerHTML = "";
alarms.forEach(alarm => {
alarms.forEach((alarm) => {
const item = document.createElement("div");
item.className = "alarm-item";
@ -274,9 +244,9 @@
item.appendChild(delBtn);
container.appendChild(item);
});
}
}
function renderNotifications() {
function renderNotifications() {
const notifyList = document.getElementById("notifyList");
notifyList.innerHTML = "";
notifications.forEach((item) => {
@ -288,12 +258,11 @@
`;
notifyList.appendChild(li);
});
}
}
/* ------------------------------------------------------------------
/* ------------------------------------------------------------------
EINFACHE UI-Funktionen: Modal open/close
------------------------------------------------------------------ */
function openAddCryptoModal() {
document.getElementById("addCryptoModal").style.display = "block";
document.getElementById("newCryptoSymbol").value = "";
@ -308,7 +277,7 @@ function openAlarmModal() {
const dropdown = document.getElementById("alarmSymbol");
dropdown.innerHTML = "";
cryptoList.forEach(symbol => {
cryptoList.forEach((symbol) => {
const option = document.createElement("option");
option.value = symbol;
option.textContent = symbol;
@ -366,7 +335,6 @@ function closeBuyMeModal() {
/* ------------------------------------------------------------------
NEU: Crypto News & Economic Calendar Modals
------------------------------------------------------------------ */
function openCryptoNews() {
document.getElementById("cryptoNewsModal").style.display = "block";
}
@ -383,8 +351,6 @@ function closeEconomicCalendarModal() {
document.getElementById("economicCalendarModal").style.display = "none";
}
function saveOptions() {
userOptions.soundFile = document.getElementById("soundSelect").value;
const alarmSound = document.getElementById("alarmSound");
@ -473,13 +439,24 @@ async function reorderCryptoList(fromIndex, toIndex) {
const item = cryptoList.splice(fromIndex, 1)[0];
cryptoList.splice(toIndex, 0, item);
renderCryptoGrid();
await saveCryptoList();
}
async function saveCryptoList() {
try {
await fetch("/api/cryptos", {
method: "PUT",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({cryptoList: cryptoList}),
});
} catch (err) {
console.error("Fehler beim Speichern der Kryptoliste:", err);
}
}
async function fetchCryptoData(symbol, elementId) {
async function fetchCryptoData(symbol, elementId) {
const preferredApi = apiPreference[symbol] || "auto";
if (preferredApi === "binance") {
@ -504,19 +481,18 @@ async function reorderCryptoList(fromIndex, toIndex) {
} else {
return setNotSupported(elementId);
}
}
}
function setNotSupported(elementId) {
function setNotSupported(elementId) {
document.getElementById("daily-" + elementId).textContent = "Daily Price: ❌";
document.getElementById("hourly-" + elementId).textContent = "Price H: ❌";
document.getElementById("price-" + elementId).innerHTML = "<strong>Current Price:</strong> ❌ Not supported";
document.getElementById("change24-" + elementId).textContent = "24h Change: -";
document.getElementById("change1h-" + elementId).textContent = "1h Change: -";
document.getElementById("api-" + elementId).textContent = "API: ?";
}
}
async function fetchFromBinance(symbol, elementId) {
async function fetchFromBinance(symbol, elementId) {
const tickerUrl = `https://api.binance.com/api/v3/ticker/24hr?symbol=${symbol}USDT`;
let resp = await fetch(tickerUrl);
if (!resp.ok) throw new Error("Binance request failed");
@ -527,7 +503,6 @@ async function reorderCryptoList(fromIndex, toIndex) {
const lastPrice = parseFloat(data.lastPrice);
const priceChange24h = parseFloat(data.priceChangePercent);
const klineUrl = `https://api.binance.com/api/v3/klines?symbol=${symbol}USDT&interval=1h&limit=1`;
let klResp = await fetch(klineUrl);
if (!klResp.ok) throw new Error("Binance 1h kline request failed");
@ -549,12 +524,11 @@ async function reorderCryptoList(fromIndex, toIndex) {
hourlyOpen,
lastPrice,
change24h: priceChange24h,
change1h: pct1h
change1h: pct1h,
});
}
}
async function fetchFromOkx(symbol, elementId) {
async function fetchFromOkx(symbol, elementId) {
const instId = `${symbol}-USDT`;
const tickerUrl = `https://www.okx.com/api/v5/market/ticker?instId=${instId}`;
let resp = await fetch(tickerUrl);
@ -571,7 +545,6 @@ async function reorderCryptoList(fromIndex, toIndex) {
pct24 = ((lastPrice - dailyOpen) / dailyOpen) * 100;
}
const cUrl = `https://www.okx.com/api/v5/market/candles?instId=${instId}&bar=1H&limit=1`;
let cResp = await fetch(cUrl);
if (!cResp.ok) throw new Error("OKX 1h candle request failed");
@ -593,52 +566,52 @@ async function reorderCryptoList(fromIndex, toIndex) {
hourlyOpen,
lastPrice,
change24h: pct24,
change1h: pct1h
change1h: pct1h,
});
}
}
function updateCryptoBox({
symbol,
elementId,
apiUsed,
dailyOpen,
hourlyOpen,
lastPrice,
change24h,
change1h
}) {
const dOpenStr = isNaN(dailyOpen) ? "-" : dailyOpen.toFixed(4);
const hOpenStr = isNaN(hourlyOpen) ? "-" : hourlyOpen.toFixed(4);
const lastStr = isNaN(lastPrice) ? "-" : lastPrice.toFixed(4);
function formatPrice(value) {
if (isNaN(value)) {
return "-";
}
if (value < 0.01) {
return value.toFixed(6);
} else {
return value.toFixed(4);
}
}
function updateCryptoBox({symbol, elementId, apiUsed, dailyOpen, hourlyOpen, lastPrice, change24h, change1h}) {
const dOpenStr = formatPrice(dailyOpen);
const hOpenStr = formatPrice(hourlyOpen);
const lastStr = formatPrice(lastPrice);
const pct24Str = isNaN(change24h) ? "-" : change24h.toFixed(2) + "%";
const pct1hStr = isNaN(change1h) ? "-" : change1h.toFixed(2) + "%";
document.getElementById("daily-" + elementId).textContent = "Daily Price: " + dOpenStr + " USDT";
document.getElementById("hourly-" + elementId).textContent = "Price H: " + hOpenStr + " USDT";
document.getElementById("price-" + elementId).innerHTML =
`<strong>Current Price:</strong> ${lastStr} USDT`;
document.getElementById("daily-" + elementId).innerHTML = `Daily Price:<br>${dOpenStr} USDT<br>`;
document.getElementById("hourly-" + elementId).innerHTML = `Price H:<br>${hOpenStr} USDT<br>`;
document.getElementById("price-" + elementId).innerHTML = `<strong>Current Price:</strong><br>${lastStr} USDT<br>`;
const c24 = document.getElementById("change24-" + elementId);
c24.textContent = "24h Change: " + pct24Str;
c24.innerHTML = `24h Change: ${pct24Str}`;
c24.className = "change " + (change24h >= 0 ? "up" : "down");
const c1h = document.getElementById("change1h-" + elementId);
c1h.textContent = "1h Change: " + pct1hStr;
c1h.innerHTML = `1h Change: ${pct1hStr}<br><br>`;
c1h.className = "change " + (change1h >= 0 ? "up" : "down");
document.getElementById("api-" + elementId).textContent = "API: " + apiUsed;
document.getElementById("api-" + elementId).innerHTML = `API: ${apiUsed}`;
checkAlarms(symbol, lastPrice);
lastPrices[symbol] = lastPrice;
}
}
function checkAlarms(symbol, currentPrice) {
alarms.forEach(alarm => {
function checkAlarms(symbol, currentPrice) {
alarms.forEach((alarm) => {
if (alarm.symbol !== symbol) return;
if (alarm.triggered) return;
const alarmPrice = parseFloat(alarm.price);
@ -647,110 +620,70 @@ async function reorderCryptoList(fromIndex, toIndex) {
let conditionMet = false;
if (alarm.direction === "Rising") {
conditionMet = (prevPrice < alarmPrice && currentPrice >= alarmPrice);
conditionMet = prevPrice < alarmPrice && currentPrice >= alarmPrice;
} else if (alarm.direction === "Falling") {
conditionMet = (prevPrice > alarmPrice && currentPrice <= alarmPrice);
conditionMet = prevPrice > alarmPrice && currentPrice <= alarmPrice;
} else if (alarm.direction === "Both") {
const crossingUp = (prevPrice < alarmPrice && currentPrice >= alarmPrice);
const crossingDown = (prevPrice > alarmPrice && currentPrice <= alarmPrice);
const crossingUp = prevPrice < alarmPrice && currentPrice >= alarmPrice;
const crossingDown = prevPrice > alarmPrice && currentPrice <= alarmPrice;
conditionMet = crossingUp || crossingDown;
}
if (conditionMet) {
const msg = `⚠️ ALARM (${alarm.frequency}, ${alarm.direction}): ${symbol} reached ${alarmPrice}!`;
showAlarmPopup(msg);
if (alarm.frequency === "Once") {
alarm.triggered = true;
}
}
});
}
}
function showErrorMessage(msg) {
function showErrorMessage(msg) {
document.getElementById("errorMessage").textContent = msg;
document.getElementById("errorOverlay").style.display = "block";
}
function closeErrorPopup() {
document.getElementById("errorOverlay").style.display = "none";
}
}
function showAlarmPopup(message) {
function closeErrorPopup() {
document.getElementById("errorOverlay").style.display = "none";
}
function showAlarmPopup(message) {
document.getElementById("alarmMessage").textContent = message;
document.getElementById("alarmOverlay").style.display = "block";
document.getElementById("alarmSound").play();
addNotification(message);
if (userOptions.enableDesktopNotifications && "Notification" in window) {
if (Notification.permission === "granted") {
new Notification("Crypto Price Alarm", { body: message });
new Notification("Crypto Price Alarm", {body: message});
} else if (Notification.permission !== "denied") {
Notification.requestPermission().then(permission => {
Notification.requestPermission().then((permission) => {
if (permission === "granted") {
new Notification("Crypto Price Alarm", { body: message });
new Notification("Crypto Price Alarm", {body: message});
}
});
}
}
}
function closeAlarmPopup() {
}
function closeAlarmPopup() {
document.getElementById("alarmOverlay").style.display = "none";
}
}
function copyToClipboard(address) {
navigator.clipboard.writeText(address).then(() => {
alert("Address is copied to the clipboard.");
});
}
async function isBinanceSupported(symbol) {
const url = `https://api.binance.com/api/v3/ticker/24hr?symbol=${symbol}USDT`;
try {
const response = await fetch(url);
if (!response.ok) return false;
const data = await response.json();
if (data.code) return false;
return true;
} catch {
return false;
}
}
async function isOkxSupported(symbol) {
const instId = `${symbol}-USDT`;
const okxUrl = `https://www.okx.com/api/v5/market/ticker?instId=${instId}`;
try {
const response = await fetch(okxUrl);
if (!response.ok) return false;
const data = await response.json();
if (!data.data || !data.data[0]) return false;
return true;
} catch {
return false;
}
}
window.addEventListener("DOMContentLoaded", init);
function copyToClipboard(address) {
function copyToClipboard(address) {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(address).then(() => {
navigator.clipboard
.writeText(address)
.then(() => {
alert("Address is copied to the clipboard.");
}).catch(err => {
})
.catch((err) => {
console.error("Clipboard API Fehler:", err);
alert("Fehler beim Kopieren der Adresse.");
});
} else {
let textArea = document.createElement("textarea");
textArea.value = address;
textArea.style.position = "fixed";
textArea.style.top = "0";
textArea.style.left = "0";
@ -765,7 +698,74 @@ async function reorderCryptoList(fromIndex, toIndex) {
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
document.execCommand("copy");
alert("Address is copied to the clipboard.");
} catch (err) {
console.error("Fallback Copy Fehler:", err);
alert("Fehler beim Kopieren der Adresse.");
}
document.body.removeChild(textArea);
}
}
async function isBinanceSupported(symbol) {
const url = `https://api.binance.com/api/v3/ticker/24hr?symbol=${symbol}USDT`;
try {
const response = await fetch(url);
if (!response.ok) return false;
const data = await response.json();
if (data.code) return false;
return true;
} catch {
return false;
}
}
async function isOkxSupported(symbol) {
const instId = `${symbol}-USDT`;
const okxUrl = `https://www.okx.com/api/v5/market/ticker?instId=${instId}`;
try {
const response = await fetch(okxUrl);
if (!response.ok) return false;
const data = await response.json();
if (!data.data || !data.data[0]) return false;
return true;
} catch {
return false;
}
}
window.addEventListener("DOMContentLoaded", init);
function copyToClipboard(address) {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard
.writeText(address)
.then(() => {
alert("Address is copied to the clipboard.");
})
.catch((err) => {
console.error("Clipboard API Fehler:", err);
alert("Fehler beim Kopieren der Adresse.");
});
} else {
let textArea = document.createElement("textarea");
textArea.value = address;
textArea.style.position = "fixed";
textArea.style.top = "0";
textArea.style.left = "0";
textArea.style.width = "2em";
textArea.style.height = "2em";
textArea.style.padding = "0";
textArea.style.border = "none";
textArea.style.outline = "none";
textArea.style.boxShadow = "none";
textArea.style.background = "transparent";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand("copy");
alert("Address is copied to the clipboard.");
} catch (err) {
console.error("Fallback Copy Fehler:", err);

View File

@ -4,6 +4,8 @@ document.addEventListener("DOMContentLoaded", function () {
const searchInput = document.getElementById("search");
let allArticles = [];
function refreshNewsFeed() {
fetch(apiUrl)
.then(response => response.json())
.then(data => {
@ -14,6 +16,8 @@ document.addEventListener("DOMContentLoaded", function () {
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({
fs.writeFileSync(
DATA_FILE,
JSON.stringify(
{
cryptos: ["BTC"],
alarms: [],
notifications: []
}, null, 2));
},
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);
});
});
// 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"
]
}