Release v1.1.2
This commit is contained in:
commit
97154be246
11 changed files with 71 additions and 23 deletions
3
Makefile
3
Makefile
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = (() => {
|
||||
|
|
|
|||
|
|
@ -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 #1–100, 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.'));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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!`
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue