diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..2654c3d --- /dev/null +++ b/.htaccess @@ -0,0 +1,38 @@ +# TeeTracker .htaccess Configuration +# Enable PUT and DELETE methods for the data directory +# Required for file-based storage to work on Apache servers + + + RewriteEngine On + + # Allow PUT and DELETE for data directory + RewriteCond %{REQUEST_METHOD} ^(PUT|DELETE)$ + RewriteCond %{REQUEST_URI} ^/data/ [NC] + RewriteRule ^ - [L] + + +# Enable all HTTP methods for the data directory + + Require all granted + + + + Require all denied + + +# Set proper MIME types for JSON files + + AddType application/json .json + + +# Disable caching for data files + + + ExpiresActive Off + + + Header set Cache-Control "no-cache, no-store, must-revalidate" + Header set Pragma "no-cache" + Header set Expires 0 + + diff --git a/SERVER_SETUP.md b/SERVER_SETUP.md new file mode 100644 index 0000000..1143d3e --- /dev/null +++ b/SERVER_SETUP.md @@ -0,0 +1,193 @@ +# TeeTracker Server Setup + +## Problem: Dateispeicherung funktioniert nicht + +Die TeeTracker-App versucht, Daten im `data/`-Verzeichnis zu speichern, aber auf den meisten Webservern funktioniert das nicht standardmäßig. Hier sind die Lösungen: + +--- + +## Lösung 1: LocalStorage (Empfohlen für Shared Hosting) + +**Funktioniert immer, ohne Server-Konfiguration!** + +Die App fällt automatisch auf localStorage zurück, wenn die Dateispeicherung nicht verfügbar ist. +- Alle Daten werden im Browser gespeichert +- Funktioniert auf jedem Webserver +- Daten sind nur auf dem aktuellen Gerät/Browser verfügbar + +**Status prüfen:** +- Gehe zu "Einstellungen" → "Datenverwaltung" +- Wenn "Lokaler Modus (localStorage)" angezeigt wird, funktioniert die Dateispeicherung nicht +- Wenn "Dateispeicherung aktiv" angezeigt wird, funktioniert die Dateispeicherung + +--- + +## Lösung 2: Apache Server (.htaccess) + +Falls dein Server Apache verwendet (häufig bei Shared Hosting): + +1. **Stelle sicher, dass die `.htaccess`-Datei aktiv ist:** + - Die Datei liegt bereits im Hauptverzeichnis + - Prüfe, ob `mod_rewrite` aktiviert ist + - Kontaktiere deinen Hosting-Anbieter, falls `.htaccess` ignoriert wird + +2. **Erlaube PUT und DELETE Methoden:** + Die `.htaccess`-Datei enthält bereits die notwendigen Regeln: + ```apache + + Require all granted + + ``` + +3. **Setze die richtigen Berechtigungen:** + ```bash + chmod 755 data/ + chmod 644 data/.gitkeep + ``` + +4. **Aktiviere die notwendigen Apache-Module:** + ```bash + a2enmod rewrite + a2enmod headers + a2enmod expires + service apache2 restart + ``` + +--- + +## Lösung 3: PHP-Backend (Alternative) + +Falls dein Server PHP unterstützt, kannst du ein einfaches Backend erstellen: + +### 1. Erstelle `save_data.php`: +```php + 'Not allowed'])); +} + +if ($_SERVER['REQUEST_METHOD'] === 'GET') { + if (file_exists($filename)) { + readfile($filename); + } else { + echo '[]'; + } +} elseif ($_SERVER['REQUEST_METHOD'] === 'PUT' || $_SERVER['REQUEST_METHOD'] === 'POST') { + $data = file_get_contents('php://input'); + file_put_contents($filename, $data); + echo json_encode(['success' => true]); +} elseif ($_SERVER['REQUEST_METHOD'] === 'DELETE') { + if (file_exists($filename)) { + unlink($filename); + } + echo json_encode(['success' => true]); +} +``` + +### 2. Ändere die `LocalFileStorage`-Klasse: +Ersetze die `fetch()`-Aufrufe durch Aufrufe an dein PHP-Skript. + +--- + +## Lösung 4: Node.js/Express Server + +Falls du Node.js verwenden kannst: + +```javascript +const express = require('express'); +const fs = require('fs'); +const path = require('path'); +const app = express(); + +app.use(express.json()); +app.use(express.static('public')); + +// Enable PUT and DELETE for data directory +app.use('/data', express.raw({ type: '*/*' })); + +app.put('/data/:file', (req, res) => { + const filePath = path.join(__dirname, 'data', req.params.file); + fs.writeFile(filePath, req.body, (err) => { + if (err) return res.status(500).send(err.message); + res.sendStatus(200); + }); +}); + +app.get('/data/:file', (req, res) => { + const filePath = path.join(__dirname, 'data', req.params.file); + if (fs.existsSync(filePath)) { + res.sendFile(filePath); + } else { + res.status(404).send('[]'); + } +}); + +app.delete('/data/:file', (req, res) => { + const filePath = path.join(__dirname, 'data', req.params.file); + if (fs.existsSync(filePath)) { + fs.unlink(filePath, (err) => { + if (err) return res.status(500).send(err.message); + res.sendStatus(200); + }); + } else { + res.sendStatus(200); + } +}); + +app.listen(3000, () => console.log('Server running on port 3000')); +``` + +--- + +## Häufige Probleme und Lösungen + +### Problem: "405 Method Not Allowed" +**Lösung:** Der Server unterstützt PUT/DELETE nicht. Verwende Lösung 2, 3 oder 4. + +### Problem: "403 Forbidden" +**Lösung:** Die Berechtigungen sind falsch. Führe aus: +```bash +chmod 755 data/ +chmod 644 data/* +``` + +### Problem: "404 Not Found" für data/-Dateien +**Lösung:** Das `data/`-Verzeichnis existiert nicht oder die Dateien sind nicht hochgeladen. + +### Problem: CORS-Fehler +**Lösung:** Falls du die App von einer anderen Domain lädst, musst du CORS-Header setzen: +```apache +Header set Access-Control-Allow-Origin "*" +Header set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" +Header set Access-Control-Allow-Headers "Content-Type" +``` + +--- + +## Testen der Dateispeicherung + +1. Öffne die Browser-Konsole (F12 → Console) +2. Gib ein: +```javascript +fetch('data/test.txt', { method: 'PUT', body: 'test' }) + .then(r => r.text()) + .then(console.log) + .catch(console.error); +``` +3. Wenn du eine erfolgreiche Antwort erhältst, funktioniert die Dateispeicherung + +--- + +## Empfehlung + +**Für Shared Hosting:** Verwende einfach localStorage (Lösung 1). Die Daten werden im Browser gespeichert und funktionieren ohne Server-Konfiguration. + +**Für eigene Server:** Konfiguriere Apache mit der `.htaccess`-Datei (Lösung 2) oder verwende das PHP-Backend (Lösung 3). + +**Für Entwickler:** Verwende den Node.js-Server (Lösung 4) oder einen lokalen Entwicklungsserver wie `python -m http.server`. diff --git a/app.js b/app.js index a6a33f1..3d6d429 100644 --- a/app.js +++ b/app.js @@ -1,4 +1,239 @@ -// TeeTracker - Main Application +// Local File Storage +// ============================================ + +class LocalFileStorage { + constructor() { + this.basePath = DATA_DIRECTORY; + this.connected = false; + this.lastError = null; + } + + async testConnection() { + try { + // Try to create a test file + const testFile = this.basePath + 'test_connection.txt'; + const response = await fetch(testFile, { + method: 'PUT', + body: 'test' + }); + + if (response.ok) { + // Clean up test file + await fetch(testFile, { method: 'DELETE' }); + this.connected = true; + this.lastError = null; + return true; + } else { + this.connected = false; + this.lastError = `HTTP Error: ${response.status}`; + return false; + } + } catch (error) { + this.connected = false; + this.lastError = error.message; + console.log('Local file storage not available:', error.message); + return false; + } + } + + async loadFile(filename) { + try { + const url = this.basePath + filename; + const response = await fetch(url, { + method: 'GET' + }); + + if (response.ok) { + const text = await response.text(); + return text.trim() === '' ? [] : (text ? JSON.parse(text) : null); + } else if (response.status === 404) { + return null; + } + return null; + } catch (error) { + console.log(`Error loading ${filename}:`, error.message); + return null; + } + } + + async saveFile(filename, data) { + try { + const url = this.basePath + filename; + const response = await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data, null, 2) + }); + return response.ok; + } catch (error) { + console.log(`Error saving ${filename}:`, error.message); + return false; + } + } + + async fileExists(filename) { + try { + const url = this.basePath + filename; + const response = await fetch(url, { + method: 'HEAD' + }); + return response.ok; + } catch (error) { + return false; + } + } +} +======= +// ============================================ +// Local File Storage +// ============================================ + +class LocalFileStorage { + constructor() { + this.basePath = DATA_DIRECTORY; + this.connected = false; + this.lastError = null; + this.tested = false; + } + + async testConnection() { + if (this.tested) return this.connected; + this.tested = true; + + try { + // First, check if we can read from the data directory + const testFile = this.basePath + 'teas.json'; + const readResponse = await fetch(testFile, { + method: 'GET' + }); + + if (readResponse.ok) { + // If we can read, try to write + const testWriteFile = this.basePath + '.write_test_' + Date.now() + '.tmp'; + const writeResponse = await fetch(testWriteFile, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ test: true }) + }); + + if (writeResponse.ok) { + // Clean up test file + await fetch(testWriteFile, { method: 'DELETE' }).catch(() => {}); + this.connected = true; + this.lastError = null; + return true; + } + } + + this.connected = false; + this.lastError = 'Server unterstützt Dateispeicherung nicht (PUT/DELETE nicht verfügbar)'; + return false; + } catch (error) { + this.connected = false; + this.lastError = error.message; + console.log('Local file storage not available:', error.message); + return false; + } + } + + async loadFile(filename) { + try { + const url = this.basePath + filename; + const response = await fetch(url, { + method: 'GET', + cache: 'no-store' + }); + + if (response.ok) { + const text = await response.text(); + if (!text || text.trim() === '') { + return []; + } + return JSON.parse(text); + } else if (response.status === 404) { + return null; + } + return null; + } catch (error) { + console.log(`Error loading ${filename}:`, error.message); + return null; + } + } + + async saveFile(filename, data) { + try { + // First ensure the data directory exists by trying to create it + // Note: We can't actually create directories via fetch, but we can try to save the file + const url = this.basePath + filename; + + // Try with PUT first (standard for creating/updating files) + let response = await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data, null, 2) + }); + + if (response.ok) { + return true; + } + + // If PUT fails, try POST (some servers use POST for file creation) + response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data, null, 2) + }); + + if (response.ok) { + return true; + } + + console.log(`Failed to save ${filename}: PUT and POST both failed`); + return false; + } catch (error) { + console.log(`Error saving ${filename}:`, error.message); + return false; + } + } + + async fileExists(filename) { + try { + const url = this.basePath + filename; + const response = await fetch(url, { + method: 'HEAD' + }); + return response.ok; + } catch (error) { + return false; + } + } + + async ensureDirectory() { + // Try to create a placeholder file to ensure directory exists + // This works on servers that support PUT for file creation + try { + const placeholder = this.basePath + '.gitkeep'; + const response = await fetch(placeholder, { + method: 'PUT', + headers: { + 'Content-Type': 'text/plain' + }, + body: 'This file ensures the data directory exists' + }); + return response.ok; + } catch (error) { + return false; + } + } +}TeeTracker - Main Application // Version 4.0: Local file storage in data/ directory // ============================================ @@ -150,6 +385,9 @@ class TeeTracker { try { if (this.useFileStorage) { + // Try to ensure data directory exists + await this.fileStorage.ensureDirectory(); + // Try to load from file storage first const teas = await this.fileStorage.loadFile('teas.json'); const entries = await this.fileStorage.loadFile('entries.json'); @@ -159,7 +397,7 @@ class TeeTracker { const localEntries = localStorage.getItem(STORAGE_KEY_ENTRIES); // Load from file storage if available, otherwise use local data - if (teas) { + if (teas && Array.isArray(teas)) { this.teas = teas.map(tea => ({ ...tea, organic: tea.organic !== undefined ? tea.organic : false, @@ -180,7 +418,7 @@ class TeeTracker { this.teas = []; } - if (entries) { + if (entries && Array.isArray(entries)) { this.entries = entries.map(entry => ({ ...entry, teaspoons: entry.teaspoons !== undefined ? entry.teaspoons : 1 @@ -198,6 +436,11 @@ class TeeTracker { } else { this.entries = []; } + + // If we loaded from file storage, save to localStorage as backup + if ((teas || entries) && (localTeas === null || localEntries === null)) { + this.saveToLocalStorage(); + } } else { // Load from localStorage only this.loadFromLocalStorage(); @@ -548,6 +791,12 @@ class TeeTracker { if (trackForm) { trackForm.addEventListener('submit', (e) => this.handleTrackFormSubmit(e)); } + + // Stats period filter + const statsPeriod = document.getElementById('stats-period'); + if (statsPeriod) { + statsPeriod.addEventListener('change', () => this.updateStats()); + } // Export/Import/Clear data buttons const exportBtn = document.getElementById('export-data'); @@ -1579,7 +1828,8 @@ class TeeTracker { // ============================================ updateStats() { - this.updateDetailedStats('all'); + const period = document.getElementById('stats-period')?.value || 'all'; + this.renderDetailedStats(period); // Update charts this.createTeaTypeChart(); @@ -1666,7 +1916,7 @@ class TeeTracker { } // Initialize application -let app; +window.app = new TeeTracker(); document.addEventListener('DOMContentLoaded', () => { - app = new TeeTracker(); + window.app = new TeeTracker(); }); diff --git a/data/.gitkeep b/data/.gitkeep new file mode 100644 index 0000000..72a4f9a --- /dev/null +++ b/data/.gitkeep @@ -0,0 +1,2 @@ +# This file ensures the data directory exists and is tracked by git +# TeeTracker stores teas.json and entries.json in this directory