Release v1.1.2

This commit is contained in:
rene 2026-04-30 15:28:05 +02:00
commit 97154be246
11 changed files with 71 additions and 23 deletions

View file

@ -138,7 +138,8 @@ release: check-ssh
@git checkout main
@git merge develop --no-ff -m "Release v$(VERSION)"
@sed -i '' 's/"version": "[^"]*"/"version": "$(VERSION)"/' backend/static/manifest.json
@git add backend/static/manifest.json
@sed -i '' "s/const APP_VERSION = '[^']*'/const APP_VERSION = '$(VERSION)'/" backend/static/js/app.js
@git add backend/static/manifest.json backend/static/js/app.js
@git commit --amend --no-edit
@git tag "v$(VERSION)"
@git push $(GIT_REMOTE) main --tags

View file

@ -87,7 +87,7 @@ def get_current_user(
user_id = int(payload["sub"])
with db() as conn:
row = conn.execute(
"SELECT id, email, name, rolle, is_premium, is_moderator, is_banned, ban_reason, is_social_media, notes_ki_enabled, breeder_status, is_founder, is_partner FROM users WHERE id=?",
"SELECT id, email, name, rolle, is_premium, is_moderator, is_banned, ban_reason, is_social_media, notes_ki_enabled, breeder_status, is_founder, is_partner, founder_number FROM users WHERE id=?",
(user_id,)
).fetchone()

View file

@ -296,6 +296,7 @@ async def list_users(
rows = conn.execute(f"""
SELECT u.id, u.name, {_email_col}, u.rolle, u.is_premium,
u.is_moderator, u.is_banned, u.ban_reason,
u.is_founder, u.is_partner, u.founder_number,
u.created_at, u.last_login,
(SELECT COUNT(*) FROM dogs d WHERE d.user_id=u.id) AS dog_count,
(SELECT COUNT(*) FROM forum_threads t WHERE t.user_id=u.id AND t.is_deleted=0) AS thread_count,

View file

@ -32,6 +32,7 @@ async def list_friends(user=Depends(get_current_user)):
u.name AS friend_name,
u.bio, u.wohnort, u.erfahrung, u.social_link,
u.profil_sichtbarkeit, u.avatar_url,
u.is_founder, u.is_partner, u.founder_number,
{dogs_sq} AS dogs_json
FROM friendships f
JOIN users u ON u.id = CASE WHEN f.requester_id=? THEN f.addressee_id ELSE f.requester_id END
@ -92,6 +93,7 @@ async def search_users(q: str = "", user=Depends(get_current_user)):
SELECT u.id, u.name,
u.bio, u.wohnort, u.erfahrung, u.social_link,
u.profil_sichtbarkeit, u.avatar_url,
u.is_founder, u.is_partner, u.founder_number,
(SELECT json_group_array(json_object('name', d.name, 'rasse', d.rasse))
FROM dogs d WHERE d.user_id=u.id AND d.is_public=1) AS dogs_json
FROM users u

View file

@ -3,8 +3,8 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
const APP_VER = '522'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.0.0'; // ← semantische Version, wird bei make release gesetzt
const APP_VER = '533'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.1.2'; // ← semantische Version, wird bei make release gesetzt
const IS_STAGING = location.hostname === 'staging.banyaro.app';
const App = (() => {

View file

@ -19,7 +19,7 @@ window.Page_admin = (() => {
{ id: 'analytics', label: 'Analytics', icon: 'target' },
{ id: 'system', label: 'System', icon: 'gear' },
{ id: 'jobs', label: 'Jobs', icon: 'clock' },
{ id: 'partner', label: 'Partner & Codes', icon: 'handshake' },
{ id: 'partner', label: 'Partner', icon: 'handshake' },
{ id: 'audit', label: 'Audit-Log', icon: 'clipboard-text' },
];
@ -1792,13 +1792,23 @@ window.Page_admin = (() => {
// TAB: AUDIT-LOG
// ------------------------------------------------------------------
async function _renderPartner(el) {
const [codes] = await Promise.all([
API.get('/api/admin/partner/codes'),
]);
const codes = (await API.get('/admin/partner/codes')) || [];
el.innerHTML = `
<div style="display:flex;flex-direction:column;gap:var(--space-5)">
<!-- Anleitung -->
<div class="by-card" style="padding:var(--space-4);background:var(--c-surface-2);border-left:3px solid var(--c-primary)">
<h3 style="margin:0 0 var(--space-3);font-size:var(--text-sm);font-weight:700">So funktioniert das Partner-System</h3>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);display:flex;flex-direction:column;gap:var(--space-2)">
<p style="margin:0"><strong>1. Partner-Code erstellen</strong> Erstelle einen Code (z. B. <code>HUNDEBLOG</code>) für einen Influencer oder Partner. Der Code wird an die Person weitergegeben.</p>
<p style="margin:0"><strong>2. Registrierung mit Code</strong> Wenn sich ein neuer User mit diesem Code registriert, wird er automatisch als <em>Gründer</em> markiert (Platz #1100, lebenslang kostenlos). Du siehst in der Tabelle wie viele Einlösungen jeder Code hat.</p>
<p style="margin:0"><strong>3. Partner-Status vergeben</strong> Den Influencer selbst suchst du unten bei «Nutzer-Status» und setzt <em>Partner-Badge</em> (blaues Badge im Profil) und <em>Gründer-Lizenz</em>. So ist auch er als Gründer #X sichtbar.</p>
<p style="margin:0"><strong>Max. 100 Gründer</strong> Ist die Zahl bei einem Code leer, ist sie unbegrenzt. Die globale Grenze über alle Codes hinweg sind 100 Gründer-Plätze.</p>
<p style="margin:0"><strong>Freunde werben</strong> Jeder eingeloggte User hat einen persönlichen Einladungslink (Einstellungen Freunde werben). Bei 10 geworbenen Usern gibt es 20 % Rabatt, bei 20 30 %, bei 50 50 % lebenslang, sobald Bezahlfunktionen aktiv sind.</p>
</div>
</div>
<!-- Neuen Code anlegen -->
<div class="by-card" style="padding:var(--space-4)">
<h3 style="margin:0 0 var(--space-3);font-size:var(--text-base)">Neuen Partner-Code erstellen</h3>
@ -1915,7 +1925,7 @@ window.Page_admin = (() => {
const code = (fd.code || '').trim().toUpperCase();
if (!code) return;
await UI.asyncButton(btn, async () => {
await API.post('/api/admin/partner/codes', {
await API.post('/admin/partner/codes', {
code,
label: fd.label || code,
grants_founder: e.target.querySelector('[name="grants_founder"]').checked ? 1 : 0,
@ -1932,7 +1942,7 @@ window.Page_admin = (() => {
if (!window.confirm(`Code wirklich löschen?`)) return;
const id = btn.dataset.id;
await UI.asyncButton(btn, async () => {
await API.del(`/api/admin/partner/codes/${id}`);
await API.del(`/admin/partner/codes/${id}`);
UI.toast.success('Code gelöscht.');
await _renderPartner(el);
});
@ -1948,22 +1958,24 @@ window.Page_admin = (() => {
clearTimeout(_searchTimeout);
_grantUserId = null;
const q = searchInput.value.trim();
if (q.length < 2) { grantResult.innerHTML = ''; return; }
if (q.length < 1) { grantResult.innerHTML = ''; return; }
_searchTimeout = setTimeout(async () => {
try {
const users = await API.get(`/api/admin/users/search?q=${encodeURIComponent(q)}`);
if (!users.length) {
const res = await API.get(`/admin/users?q=${encodeURIComponent(q)}&limit=10`);
const users = res?.users || res || [];
if (!users || !users.length) {
grantResult.innerHTML = `<p style="font-size:var(--text-xs);color:var(--c-text-muted)">Kein User gefunden.</p>`;
return;
}
grantResult.innerHTML = users.map(u => `
<div class="adm-grant-user" data-id="${u.id}" data-name="${u.name}"
data-founder="${u.is_founder||0}" data-partner="${u.is_partner||0}"
style="padding:var(--space-2) var(--space-3);border-radius:var(--radius-sm);
cursor:pointer;background:var(--c-surface-2);margin-bottom:2px;
font-size:var(--text-sm);display:flex;justify-content:space-between">
<span><strong>${u.name}</strong></span>
<span style="color:var(--c-text-muted);font-size:var(--text-xs)">
${u.is_founder ? ' Gründer ' : ''}${u.is_partner ? '🤝 Partner' : ''}
${u.rolle}${u.is_founder ? ' · ⭐' : ''}${u.is_partner ? ' · 🤝' : ''}
</span>
</div>
`).join('');
@ -1971,10 +1983,18 @@ window.Page_admin = (() => {
div.addEventListener('click', () => {
_grantUserId = parseInt(div.dataset.id);
searchInput.value = div.dataset.name;
grantResult.innerHTML = `<p style="font-size:var(--text-xs);color:var(--c-success,#16a34a)">✓ ${div.dataset.name} ausgewählt</p>`;
// Aktuellen Status in Checkboxen setzen
const form = el.querySelector('#adm-partner-grant');
if (form) {
form.querySelector('[name="is_founder"]').checked = div.dataset.founder === '1';
form.querySelector('[name="is_partner"]').checked = div.dataset.partner === '1';
}
grantResult.innerHTML = `<p style="font-size:var(--text-xs);color:var(--c-success,#16a34a)">✓ ${div.dataset.name} ausgewählt${div.dataset.founder==='1' ? ' · ⭐ Gründer' : ''}${div.dataset.partner==='1' ? ' · 🤝 Partner' : ''}</p>`;
});
});
} catch { grantResult.innerHTML = ''; }
} catch(e) {
grantResult.innerHTML = `<p style="font-size:var(--text-xs);color:var(--c-danger)">${e.message || 'Suchfehler'}</p>`;
}
}, 400);
});
@ -1985,13 +2005,14 @@ window.Page_admin = (() => {
const isFounder = e.target.querySelector('[name="is_founder"]').checked ? 1 : 0;
const isPartner = e.target.querySelector('[name="is_partner"]').checked ? 1 : 0;
await UI.asyncButton(btn, async () => {
const result = await API.post(`/api/admin/partner/users/${_grantUserId}/grant`, {
const result = await API.post(`/admin/partner/users/${_grantUserId}/grant`, {
is_founder: isFounder,
is_partner: isPartner,
});
if (!result) throw new Error('Keine Antwort vom Server.');
UI.toast.success(`Status für ${result.name} gesetzt.`);
grantResult.innerHTML = `<p style="font-size:var(--text-xs);color:var(--c-success,#16a34a)">✓ Gründer: ${result.is_founder ? 'Ja' : 'Nein'} | Partner: ${result.is_partner ? 'Ja' : 'Nein'}</p>`;
});
}).catch(e => UI.toast.error(e.message || 'Fehler beim Speichern.'));
});
}

View file

@ -610,10 +610,23 @@ window.Page_friends = (() => {
margin-bottom:var(--space-4)">${parts.join('')}</div>`;
})();
const badgesHTML = (profile.is_founder || profile.is_partner) ? `
<div style="display:flex;gap:var(--space-2);margin-bottom:var(--space-3);flex-wrap:wrap">
${profile.is_founder ? `<span class="badge" style="background:#7c3aed;color:#fff">
<svg class="ph-icon" aria-hidden="true" style="width:12px;height:12px"><use href="/icons/phosphor.svg#key"></use></svg>
${profile.founder_number ? `Gründer #${profile.founder_number}` : 'Gründer'}
</span>` : ''}
${profile.is_partner ? `<span class="badge" style="background:#0ea5e9;color:#fff">
<svg class="ph-icon" aria-hidden="true" style="width:12px;height:12px"><use href="/icons/phosphor.svg#handshake"></use></svg>
Partner
</span>` : ''}
</div>` : '';
UI.modal.open({
title: _esc(friendName),
body: `
<div>
${badgesHTML}
${profileInfoHTML}
<div class="by-section-label">${dogs.length === 1 ? 'Hund' : 'Hunde'}</div>
${dogsHTML}
@ -667,10 +680,12 @@ window.Page_friends = (() => {
${i < results.length - 1 ? 'border-bottom:1px solid var(--c-border)' : ''}">
${_userAvatar(u.name, null, u.avatar_url)}
<div style="flex:1;min-width:0">
<div style="display:flex;align-items:center;flex-wrap:wrap;gap:2px;
<div style="display:flex;align-items:center;flex-wrap:wrap;gap:4px;
margin-bottom:2px">
<span style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text)">${_esc(u.name)}</span>
${u.is_founder ? `<span style="font-size:10px;font-weight:700;background:#7c3aed;color:#fff;padding:1px 5px;border-radius:4px">${u.founder_number ? `Gründer #${u.founder_number}` : 'Gründer'}</span>` : ''}
${u.is_partner ? `<span style="font-size:10px;font-weight:700;background:#0ea5e9;color:#fff;padding:1px 5px;border-radius:4px">Partner</span>` : ''}
${_erfahrungSpan(u.erfahrung)}
</div>
${_wohnortLine(u.wohnort)}

View file

@ -39,7 +39,7 @@ window.Page_gruender = (() => {
async function _load() {
const el = _container.querySelector('#grnd-content');
try {
const d = await API.get('/api/partner/founders/stats');
const d = await API.get('/partner/founders/stats');
if (!d || typeof d.total === 'undefined') throw new Error('Ungültige Antwort vom Server.');
el.innerHTML = _renderStats(d);
} catch (e) {

View file

@ -54,6 +54,14 @@ window.Page_settings = (() => {
_container = container;
_appState = appState;
_render();
// Frischen User-State laden damit Badges (is_founder, is_partner) aktuell sind
if (_appState.user) {
try {
const fresh = await API.auth.me();
Object.assign(_appState.user, fresh);
_render();
} catch {}
}
}
function refresh() {
@ -1450,7 +1458,7 @@ window.Page_settings = (() => {
if (code.length < 3) return;
_debounce = setTimeout(async () => {
try {
const info = await API.get(`/api/partner/codes/${encodeURIComponent(code)}/info`);
const info = await API.get(`/partner/codes/${encodeURIComponent(code)}/info`);
if (info.redeemable) {
partnerHint.textContent = info.grants_founder
? `✓ Gültiger Code von "${info.label}" — du erhältst eine lebenslange Gründer-Lizenz!`

View file

@ -1,6 +1,6 @@
{
"id": "/",
"version": "1.1.1",
"version": "1.1.2",
"name": "Ban Yaro — Die Hunde-Plattform",
"short_name": "Ban Yaro",
"description": "Alles rund um deinen Hund. Von Welpe bis Opa.",

View file

@ -3,7 +3,7 @@
Offline-Cache + Push Notifications + Tile-Cache
============================================================ */
const CACHE_VERSION = 'by-v545';
const CACHE_VERSION = 'by-v556';
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache