Improved Licensing

This commit is contained in:
2026-02-18 00:00:32 +01:00
parent a062383af0
commit af2c9e7fd8
17 changed files with 831 additions and 250 deletions

View File

@@ -62,8 +62,7 @@
<td class="px-6 py-4">
<div class="text-sm text-gray-300 space-y-1">
<div>👥 {{ key.max_employees }} Mitarbeiter</div>
<div>📝 {{ key.max_posts_per_day }} Posts/Tag</div>
<div>🔍 {{ key.max_researches_per_day }} Researches/Tag</div>
<div>🪙 {% if key.daily_token_limit %}{{ key.daily_token_limit | int | string }} Tokens/Tag{% else %}Unbegrenzt{% endif %}</div>
</div>
</td>
<td class="px-6 py-4">
@@ -81,13 +80,20 @@
{{ key.created_at.strftime('%d.%m.%Y') if key.created_at else '-' }}
</td>
<td class="px-6 py-4 text-right">
<button onclick="editKey('{{ key.id }}', {{ key.max_employees }}, {{ key.max_posts_per_day }}, {{ key.max_researches_per_day }}, '{{ key.description or '' }}')"
<button onclick="editKey('{{ key.id }}', {{ key.max_employees }}, {{ key.daily_token_limit or '' }}, '{{ key.description or '' }}')"
class="text-blue-400 hover:text-blue-300 p-2 rounded transition-colors"
title="Bearbeiten">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
</button>
<button onclick="openOfferModal('{{ key.id }}', {{ key.max_employees }}, {{ key.daily_token_limit or 0 }}, '{{ key.description or '' }}')"
class="text-green-400 hover:text-green-300 p-2 rounded transition-colors"
title="Angebot senden">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
</button>
{% if not key.used %}
<button onclick="copyKey('{{ key.key }}')"
class="text-brand-highlight hover:text-brand-highlight-dark p-2 rounded transition-colors"
@@ -129,18 +135,11 @@
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">
Max. Posts pro Tag
Tägliches Token-Limit
</label>
<input type="number" id="edit_max_posts_per_day" name="max_posts_per_day" min="1" required
class="w-full px-4 py-3 bg-brand-bg border border-brand-bg-light rounded-lg text-white">
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">
Max. Researches pro Tag
</label>
<input type="number" id="edit_max_researches_per_day" name="max_researches_per_day" min="1" required
<input type="number" id="edit_daily_token_limit" name="daily_token_limit" min="1000" placeholder="z.B. 100000"
class="w-full px-4 py-3 bg-brand-bg border border-brand-bg-light rounded-lg text-white">
<p class="text-xs text-gray-500 mt-1">Leer lassen = unbegrenzt</p>
</div>
<div>
@@ -164,6 +163,80 @@
</div>
</div>
<!-- Offer Modal -->
<div id="offerModal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div class="card-bg rounded-xl border p-8 max-w-lg w-full mx-4">
<h2 class="text-2xl font-bold text-white mb-1">Angebot senden</h2>
<p class="text-gray-400 text-sm mb-6">Erstellt ein professionelles PDF-Angebot und sendet es per E-Mail.</p>
<form id="offerForm" class="space-y-4">
<input type="hidden" id="offer_key_id" name="key_id">
<!-- Calculated Info -->
<div id="offerCalcInfo" class="bg-brand-bg/60 rounded-lg p-4 text-sm space-y-1 border border-gray-600">
<div class="flex justify-between"><span class="text-gray-400">Plan:</span><span id="offer_plan" class="text-white font-medium"></span></div>
<div class="flex justify-between"><span class="text-gray-400">Mitarbeiter:</span><span id="offer_employees" class="text-white"></span></div>
<div class="flex justify-between"><span class="text-gray-400">Token-Limit/Tag:</span><span id="offer_tokens" class="text-white"></span></div>
<div class="flex justify-between"><span class="text-gray-400">Ca. Posts/Monat:</span><span id="offer_posts" class="text-white"></span></div>
<div class="border-t border-gray-600 pt-2 mt-2 flex justify-between"><span class="text-gray-400">Empfohlener Preis:</span><span id="offer_suggested" class="text-brand-highlight font-bold"></span></div>
</div>
<!-- Price -->
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Angebotspreis (€/Monat)</label>
<input type="number" id="offer_price" name="price" step="0.01" min="0" required
class="w-full px-4 py-3 bg-brand-bg border border-brand-bg-light rounded-lg text-white focus:border-brand-highlight focus:outline-none">
</div>
<!-- Payment frequency -->
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Zahlungsweise</label>
<div class="grid grid-cols-3 gap-2" id="freq-buttons">
<button type="button" onclick="selectFreq('monatlich')" id="freq-monatlich"
class="freq-btn active-freq px-4 py-2.5 rounded-lg text-sm font-medium border transition-colors">
Monatlich
</button>
<button type="button" onclick="selectFreq('jährlich')" id="freq-jährlich"
class="freq-btn px-4 py-2.5 rounded-lg text-sm font-medium border transition-colors">
Jährlich
</button>
<button type="button" onclick="selectFreq('einmalig')" id="freq-einmalig"
class="freq-btn px-4 py-2.5 rounded-lg text-sm font-medium border transition-colors">
Einmalig
</button>
</div>
<input type="hidden" id="offer_payment_frequency" name="payment_frequency" value="monatlich">
</div>
<!-- Yearly total hint -->
<div id="yearly-hint" class="hidden text-sm text-gray-400 bg-brand-bg/40 rounded p-3 border border-gray-700">
Jährlicher Gesamtbetrag: <span id="yearly-total" class="text-white font-semibold"></span>
</div>
<!-- Email -->
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">E-Mail-Adresse</label>
<input type="email" id="offer_email" name="email" placeholder="kunde@beispiel.de" required
class="w-full px-4 py-3 bg-brand-bg border border-brand-bg-light rounded-lg text-white focus:border-brand-highlight focus:outline-none">
</div>
<div id="offer-status" class="hidden text-sm rounded-lg px-4 py-3"></div>
<div class="flex gap-3 pt-2">
<button type="submit" id="offerSubmitBtn"
class="flex-1 px-6 py-3 btn-primary rounded-lg font-medium flex items-center justify-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
Angebot senden
</button>
<button type="button" onclick="closeOfferModal()"
class="px-6 py-3 bg-gray-600 hover:bg-gray-700 text-white rounded-lg font-medium transition-colors">
Abbrechen
</button>
</div>
</form>
</div>
</div>
<!-- Generate Modal -->
<div id="generateModal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div class="card-bg rounded-xl border p-8 max-w-md w-full mx-4">
@@ -180,18 +253,11 @@
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">
Max. Posts pro Tag
Tägliches Token-Limit
</label>
<input type="number" name="max_posts_per_day" min="1" value="10" required
class="w-full px-4 py-3 bg-brand-bg border border-brand-bg-light rounded-lg text-white">
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">
Max. Researches pro Tag
</label>
<input type="number" name="max_researches_per_day" min="1" value="5" required
<input type="number" name="daily_token_limit" min="1000" value="100000"
class="w-full px-4 py-3 bg-brand-bg border border-brand-bg-light rounded-lg text-white">
<p class="text-xs text-gray-500 mt-1">Leer lassen = unbegrenzt</p>
</div>
<div>
@@ -216,6 +282,14 @@
</div>
{% endblock %}
{% block head %}
<style>
.freq-btn { background-color: #3d4848; border-color: #5a6868; color: #9ca3af; }
.freq-btn:hover { background-color: #4a5858; color: #e5e7eb; }
.freq-btn.active-freq { background-color: #ffc700; border-color: #ffc700; color: #2d3838; font-weight: 700; }
</style>
{% endblock %}
{% block scripts %}
<script>
function openGenerateModal() {
@@ -227,11 +301,10 @@ function closeGenerateModal() {
document.getElementById('generateForm').reset();
}
function editKey(keyId, maxEmployees, maxPostsPerDay, maxResearchesPerDay, description) {
function editKey(keyId, maxEmployees, dailyTokenLimit, description) {
document.getElementById('edit_key_id').value = keyId;
document.getElementById('edit_max_employees').value = maxEmployees;
document.getElementById('edit_max_posts_per_day').value = maxPostsPerDay;
document.getElementById('edit_max_researches_per_day').value = maxResearchesPerDay;
document.getElementById('edit_daily_token_limit').value = dailyTokenLimit || '';
document.getElementById('edit_description').value = description;
document.getElementById('editModal').classList.remove('hidden');
}
@@ -300,5 +373,108 @@ async function deleteKey(keyId) {
alert('Fehler beim Löschen: ' + error.message);
}
}
// ── Offer Modal ────────────────────────────────────────────────────────────
const AVG_TOKENS_PER_POST = 50000;
const API_COST_PER_1K_EUR = 0.003;
const SERVER_SHARE_EUR = 1.60;
function calcSuggestedPrice(dailyTokenLimit) {
if (!dailyTokenLimit) return null;
const monthlyTokens = dailyTokenLimit * 30;
const apiCostEur = (monthlyTokens / 1000) * API_COST_PER_1K_EUR;
const raw = apiCostEur * 2.5 + SERVER_SHARE_EUR * 2 + 5;
return Math.ceil(raw / 5) * 5; // round up to nearest 5€
}
function fmtNum(n) {
return n.toLocaleString('de-DE');
}
function openOfferModal(keyId, maxEmployees, dailyTokenLimit, description) {
document.getElementById('offer_key_id').value = keyId;
document.getElementById('offer_plan').textContent = description || 'Standard Plan';
document.getElementById('offer_employees').textContent = maxEmployees + ' Nutzer';
if (dailyTokenLimit) {
const postsPerMonth = Math.floor(dailyTokenLimit * 30 / AVG_TOKENS_PER_POST);
document.getElementById('offer_tokens').textContent = fmtNum(dailyTokenLimit) + ' Tokens/Tag';
document.getElementById('offer_posts').textContent = '~' + postsPerMonth + ' Posts';
const suggested = calcSuggestedPrice(dailyTokenLimit);
document.getElementById('offer_suggested').textContent = suggested + ' €';
document.getElementById('offer_price').value = suggested;
} else {
document.getElementById('offer_tokens').textContent = 'Unbegrenzt';
document.getElementById('offer_posts').textContent = '';
document.getElementById('offer_suggested').textContent = '';
}
selectFreq('monatlich');
document.getElementById('offer_email').value = '';
document.getElementById('offer-status').classList.add('hidden');
document.getElementById('offerModal').classList.remove('hidden');
}
function closeOfferModal() {
document.getElementById('offerModal').classList.add('hidden');
document.getElementById('offerForm').reset();
}
function selectFreq(freq) {
document.getElementById('offer_payment_frequency').value = freq;
['monatlich', 'jährlich', 'einmalig'].forEach(f => {
const btn = document.getElementById('freq-' + f);
btn.classList.toggle('active-freq', f === freq);
});
// Show yearly total hint
const price = parseFloat(document.getElementById('offer_price').value) || 0;
const hint = document.getElementById('yearly-hint');
if (freq === 'jährlich' && price > 0) {
document.getElementById('yearly-total').textContent = (price * 12).toLocaleString('de-DE', {minimumFractionDigits: 2}) + ' €';
hint.classList.remove('hidden');
} else {
hint.classList.add('hidden');
}
}
document.getElementById('offer_price').addEventListener('input', () => {
if (document.getElementById('offer_payment_frequency').value === 'jährlich') {
selectFreq('jährlich');
}
});
document.getElementById('offerForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const keyId = formData.get('key_id');
const statusEl = document.getElementById('offer-status');
const submitBtn = document.getElementById('offerSubmitBtn');
submitBtn.disabled = true;
submitBtn.textContent = 'Wird gesendet…';
statusEl.classList.add('hidden');
try {
const response = await fetch(`/admin/api/license-keys/${keyId}/send-offer`, {
method: 'POST',
body: formData
});
const data = await response.json();
if (!response.ok) throw new Error(data.detail || 'Fehler beim Senden');
statusEl.className = 'text-sm rounded-lg px-4 py-3 bg-green-900/40 border border-green-600 text-green-300';
statusEl.textContent = data.message || 'Angebot erfolgreich gesendet!';
statusEl.classList.remove('hidden');
submitBtn.textContent = '✓ Gesendet';
} catch (error) {
statusEl.className = 'text-sm rounded-lg px-4 py-3 bg-red-900/40 border border-red-600 text-red-300';
statusEl.textContent = 'Fehler: ' + error.message;
statusEl.classList.remove('hidden');
submitBtn.disabled = false;
submitBtn.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg> Angebot senden';
}
});
</script>
{% endblock %}

View File

@@ -78,9 +78,17 @@
{% block head %}{% endblock %}
</head>
<body class="text-gray-100 min-h-screen flex">
<body class="text-gray-100 min-h-screen flex {% if limit_reached %}pt-6{% endif %}">
{% if limit_reached %}
<!-- Token Limit Banner -->
<div class="fixed top-0 left-0 right-0 z-[9999] bg-red-600 text-white text-xs font-medium flex items-center justify-center gap-2 px-4" style="height: 1.5rem;">
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/></svg>
Token-Limit erreicht keine KI-Aktionen mehr heute möglich. Morgen wird das Limit zurückgesetzt.
</div>
{% endif %}
<!-- Sidebar -->
<aside id="sidebar" class="w-64 sidebar-bg border-r border-gray-600 flex flex-col fixed h-full">
<aside id="sidebar" class="w-64 sidebar-bg border-r border-gray-600 flex flex-col fixed h-full top-0" {% if limit_reached %}style="top: 1.5rem; height: calc(100vh - 1.5rem);"{% endif %}>
<div class="p-4 border-b border-gray-600">
<div class="flex items-center justify-between gap-3 logo-container">
<div class="logo-full">

View File

@@ -4,9 +4,9 @@
{% block content %}
<!-- Posts Sidebar -->
<div class="posts-sidebar">
<div class="posts-sidebar" {% if limit_reached %}style="top: 1.5rem; height: calc(100vh - 1.5rem);"{% endif %}>
<div class="posts-sidebar-header">
<button onclick="startNewPost()">
<button id="new-post-btn" onclick="startNewPost()" {% if limit_reached %}disabled title="{{ limit_message }}"{% endif %}>
+ Neuer Post
</button>
</div>
@@ -123,6 +123,12 @@ aside.collapsed ~ main .posts-sidebar {
background-color: #e6b300;
}
.posts-sidebar-header button:disabled {
background-color: #4a5568;
color: #a0aec0;
cursor: not-allowed;
}
/* Posts List */
.posts-sidebar-list {
flex: 1;
@@ -226,7 +232,7 @@ aside.collapsed ~ main .chat-fixed-input {
</style>
<!-- Header (Fixed at top) -->
<div class="chat-fixed-header fixed top-0 bg-brand-bg z-20">
<div class="chat-fixed-header fixed bg-brand-bg z-20" style="top: {% if limit_reached %}1.5rem{% else %}0{% endif %}">
<div class="px-8 py-4">
<h1 class="text-xl font-bold text-white">💬 Chat Assistent</h1>
</div>
@@ -277,15 +283,16 @@ aside.collapsed ~ main .chat-fixed-input {
<input
type="text"
id="chat-input"
placeholder="Beschreibe deinen Post..."
class="w-full input-bg border border-gray-600 rounded-full px-6 py-3 text-white focus:outline-none focus:border-brand-highlight transition-colors"
onkeydown="handleChatKeydown(event)">
placeholder="{% if limit_reached %}Token-Limit erreicht morgen wieder verfügbar{% else %}Beschreibe deinen Post...{% endif %}"
class="w-full input-bg border border-gray-600 rounded-full px-6 py-3 text-white focus:outline-none focus:border-brand-highlight transition-colors {% if limit_reached %}opacity-50 cursor-not-allowed{% endif %}"
onkeydown="handleChatKeydown(event)"
{% if limit_reached %}disabled{% endif %}>
</div>
<button
onclick="sendMessage()"
id="send-btn"
class="w-12 h-12 bg-brand-highlight hover:bg-yellow-500 text-black rounded-full transition-all flex items-center justify-center flex-shrink-0 hover:scale-110"
title="Senden (Enter)">
class="w-12 h-12 bg-brand-highlight hover:bg-yellow-500 text-black rounded-full transition-all flex items-center justify-center flex-shrink-0 hover:scale-110 {% if limit_reached %}opacity-50 cursor-not-allowed{% endif %}"
{% if limit_reached %}disabled title="{{ limit_message }}"{% else %}title="Senden (Enter)"{% endif %}>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"/>
</svg>
@@ -310,6 +317,28 @@ let currentPost = null;
let conversationId = null;
let selectedPostTypeId = null;
let currentLoadedPostId = null; // Track active post
let tokenLimitReached = {{ 'true' if limit_reached else 'false' }};
function disableChat(message) {
tokenLimitReached = true;
const input = document.getElementById('chat-input');
const sendBtn = document.getElementById('send-btn');
const newPostBtn = document.getElementById('new-post-btn');
if (input) {
input.disabled = true;
input.placeholder = message || 'Token-Limit erreicht morgen wieder verfügbar';
input.classList.add('opacity-50', 'cursor-not-allowed');
}
if (sendBtn) {
sendBtn.disabled = true;
sendBtn.classList.add('opacity-50', 'cursor-not-allowed');
sendBtn.title = message || 'Token-Limit erreicht';
}
if (newPostBtn) {
newPostBtn.disabled = true;
newPostBtn.title = message || 'Token-Limit erreicht';
}
}
// User profile data
const userProfilePicture = "{{ profile_picture or '' }}";
@@ -348,6 +377,8 @@ function handleChatKeydown(event) {
}
async function sendMessage() {
if (tokenLimitReached) return;
const input = document.getElementById('chat-input');
const message = input.value.trim();
@@ -436,6 +467,12 @@ async function sendMessage() {
saveBtn.classList.remove('hidden');
saveBtn.classList.add('flex');
}
} else if (result.token_limit_exceeded) {
disableChat(result.error);
// Remove temp message, show save option if there's an unsaved new post
if (!currentLoadedPostId && currentPost) {
showTokenLimitModal();
}
} else {
showToast('Fehler: ' + (result.error || 'Unbekannter Fehler'), 'error');
addMessageToChat('ai', '❌ Entschuldigung, es gab einen Fehler. Bitte versuche es erneut.');
@@ -599,6 +636,8 @@ function escapeHtml(text) {
}
function startNewPost() {
if (tokenLimitReached) return;
// Reset state
chatHistory = [];
currentPost = null;
@@ -824,6 +863,40 @@ function showToast(message, type = 'info') {
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// Token limit modal (for new posts when limit is reached mid-session)
function showTokenLimitModal() {
document.getElementById('tokenLimitModal').classList.remove('hidden');
}
function closeLimitModal() {
document.getElementById('tokenLimitModal').classList.add('hidden');
}
async function saveAndCloseLimitModal() {
closeLimitModal();
if (currentPost) {
// Trigger save
const saveBtn = document.getElementById('save-btn');
if (saveBtn && !saveBtn.classList.contains('hidden')) {
saveBtn.click();
} else {
showToast('Bitte speichere deinen Post manuell.', 'error');
}
}
}
</script>
<!-- Token Limit Modal -->
<div id="tokenLimitModal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div class="card-bg rounded-xl border p-6 max-w-md w-full mx-4">
<h3 class="text-lg font-semibold text-white mb-3">Token-Limit erreicht</h3>
<p class="text-gray-400 text-sm mb-4">Das tägliche Token-Limit wurde erreicht. Möchtest du deinen bisherigen Post speichern?</p>
<div class="flex gap-3">
<button onclick="saveAndCloseLimitModal()" class="flex-1 btn-primary px-4 py-2 rounded-lg">Post speichern</button>
<button onclick="closeLimitModal()" class="px-4 py-2 bg-gray-600 rounded-lg text-white">Verwerfen</button>
</div>
</div>
</div>
{% endblock %}

View File

@@ -79,9 +79,18 @@
{% block head %}{% endblock %}
</head>
<body class="text-gray-100 min-h-screen flex">
<body class="text-gray-100 min-h-screen flex {% if limit_reached %}pt-6{% endif %}">
{% if limit_reached %}
<!-- Token Limit Banner -->
<div class="fixed top-0 left-0 right-0 z-[9999] bg-red-600 text-white text-xs font-medium flex items-center justify-center gap-2 px-4" style="height: 1.5rem;">
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/></svg>
Token-Limit erreicht keine KI-Aktionen mehr heute möglich. Morgen wird das Limit zurückgesetzt.
</div>
{% endif %}
<!-- Sidebar -->
<aside id="sidebar" class="w-64 sidebar-bg border-r border-gray-600 flex flex-col fixed h-full">
<aside id="sidebar" class="w-64 sidebar-bg border-r border-gray-600 flex flex-col fixed h-full top-0" {% if limit_reached %}style="top: 1.5rem; height: calc(100vh - 1.5rem);"{% endif %}>
<div class="p-4 border-b border-gray-600">
<div class="flex items-center justify-between gap-3 logo-container">
<div class="logo-full">

View File

@@ -80,46 +80,35 @@
</div>
<!-- Daily Quota Section -->
{% if quota and license_key %}
{% if quota %}
<div class="card-bg border rounded-xl p-6 mb-8">
<h2 class="text-lg font-semibold text-white mb-4">Tägliche Limits</h2>
<h2 class="text-lg font-semibold text-white mb-4">Token-Verbrauch heute</h2>
<div class="grid md:grid-cols-2 gap-4">
<!-- Posts Quota -->
<div class="bg-brand-bg/30 rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<span class="text-sm text-gray-400">Posts heute</span>
<span class="text-xs text-gray-500">{{ quota.posts_created | default(0) }}/{{ license_key.max_posts_per_day }}</span>
</div>
<div class="w-full bg-brand-bg rounded-full h-2">
{% set posts_pct = ((quota.posts_created / license_key.max_posts_per_day * 100) if license_key.max_posts_per_day > 0 else 0) | round %}
<div class="bg-brand-highlight h-2 rounded-full transition-all"
style="width: {{ posts_pct }}%"></div>
</div>
<div class="bg-brand-bg/30 rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<span class="text-sm text-gray-400">Tokens heute</span>
<span class="text-xs text-gray-500">
{{ quota.tokens_used | default(0) }}
/
{% if license_key and license_key.daily_token_limit %}{{ license_key.daily_token_limit }}{% else %}∞{% endif %}
</span>
</div>
<!-- Researches Quota -->
<div class="bg-brand-bg/30 rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<span class="text-sm text-gray-400">Researches heute</span>
<span class="text-xs text-gray-500">{{ quota.researches_created | default(0) }}/{{ license_key.max_researches_per_day }}</span>
</div>
<div class="w-full bg-brand-bg rounded-full h-2">
{% set researches_pct = ((quota.researches_created / license_key.max_researches_per_day * 100) if license_key.max_researches_per_day > 0 else 0) | round %}
<div class="bg-blue-500 h-2 rounded-full transition-all"
style="width: {{ researches_pct }}%"></div>
</div>
<div class="w-full bg-brand-bg rounded-full h-2">
{% if license_key and license_key.daily_token_limit and license_key.daily_token_limit > 0 %}
{% set token_pct = ((quota.tokens_used / license_key.daily_token_limit * 100) | round) %}
{% set token_pct = [token_pct, 100] | min %}
{% else %}
{% set token_pct = 0 %}
{% endif %}
<div class="h-2 rounded-full transition-all {% if token_pct >= 90 %}bg-red-500{% elif token_pct >= 70 %}bg-yellow-500{% else %}bg-brand-highlight{% endif %}"
style="width: {{ token_pct }}%"></div>
</div>
</div>
<p class="text-xs text-gray-500 mt-3">
Limits werden täglich um Mitternacht zurückgesetzt. (Lizenz: {{ license_key.key }})
Limits werden täglich um Mitternacht zurückgesetzt.
{% if license_key %}(Lizenz: {{ license_key.key }}){% else %}Unbegrenzter Account.{% endif %}
</p>
</div>
{% elif quota %}
<!-- No license key - show info message -->
<div class="bg-blue-900/50 border border-blue-500 text-blue-200 px-4 py-3 rounded-lg mb-6">
<strong>Info:</strong> Dieser Account hat keinen Lizenzschlüssel und ist daher unbegrenzt.
</div>
{% endif %}
<!-- Quick Actions -->

View File

@@ -13,23 +13,6 @@
</nav>
</div>
<!-- Limit Warning -->
{% if limit_reached %}
<div class="max-w-2xl mx-auto mb-8">
<div class="bg-red-900/50 border border-red-500 text-red-200 px-6 py-4 rounded-xl">
<div class="flex items-center gap-3">
<svg class="w-6 h-6 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
<div>
<strong class="font-semibold">Limit erreicht</strong>
<p class="text-sm mt-1">{{ limit_message }}</p>
</div>
</div>
</div>
</div>
{% endif %}
<!-- Wizard Container (hidden during generation) -->
<div id="wizardContainer" {% if limit_reached %}style="pointer-events: none; opacity: 0.5;"{% endif %}>
<div class="max-w-2xl mx-auto">

View File

@@ -18,20 +18,6 @@
<p class="text-gray-400">Recherchiere neue Content-Themen für {{ employee_name }}</p>
</div>
<!-- Limit Warning -->
{% if limit_reached %}
<div class="bg-red-900/50 border border-red-500 text-red-200 px-6 py-4 rounded-xl mb-8">
<div class="flex items-center gap-3">
<svg class="w-6 h-6 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
<div>
<strong class="font-semibold">Limit erreicht</strong>
<p class="text-sm mt-1">{{ limit_message }}</p>
</div>
</div>
</div>
{% endif %}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Left: Form -->

View File

@@ -2,23 +2,6 @@
{% block title %}Post erstellen - LinkedIn Posts{% endblock %}
{% block content %}
<!-- Limit Warning -->
{% if limit_reached %}
<div class="max-w-2xl mx-auto mb-8">
<div class="bg-red-900/50 border border-red-500 text-red-200 px-6 py-4 rounded-xl">
<div class="flex items-center gap-3">
<svg class="w-6 h-6 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
<div>
<strong class="font-semibold">Limit erreicht</strong>
<p class="text-sm mt-1">{{ limit_message }}</p>
</div>
</div>
</div>
</div>
{% endif %}
<!-- Wizard Container (hidden during generation) -->
<div id="wizardContainer" {% if limit_reached %}style="pointer-events: none; opacity: 0.5;"{% endif %}>
<div class="max-w-2xl mx-auto">

View File

@@ -730,7 +730,7 @@
<svg class="w-5 h-5 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg>
KI-Verbesserungen
</h3>
<button onclick="loadSuggestions()" id="refreshSuggestionsBtn" class="p-1.5 rounded-lg bg-brand-bg hover:bg-brand-bg-light text-gray-400 hover:text-white transition-colors" title="KI-Vorschläge generieren">
<button onclick="loadSuggestions()" id="refreshSuggestionsBtn" {% if limit_reached %}disabled{% endif %} class="p-1.5 rounded-lg bg-brand-bg hover:bg-brand-bg-light text-gray-400 hover:text-white transition-colors {% if limit_reached %}opacity-50 cursor-not-allowed{% endif %}" title="{% if limit_reached %}{{ limit_message }}{% else %}KI-Vorschläge generieren{% endif %}">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
</button>
</div>
@@ -738,19 +738,19 @@
<!-- Quick Suggestions (Default) -->
<div id="quickSuggestions" class="space-y-2 mb-4">
<p class="text-xs text-gray-500 mb-3">Schnelle Anpassungen:</p>
<button onclick="applyQuickSuggestion('Mache den Hook emotionaler und aufmerksamkeitsstärker')" class="w-full text-left px-3 py-2 bg-brand-bg/50 hover:bg-brand-bg rounded-lg text-sm text-gray-300 hover:text-white transition-colors flex items-center gap-2">
<button {% if not limit_reached %}onclick="applyQuickSuggestion('Mache den Hook emotionaler und aufmerksamkeitsstärker')"{% endif %} {% if limit_reached %}disabled{% endif %} class="w-full text-left px-3 py-2 bg-brand-bg/50 hover:bg-brand-bg rounded-lg text-sm text-gray-300 hover:text-white transition-colors flex items-center gap-2 {% if limit_reached %}opacity-50 cursor-not-allowed{% endif %}">
<span class="text-brand-highlight"></span> Hook verstärken
</button>
<button onclick="applyQuickSuggestion('Füge einen starken Call-to-Action am Ende hinzu')" class="w-full text-left px-3 py-2 bg-brand-bg/50 hover:bg-brand-bg rounded-lg text-sm text-gray-300 hover:text-white transition-colors flex items-center gap-2">
<button {% if not limit_reached %}onclick="applyQuickSuggestion('Füge einen starken Call-to-Action am Ende hinzu')"{% endif %} {% if limit_reached %}disabled{% endif %} class="w-full text-left px-3 py-2 bg-brand-bg/50 hover:bg-brand-bg rounded-lg text-sm text-gray-300 hover:text-white transition-colors flex items-center gap-2 {% if limit_reached %}opacity-50 cursor-not-allowed{% endif %}">
<span class="text-brand-highlight">🎯</span> Call-to-Action hinzufügen
</button>
<button onclick="applyQuickSuggestion('Füge eine kurze persönliche Anekdote oder Erfahrung hinzu')" class="w-full text-left px-3 py-2 bg-brand-bg/50 hover:bg-brand-bg rounded-lg text-sm text-gray-300 hover:text-white transition-colors flex items-center gap-2">
<button {% if not limit_reached %}onclick="applyQuickSuggestion('Füge eine kurze persönliche Anekdote oder Erfahrung hinzu')"{% endif %} {% if limit_reached %}disabled{% endif %} class="w-full text-left px-3 py-2 bg-brand-bg/50 hover:bg-brand-bg rounded-lg text-sm text-gray-300 hover:text-white transition-colors flex items-center gap-2 {% if limit_reached %}opacity-50 cursor-not-allowed{% endif %}">
<span class="text-brand-highlight">📖</span> Storytelling einbauen
</button>
<button onclick="applyQuickSuggestion('Verbessere die Struktur: Kürzere Absätze, mehr Weißraum, bessere Lesbarkeit')" class="w-full text-left px-3 py-2 bg-brand-bg/50 hover:bg-brand-bg rounded-lg text-sm text-gray-300 hover:text-white transition-colors flex items-center gap-2">
<button {% if not limit_reached %}onclick="applyQuickSuggestion('Verbessere die Struktur: Kürzere Absätze, mehr Weißraum, bessere Lesbarkeit')"{% endif %} {% if limit_reached %}disabled{% endif %} class="w-full text-left px-3 py-2 bg-brand-bg/50 hover:bg-brand-bg rounded-lg text-sm text-gray-300 hover:text-white transition-colors flex items-center gap-2 {% if limit_reached %}opacity-50 cursor-not-allowed{% endif %}">
<span class="text-brand-highlight">📝</span> Struktur optimieren
</button>
<button onclick="applyQuickSuggestion('Kürze den Post auf das Wesentliche, entferne überflüssige Worte')" class="w-full text-left px-3 py-2 bg-brand-bg/50 hover:bg-brand-bg rounded-lg text-sm text-gray-300 hover:text-white transition-colors flex items-center gap-2">
<button {% if not limit_reached %}onclick="applyQuickSuggestion('Kürze den Post auf das Wesentliche, entferne überflüssige Worte')"{% endif %} {% if limit_reached %}disabled{% endif %} class="w-full text-left px-3 py-2 bg-brand-bg/50 hover:bg-brand-bg rounded-lg text-sm text-gray-300 hover:text-white transition-colors flex items-center gap-2 {% if limit_reached %}opacity-50 cursor-not-allowed{% endif %}">
<span class="text-brand-highlight">✂️</span> Kürzer & prägnanter
</button>
</div>
@@ -768,8 +768,8 @@
<div class="mt-4 pt-4 border-t border-brand-bg-light">
<label class="text-xs text-gray-400 block mb-2">Eigene Anweisung</label>
<div class="flex gap-2">
<input type="text" id="customSuggestion" placeholder="z.B. Mache es humorvoller" class="flex-1 px-3 py-2 bg-brand-bg border border-brand-bg-light rounded-lg text-sm text-white placeholder-gray-500 focus:outline-none focus:border-brand-highlight">
<button onclick="applyCustomSuggestion()" id="applyCustomBtn" class="px-3 py-2 bg-brand-highlight hover:bg-brand-highlight/90 text-brand-bg-dark rounded-lg transition-colors">
<input type="text" id="customSuggestion" placeholder="z.B. Mache es humorvoller" class="flex-1 px-3 py-2 bg-brand-bg border border-brand-bg-light rounded-lg text-sm text-white placeholder-gray-500 focus:outline-none focus:border-brand-highlight {% if limit_reached %}opacity-50 cursor-not-allowed{% endif %}" {% if limit_reached %}disabled{% endif %}>
<button {% if not limit_reached %}onclick="applyCustomSuggestion()"{% endif %} id="applyCustomBtn" class="px-3 py-2 bg-brand-highlight hover:bg-brand-highlight/90 text-brand-bg-dark rounded-lg transition-colors {% if limit_reached %}opacity-50 cursor-not-allowed{% endif %}" {% if limit_reached %}disabled{% endif %}>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7"/></svg>
</button>
</div>

View File

@@ -7,20 +7,6 @@
<p class="text-gray-400">Recherchiere neue Content-Themen mit Perplexity AI</p>
</div>
<!-- Limit Warning -->
{% if limit_reached %}
<div class="bg-red-900/50 border border-red-500 text-red-200 px-6 py-4 rounded-xl mb-8">
<div class="flex items-center gap-3">
<svg class="w-6 h-6 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
<div>
<strong class="font-semibold">Limit erreicht</strong>
<p class="text-sm mt-1">{{ limit_message }}</p>
</div>
</div>
</div>
{% endif %}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Left: Form -->