Feature+Fix: Referral-Admin, Pro-Gates, Karten-Layer, onDogChange, Staging-Media (SW by-v855)
Features: - Admin: Referral-Tab (Virality Factor, Top-Werber, letzte Einladungen) - Karte: Regenradar (RainViewer, zoom→7, color=4), Temperatur-Layer (OWM) mit Zahlen-Grid + Legende - Wetter-Chip: Umschwung-Warnung bei ≥40%-Sprung in Niederschlagswahrscheinlichkeit - Freundschaftsanfragen: Accept/Decline direkt in Notifications (kein Pro nötig) - Freunde-Seite für Standard-User freigeschaltet Pro-Gates: - KI-Trainer, Routenvorschläge, Regenradar, Temperatur-Layer jetzt Pro-Feature - Pro-Badge (P) auf Chips für Admins/Mods in allen Welten + Welten-einrichten - Oranger Banner auf Pro-Seiten für Admin/Mod/Manager Bugfixes: - onDogChange: uebungen.js (Cache leeren + _render), trainingsplaene.js (war leer) - robots.txt vereinfacht (nur Disallow, kein Allow-Durcheinander) - Hintergrund-Foto: Querformat-Filter korrigiert (kein Fallback auf Hochformat) - Staging Media: FileResponse mit korrektem MIME-Type, no-cache statt immutable - Staging Docker: MEDIA_DIR=/data/media + /prod-media:ro Fallback-Handler - Staging-Fix: Bild-Upload auf zweitem Hund (war Read-only file system)
This commit is contained in:
parent
2f021f54c2
commit
79fa5684b9
22 changed files with 570 additions and 58 deletions
|
|
@ -203,6 +203,8 @@ window.Page_map = (() => {
|
|||
|
||||
<div class="map-fabs">
|
||||
<button class="map-fab map-fab--pin" id="map-pin-btn" title="Marker setzen"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#push-pin"></use></svg></button>
|
||||
<button class="map-fab" id="map-radar-btn" title="Regenradar ein-/ausblenden"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#cloud-rain"></use></svg></button>
|
||||
<button class="map-fab" id="map-temp-btn" title="Temperatur ein-/ausblenden"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#thermometer"></use></svg></button>
|
||||
<button class="map-fab" id="map-locate-btn" title="Meinen Standort"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#map-pin"></use></svg></button>
|
||||
</div>
|
||||
|
||||
|
|
@ -285,6 +287,186 @@ window.Page_map = (() => {
|
|||
});
|
||||
|
||||
document.getElementById('map-pin-btn').addEventListener('click', _togglePlacementMode);
|
||||
document.getElementById('map-radar-btn').addEventListener('click', _toggleRadar);
|
||||
document.getElementById('map-temp-btn').addEventListener('click', _toggleTemp);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// REGENRADAR (RainViewer) + OWM-LAYER (Temperatur etc.)
|
||||
// ----------------------------------------------------------
|
||||
let _radarLayer = null;
|
||||
let _radarActive = false;
|
||||
let _radarTimer = null;
|
||||
let _tempLayer = null;
|
||||
let _tempActive = false;
|
||||
let _tempMarkers = [];
|
||||
let _tempDebounce = null;
|
||||
|
||||
async function _toggleRadar() {
|
||||
if (!App.hasPro(_appState?.user)) {
|
||||
UI.toast.info('Regenradar ist ein Pro-Feature.');
|
||||
return;
|
||||
}
|
||||
const btn = document.getElementById('map-radar-btn');
|
||||
if (_radarActive) {
|
||||
_radarActive = false;
|
||||
if (_radarLayer) { _map.removeLayer(_radarLayer); _radarLayer = null; }
|
||||
clearInterval(_radarTimer);
|
||||
btn?.classList.remove('active');
|
||||
return;
|
||||
}
|
||||
_radarActive = true;
|
||||
btn?.classList.add('active');
|
||||
if (_map && _map.getZoom() > 7) _map.setZoom(7);
|
||||
await _loadRadar();
|
||||
_radarTimer = setInterval(_loadRadar, 5 * 60 * 1000);
|
||||
}
|
||||
|
||||
async function _toggleTemp() {
|
||||
if (!App.hasPro(_appState?.user)) {
|
||||
UI.toast.info('Temperatur-Layer ist ein Pro-Feature.');
|
||||
return;
|
||||
}
|
||||
const btn = document.getElementById('map-temp-btn');
|
||||
if (_tempActive) {
|
||||
_tempActive = false;
|
||||
if (_tempLayer) { _map.removeLayer(_tempLayer); _tempLayer = null; }
|
||||
_tempMarkers.forEach(m => _map.removeLayer(m));
|
||||
_tempMarkers = [];
|
||||
clearTimeout(_tempDebounce);
|
||||
_map.off('moveend zoomend', _debounceTempLabels);
|
||||
document.getElementById('map-temp-legend')?.remove();
|
||||
btn?.classList.remove('active');
|
||||
return;
|
||||
}
|
||||
_tempActive = true;
|
||||
btn?.classList.add('active');
|
||||
try {
|
||||
const cfg = await API.get('/weather/layer-tiles?layer=temp_new');
|
||||
_tempLayer = window.L.tileLayer(cfg.url, {
|
||||
opacity: 1.0,
|
||||
tileSize: 256,
|
||||
zIndex: 290,
|
||||
maxNativeZoom: cfg.maxNativeZoom ?? 18,
|
||||
maxZoom: 18,
|
||||
attribution: 'Temp © <a href="https://openweathermap.org">OpenWeatherMap</a>',
|
||||
}).addTo(_map);
|
||||
_showTempLegend();
|
||||
_map.on('moveend zoomend', _debounceTempLabels);
|
||||
await _loadTempLabels();
|
||||
} catch {
|
||||
_tempActive = false;
|
||||
btn?.classList.remove('active');
|
||||
UI.toast.error('Temperatur-Layer nicht verfügbar.');
|
||||
}
|
||||
}
|
||||
|
||||
function _debounceTempLabels() {
|
||||
clearTimeout(_tempDebounce);
|
||||
_tempDebounce = setTimeout(_loadTempLabels, 600);
|
||||
}
|
||||
|
||||
function _tempColor(t) {
|
||||
if (t <= -10) return '#0033cc';
|
||||
if (t <= 0) return '#0099ff';
|
||||
if (t <= 10) return '#00cc88';
|
||||
if (t <= 15) return '#88cc00';
|
||||
if (t <= 20) return '#ffcc00';
|
||||
if (t <= 25) return '#ff8800';
|
||||
if (t <= 30) return '#ff3300';
|
||||
return '#990000';
|
||||
}
|
||||
|
||||
async function _loadTempLabels() {
|
||||
if (!_tempActive || !_map) return;
|
||||
const bounds = _map.getBounds();
|
||||
const n = bounds.getNorth(), s = bounds.getSouth();
|
||||
const e = bounds.getEast(), w = bounds.getWest();
|
||||
|
||||
// 3×3 Raster
|
||||
const rows = 3, cols = 3;
|
||||
const points = [];
|
||||
for (let r = 0; r < rows; r++) {
|
||||
for (let c = 0; c < cols; c++) {
|
||||
const lat = s + (n - s) * (r + 0.5) / rows;
|
||||
const lon = w + (e - w) * (c + 0.5) / cols;
|
||||
points.push([lat, lon]);
|
||||
}
|
||||
}
|
||||
|
||||
const results = await Promise.all(points.map(([lat, lon]) =>
|
||||
fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat.toFixed(2)}&longitude=${lon.toFixed(2)}¤t=temperature_2m&timezone=auto`, { cache: 'no-store' })
|
||||
.then(r => r.json())
|
||||
.then(d => ({ lat, lon, t: d.current?.temperature_2m }))
|
||||
.catch(() => null)
|
||||
));
|
||||
|
||||
// Alte Marker entfernen
|
||||
_tempMarkers.forEach(m => _map.removeLayer(m));
|
||||
_tempMarkers = [];
|
||||
|
||||
results.filter(Boolean).forEach(({ lat, lon, t }) => {
|
||||
if (t == null) return;
|
||||
const temp = Math.round(t);
|
||||
const color = _tempColor(temp);
|
||||
const icon = window.L.divIcon({
|
||||
className: '',
|
||||
html: `<div style="background:${color};color:#fff;font-size:12px;font-weight:800;
|
||||
padding:2px 6px;border-radius:10px;white-space:nowrap;
|
||||
box-shadow:0 1px 4px rgba(0,0,0,0.5);text-shadow:0 1px 2px rgba(0,0,0,0.4)">${temp}°</div>`,
|
||||
iconSize: null,
|
||||
iconAnchor: [20, 10],
|
||||
});
|
||||
const m = window.L.marker([lat, lon], { icon, zIndexOffset: 500, interactive: false });
|
||||
m.addTo(_map);
|
||||
_tempMarkers.push(m);
|
||||
});
|
||||
}
|
||||
|
||||
function _showTempLegend() {
|
||||
const existing = document.getElementById('map-temp-legend');
|
||||
if (existing) return;
|
||||
const steps = [
|
||||
{ c: '#0000cc', v: '−20°' }, { c: '#0055ff', v: '−10°' },
|
||||
{ c: '#00aaff', v: '0°' }, { c: '#00ffaa', v: '10°' },
|
||||
{ c: '#aaff00', v: '15°' }, { c: '#ffee00', v: '20°' },
|
||||
{ c: '#ff8800', v: '25°' }, { c: '#ff2200', v: '30°' },
|
||||
{ c: '#990000', v: '35°' },
|
||||
];
|
||||
const gradient = steps.map(s => s.c).join(',');
|
||||
const labels = steps.map(s =>
|
||||
`<span style="flex:1;text-align:center;font-size:9px;color:#fff;text-shadow:0 0 3px #000">${s.v}</span>`
|
||||
).join('');
|
||||
const el = document.createElement('div');
|
||||
el.id = 'map-temp-legend';
|
||||
el.style.cssText = `position:absolute;bottom:36px;left:50%;transform:translateX(-50%);
|
||||
z-index:800;background:rgba(0,0,0,0.55);border-radius:6px;padding:4px 8px;
|
||||
min-width:220px;pointer-events:none`;
|
||||
el.innerHTML = `
|
||||
<div style="height:10px;border-radius:3px;background:linear-gradient(to right,${gradient});margin-bottom:2px"></div>
|
||||
<div style="display:flex">${labels}</div>`;
|
||||
document.getElementById('central-map')?.appendChild(el);
|
||||
}
|
||||
|
||||
async function _loadRadar() {
|
||||
if (!_radarActive || !_map) return;
|
||||
try {
|
||||
const resp = await fetch('https://api.rainviewer.com/public/weather-maps.json', { cache: 'no-store' });
|
||||
const data = await resp.json();
|
||||
const frames = [...(data.radar?.past || []), ...(data.radar?.nowcast || [])];
|
||||
if (!frames.length) return;
|
||||
const latest = frames[frames.length - 1].path;
|
||||
const url = `https://tilecache.rainviewer.com${latest}/256/{z}/{x}/{y}/4/1_1.png`;
|
||||
if (_radarLayer) _map.removeLayer(_radarLayer);
|
||||
_radarLayer = window.L.tileLayer(url, {
|
||||
opacity: 0.7,
|
||||
tileSize: 256,
|
||||
zIndex: 300,
|
||||
maxNativeZoom: 7,
|
||||
maxZoom: 18,
|
||||
attribution: 'Radar © <a href="https://rainviewer.com">RainViewer</a>',
|
||||
}).addTo(_map);
|
||||
} catch { /* still */ }
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -1634,12 +1816,15 @@ window.Page_map = (() => {
|
|||
const regen = w.precip_prob != null
|
||||
? (w.next_rain_time ? ` · 💧 ${w.precip_prob}% ab ${w.next_rain_time}` : ` · 💧 ${w.precip_prob}%`)
|
||||
: '';
|
||||
const warning = w.rain_warning_time
|
||||
? ` · <span style="color:#f59e0b;font-weight:700">⚠ ab ${w.rain_warning_time}</span>`
|
||||
: '';
|
||||
let zecken = '';
|
||||
if (w.zecken_warnung) {
|
||||
const col = w.zecken_warnung === 'hoch' ? '#991B1B' : '#92400E';
|
||||
zecken = ` · <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="${col}" style="display:inline;width:14px;height:14px;vertical-align:-2px" title="Zeckenrisiko ${w.zecken_warnung}"><ellipse cx="128" cy="68" rx="26" ry="22"/><ellipse cx="128" cy="158" rx="88" ry="80"/><path d="M52,120 Q20,106 8,94" fill="none" stroke="${col}" stroke-width="14" stroke-linecap="round"/><path d="M44,142 Q12,136 2,132" fill="none" stroke="${col}" stroke-width="14" stroke-linecap="round"/><path d="M44,164 Q12,162 2,162" fill="none" stroke="${col}" stroke-width="14" stroke-linecap="round"/><path d="M52,184 Q22,192 10,204" fill="none" stroke="${col}" stroke-width="14" stroke-linecap="round"/><path d="M204,120 Q236,106 248,94" fill="none" stroke="${col}" stroke-width="14" stroke-linecap="round"/><path d="M212,142 Q244,136 254,132" fill="none" stroke="${col}" stroke-width="14" stroke-linecap="round"/><path d="M212,164 Q244,162 254,162" fill="none" stroke="${col}" stroke-width="14" stroke-linecap="round"/><path d="M204,184 Q234,192 246,204" fill="none" stroke="${col}" stroke-width="14" stroke-linecap="round"/></svg>`;
|
||||
}
|
||||
info.innerHTML = `${icon} ${temp} ${w.desc}${regen}${zecken}`;
|
||||
info.innerHTML = `${icon} ${temp} ${w.desc}${regen}${warning}${zecken}`;
|
||||
info.classList.remove('map-weather-chip--hidden');
|
||||
sep.classList.remove('map-weather-chip--hidden');
|
||||
} catch { /* still */ }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue