Compare commits

..

11 Commits

Author SHA1 Message Date
Aiirondev_dev 1d6c722cd3 Implement dynamic page size for mobile devices and optimize rendering with content visibility 2026-04-19 21:43:17 +02:00
Aiirondev_dev 3e48aa23cc Refactor scroll distance calculation and prefetch logic for mobile layout in main and admin templates 2026-04-19 21:36:41 +02:00
Aiirondev_dev e2f8da213a Remove manifest and service worker routes; update base.html to reflect changes 2026-04-19 21:15:40 +02:00
Aiirondev_dev 91fc5cae21 Add routes to serve manifest.json and service worker; update manifest link in base.html 2026-04-19 20:06:11 +02:00
Aiirondev_dev b47fcdf804 Refactor manifest.json for Ausleihsystem; add service worker for caching and offline support 2026-04-19 19:58:26 +02:00
Aiirondev_dev 92bea9a87e Remove student account fields and related logic from registration process 2026-04-19 19:38:46 +02:00
Aiirondev_dev 84ac9e812a Enhance get_current_status logic to maintain 'completed' status and add TTL for push notifications 2026-04-19 16:57:35 +02:00
Aiirondev_dev d2f882ed61 Add booking type options and enhance booking logic in terminplan.html; create test for terminplan endpoint 2026-04-19 16:25:30 +02:00
Aiirondev_dev 4c586abee0 Fix typo in requirements.txt (missing newline merged cryptography and pywebpush) 2026-04-19 15:39:42 +02:00
Aiirondev_dev 50c4096f3f Auto-generate VAPID keys upon startup using py-vapid, and add pywebpush to requirements, drop obsolete script 2026-04-19 15:34:13 +02:00
Aiirondev_dev c42ba39882 Integrate frontend UI to request Web Push permission and show setting panel 2026-04-19 15:05:20 +02:00
22 changed files with 159 additions and 303 deletions
+3 -1
View File
@@ -4,4 +4,6 @@ certs
build
.venv
__pycache__
.pyc
.pycvapid.json
Web/vapid.json
Web/vapid_*.pem
View File
+4 -20
View File
@@ -7060,9 +7060,7 @@ def register():
permission_preset = (request.form.get('permission_preset') or 'standard_user').strip()
use_custom_permissions = request.form.get('use_custom_permissions') == 'on'
is_student = bool(request.form.get('is_student')) if cfg.STUDENT_CARDS_MODULE_ENABLED else False
student_card_id = us.normalize_student_card_id(request.form.get('student_card_id')) if cfg.STUDENT_CARDS_MODULE_ENABLED else ''
max_borrow_days_raw = request.form.get('max_borrow_days') if cfg.STUDENT_CARDS_MODULE_ENABLED else None
if not username or not password or not name or not last_name:
flash('Bitte füllen Sie alle Felder aus', 'error')
return redirect(url_for('register'))
@@ -7081,28 +7079,14 @@ def register():
for endpoint_name, _ in PERMISSION_PAGE_OPTIONS:
page_permissions[endpoint_name] = request.form.get(f'page_{endpoint_name}') == 'on'
max_borrow_days = None
if is_student:
if not student_card_id:
flash('Für Schüler muss eine Ausweis-ID angegeben werden.', 'error')
return redirect(url_for('register'))
if us.student_card_exists(student_card_id):
flash('Die Ausweis-ID ist bereits vergeben.', 'error')
return redirect(url_for('register'))
try:
max_borrow_days = int(max_borrow_days_raw) if max_borrow_days_raw else cfg.STUDENT_DEFAULT_BORROW_DAYS
except (TypeError, ValueError):
max_borrow_days = cfg.STUDENT_DEFAULT_BORROW_DAYS
max_borrow_days = max(1, min(max_borrow_days, cfg.STUDENT_MAX_BORROW_DAYS))
us.add_user(
username,
password,
name,
last_name,
is_student=is_student,
student_card_id=student_card_id if is_student else None,
max_borrow_days=max_borrow_days,
is_student=False,
student_card_id=None,
max_borrow_days=None,
permission_preset=permission_preset,
action_permissions=action_permissions,
page_permissions=page_permissions,
+4 -4
View File
@@ -71,6 +71,10 @@ def get_current_status(ausleihung, log_changes=False, user=None):
# Bei stornierten Ausleihungen bleibt der Status immer storniert
if original_status == 'cancelled':
return 'cancelled'
# Wenn die Ausleihung bereits abgeschlossen ist, bleibt sie es
if original_status == 'completed':
return 'completed'
current_time = datetime.datetime.now()
start_time = ausleihung.get('Start')
@@ -79,10 +83,6 @@ def get_current_status(ausleihung, log_changes=False, user=None):
# Wenn kein Startdatum vorhanden ist, Status auf 'planned' setzen
if not start_time:
new_status = 'planned'
# Wenn die Ausleihung als 'completed' markiert wurde und ein Enddatum hat,
# bleibt sie bei 'completed'
elif original_status == 'completed' and end_time:
new_status = 'completed'
# Wenn die aktuelle Zeit vor dem Startdatum liegt, ist die Ausleihung geplant
elif current_time < start_time:
# DEBUG: Log info wenn Booking noch lange in der Zukunft ist
+1
View File
@@ -0,0 +1 @@
[2026-04-19 20:20:39,502] ERROR in app: Error during scheduler shutdown: name '__file__' is not defined
+31 -2
View File
@@ -16,11 +16,39 @@ import settings as cfg
logger = logging.getLogger(__name__)
# VAPID keys for push notifications (should be in environment variables)
# VAPID keys for push notifications
VAPID_PUBLIC_KEY = os.getenv('VAPID_PUBLIC_KEY', '')
VAPID_PRIVATE_KEY = os.getenv('VAPID_PRIVATE_KEY', '')
VAPID_SUBJECT = os.getenv('VAPID_SUBJECT', f'mailto:admin@{os.getenv("SERVER_NAME", "localhost")}')
VAPID_PRIVATE_PEM = os.path.join(os.path.dirname(__file__), 'vapid_private.pem')
VAPID_PUBLIC_PEM = os.path.join(os.path.dirname(__file__), 'vapid_public.pem')
# Auto-generate VAPID keys if none are provided
if not VAPID_PUBLIC_KEY or not VAPID_PRIVATE_KEY:
try:
from py_vapid import Vapid, b64urlencode
from cryptography.hazmat.primitives import serialization
vapid = Vapid()
if not os.path.exists(VAPID_PRIVATE_PEM):
vapid.generate_keys()
vapid.save_key(VAPID_PRIVATE_PEM)
vapid.save_public_key(VAPID_PUBLIC_PEM)
logger.info("Auto-generated new VAPID keys")
else:
vapid = Vapid.from_file(VAPID_PRIVATE_PEM)
raw_pub = vapid.public_key.public_bytes(
serialization.Encoding.X962,
serialization.PublicFormat.UncompressedPoint
)
VAPID_PUBLIC_KEY = b64urlencode(raw_pub)
VAPID_PRIVATE_KEY = VAPID_PRIVATE_PEM
except Exception as e:
logger.error(f'Could not load or generate VAPID keys: {e}')
# Push service endpoint (typically Firebase or Web Push Service)
PUSH_SERVICE_URL = 'https://fcm.googleapis.com/fcm/send' # Firebase Cloud Messaging
FCM_API_KEY = os.getenv('FCM_API_KEY', '') # Firebase API key
@@ -296,7 +324,8 @@ def _send_web_push_notification(subscription, payload):
data=json.dumps(payload),
vapid_private_key=VAPID_PRIVATE_KEY,
vapid_claims={'sub': VAPID_SUBJECT},
timeout=10
timeout=10,
ttl=3600 # Notification expires after 1 hour if device is offline
)
return True
+2 -1
View File
@@ -13,4 +13,5 @@ redis
reportlab
python-barcode
openpyxl
cryptography
cryptography
pywebpush
Binary file not shown.

After

Width:  |  Height:  |  Size: 545 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

+9 -120
View File
@@ -1,134 +1,23 @@
{
"name": "Inventarsystem",
"short_name": "Inventar",
"description": "Verwaltungssystem für Schulinventare mit Ausleihmanagement",
"name": "Ausleihsystem",
"short_name": "Ausleihe",
"description": "System zur Verwaltung von Ausleihen",
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "portrait-primary",
"background_color": "#ffffff",
"theme_color": "#007bff",
"dir": "ltr",
"lang": "de-DE",
"prefer_related_applications": false,
"related_applications": [],
"theme_color": "#1f2937",
"icons": [
{
"src": "/static/img/logo-72x72.png",
"sizes": "72x72",
"src": "/static/img/icon-192.png",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static/img/logo-96x96.png",
"sizes": "96x96",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static/img/logo-128x128.png",
"sizes": "128x128",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static/img/logo-144x144.png",
"sizes": "144x144",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static/img/logo-152x152.png",
"sizes": "152x152",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static/img/logo-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
"purpose": "any maskable"
},
{
"src": "/static/img/logo-384x384.png",
"sizes": "384x384",
"src": "/static/img/icon-512.png",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static/img/logo-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/static/img/logo-192x192-maskable.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/static/img/logo-512x512-maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
"purpose": "any maskable"
}
],
"screenshots": [
{
"src": "/static/img/screenshot-540x720.png",
"sizes": "540x720",
"type": "image/png",
"form_factor": "narrow"
},
{
"src": "/static/img/screenshot-1280x720.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide"
}
],
"categories": ["education", "productivity"],
"shortcuts": [
{
"name": "Meine Ausleihungen",
"short_name": "Ausleihungen",
"description": "Zeige meine aktuellen Ausleihungen",
"url": "/my_borrowed_items",
"icons": [
{
"src": "/static/img/shortcut-96x96.png",
"sizes": "96x96"
}
]
},
{
"name": "Kalender",
"short_name": "Kalender",
"description": "Kalender für Ausleihplanungen",
"url": "/terminplan",
"icons": [
{
"src": "/static/img/shortcut-96x96.png",
"sizes": "96x96"
}
]
}
],
"share_target": {
"action": "/share",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"title": "title",
"text": "text",
"url": "url",
"files": [
{
"name": "media",
"accept": ["image/*", "video/*"]
}
]
}
}
]
}
+18
View File
@@ -0,0 +1,18 @@
const CACHE_NAME = 'ausleihe-cache-v1';
self.addEventListener('install', (event) => {
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil(clients.claim());
});
self.addEventListener('fetch', (event) => {
// Einfacher Fallback aufs Netzwerk (Offline-Seite könnte hier ergänzt werden)
event.respondWith(
fetch(event.request).catch(() => {
return caches.match(event.request);
})
);
});
+2 -22
View File
@@ -1754,27 +1754,7 @@
<script src="{{ url_for('static', filename='js/mobile_compatibility.js') }}"></script>
<script src="{{ url_for('static', filename='js/ios_fixes.js') }}"></script>
<!-- Service Worker Registration for Push Notifications -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Register service worker for push notifications and offline support
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('{{ url_for("static", filename="service-worker.js") }}')
.then(function(registration) {
console.log('Service Worker registered successfully:', registration);
// Check if we have permission for push notifications
if ('Notification' in window && Notification.permission === 'granted') {
console.log('Push notifications already enabled');
}
})
.catch(function(error) {
console.warn('Service Worker registration failed:', error);
});
} else {
console.log('Service Workers not supported in this browser');
}
});
</script>
<!-- Push Notifications Script -->
<script src="{{ url_for('static', filename='js/push-notifications.js') }}"></script>
</body>
</html>
+10 -4
View File
@@ -724,7 +724,9 @@
// Add this line to define allItems globally
let allItems = [];
const MAIN_ITEMS_PAGE_SIZE = 120;
// Dynamic page size: scan viewport/screen size to only load the exact amount needed.
const isMobileDevice = window.matchMedia('(max-width: 768px)').matches;
const MAIN_ITEMS_PAGE_SIZE = isMobileDevice ? 20 : 120;
let mainItemsNextOffset = 0;
let mainItemsHasMore = false;
let mainItemsLoadingMore = false;
@@ -781,10 +783,10 @@
styles.flexDirection === 'column';
const distanceToEnd = isMobileLayout
? itemsContainer.scrollHeight - (itemsContainer.scrollTop + itemsContainer.clientHeight)
? Math.max(document.body.scrollHeight, document.documentElement.scrollHeight) - (window.scrollY + window.innerHeight)
: itemsContainer.scrollWidth - (itemsContainer.scrollLeft + itemsContainer.clientWidth);
const prefetchThreshold = isMobileLayout
? Math.max(220, itemsContainer.clientHeight * 0.9)
? window.innerHeight * 1.5
: Math.max(260, itemsContainer.clientWidth * 1.4);
if (distanceToEnd > prefetchThreshold) {
return;
@@ -1167,6 +1169,7 @@
if (!mainItemsScrollPrefetchBound) {
itemsContainer.addEventListener('scroll', maybePrefetchMainItems, { passive: true });
window.addEventListener('scroll', maybePrefetchMainItems, { passive: true });
mainItemsScrollPrefetchBound = true;
}
@@ -1188,7 +1191,7 @@
updateMainItemsLoadingIndicator();
});
}, {
root: itemsContainer,
root: isMobileLayout ? null : itemsContainer,
threshold: 0.15,
rootMargin: isMobileLayout ? '0px 0px 900px 0px' : '0px 900px 0px 0px'
});
@@ -3561,6 +3564,9 @@
margin: 0 !important;
padding: 15px !important;
scroll-snap-align: none !important;
/* Only render when user looks at it (content-visibility virtualizes the DOM) */
content-visibility: auto;
contain-intrinsic-size: auto 300px;
}
.item-card:hover {
+10 -4
View File
@@ -1612,6 +1612,9 @@
margin: 0 !important;
padding: 15px !important;
scroll-snap-align: none !important;
/* Only render when user looks at it (content-visibility virtualizes the DOM) */
content-visibility: auto;
contain-intrinsic-size: auto 300px;
}
.item-card:hover {
@@ -3339,7 +3342,9 @@ document.addEventListener('DOMContentLoaded', ()=>{
});
// Function to load items from server
const MAIN_ADMIN_ITEMS_PAGE_SIZE = 120;
// Dynamic page size: scan viewport/screen size to only load the exact amount needed.
const isMobileDevice = window.matchMedia('(max-width: 768px)').matches;
const MAIN_ADMIN_ITEMS_PAGE_SIZE = isMobileDevice ? 20 : 120;
let mainAdminItemsNextOffset = 0;
let mainAdminItemsHasMore = false;
let mainAdminItemsLoadingMore = false;
@@ -3397,10 +3402,10 @@ document.addEventListener('DOMContentLoaded', ()=>{
styles.flexDirection === 'column';
const distanceToEnd = isMobileLayout
? itemsContainer.scrollHeight - (itemsContainer.scrollTop + itemsContainer.clientHeight)
? Math.max(document.body.scrollHeight, document.documentElement.scrollHeight) - (window.scrollY + window.innerHeight)
: itemsContainer.scrollWidth - (itemsContainer.scrollLeft + itemsContainer.clientWidth);
const prefetchThreshold = isMobileLayout
? Math.max(220, itemsContainer.clientHeight * 0.9)
? window.innerHeight * 1.5
: Math.max(260, itemsContainer.clientWidth * 1.4);
if (distanceToEnd > prefetchThreshold) {
return;
@@ -3772,6 +3777,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
if (!mainAdminItemsScrollPrefetchBound) {
itemsContainer.addEventListener('scroll', maybePrefetchMainAdminItems, { passive: true });
window.addEventListener('scroll', maybePrefetchMainAdminItems, { passive: true });
mainAdminItemsScrollPrefetchBound = true;
}
@@ -3793,7 +3799,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
updateMainAdminItemsLoadingIndicator();
});
}, {
root: itemsContainer,
root: isMobileLayout ? null : itemsContainer,
threshold: 0.15,
rootMargin: isMobileLayout ? '0px 0px 900px 0px' : '0px 900px 0px 0px'
});
+10
View File
@@ -14,6 +14,8 @@
</form>
</div>
<div id="push-notification-settings" style="margin-bottom: 24px;"></div>
<div style="display:grid; grid-template-columns:1fr; gap:14px;">
<section style="background:#fff; border:1px solid #e2e8f0; border-radius:14px; padding:16px;">
<h2 style="margin:0 0 12px; font-size:1.15rem;">Meine Benachrichtigungen</h2>
@@ -83,4 +85,12 @@
{% endif %}
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof showPushNotificationSettings === 'function') {
showPushNotificationSettings();
}
});
</script>
{% endblock %}
-40
View File
@@ -70,27 +70,6 @@
</div>
</div>
{% if student_cards_module_enabled %}
<div class="form-group">
<label style="display:flex; align-items:center; gap:8px; color:#1f2937;">
<input type="checkbox" id="is-student" name="is_student" style="width:auto;">
Schülerkonto mit Ausweis
</label>
<div id="student-fields" style="display:none; margin-top:10px;">
<label for="student-card-id">Ausweis-ID</label>
<div class="input-container">
<span class="input-icon">🪪</span>
<input type="text" id="student-card-id" name="student_card_id" placeholder="z.B. S-2026-001">
</div>
<label for="max-borrow-days" style="margin-top:10px;">Standard-Ausleihdauer (Tage)</label>
<div class="input-container">
<span class="input-icon">📅</span>
<input type="number" id="max-borrow-days" name="max_borrow_days" min="1" max="{{ student_max_borrow_days }}" value="{{ student_default_borrow_days }}">
</div>
</div>
</div>
{% endif %}
<div class="form-group">
<label for="permission-preset">Berechtigungs-Preset</label>
<select id="permission-preset" name="permission_preset" class="form-select">
@@ -561,25 +540,6 @@ document.addEventListener('DOMContentLoaded', function () {
passwordInput.addEventListener('blur', updatePasswordRules);
updatePasswordRules();
}
{% if student_cards_module_enabled %}
const studentCheckbox = document.getElementById('is-student');
const studentFields = document.getElementById('student-fields');
const studentCardInput = document.getElementById('student-card-id');
if (!studentCheckbox || !studentFields || !studentCardInput) {
return;
}
function toggleStudentFields() {
const isChecked = studentCheckbox.checked;
studentFields.style.display = isChecked ? 'block' : 'none';
studentCardInput.required = isChecked;
}
studentCheckbox.addEventListener('change', toggleStudentFields);
toggleStudentFields();
{% endif %}
});
</script>
{% endblock %}
+41 -3
View File
@@ -51,6 +51,8 @@
<label for="booking-type">Reservierungstyp:</label>
<select id="booking-type" name="booking_type">
<option value="single">Einzeltermin</option>
<option value="range">Zeitraum</option>
<option value="recurring">Wiederkehrend</option>
</select>
</div>
@@ -624,6 +626,7 @@ document.addEventListener('DOMContentLoaded', function() {
}
const userId = document.getElementById('user-id').value || 'Admin'; // Get user ID or use fallback
const bookingType = document.getElementById('booking-type').value;
// Convert JSON data to FormData
const formData = new FormData();
@@ -631,16 +634,43 @@ document.addEventListener('DOMContentLoaded', function() {
// Add all required fields with correct names the server expects
formData.append('item_id', itemId);
formData.append('booking_date', startDate);
formData.append('booking_end_date', startDate); // For single bookings, end = start
formData.append('period_start', periodStart);
formData.append('period_end', periodEnd);
formData.append('notes', document.getElementById('booking-notes').value || '');
formData.append('booking_type', document.getElementById('booking-type').value);
formData.append('user_id', userId);
if (bookingType === 'range') {
const endDate = document.getElementById('booking-end-date').value;
if (!endDate) {
alert('Bitte wählen Sie ein Enddatum für den Zeitraum.');
return;
}
formData.append('booking_end_date', endDate);
formData.append('booking_type', 'range');
} else if (bookingType === 'recurring') {
formData.append('booking_type', 'single'); // Server expects 'single' structure for these individually sent requests
const dates = calculateDates();
if (dates.length === 0) {
alert('Es konnten keine Termine berechnet werden. Bitte überprüfen Sie Ihre Eingaben.');
return;
}
if (confirm(`Möchten Sie wirklich ${dates.length} Termine planen?`)) {
processMultipleDates(formData, dates, periodStart, periodEnd);
}
return; // Use processMultipleDates instead
} else {
formData.append('booking_end_date', startDate); // For single bookings, end = start
formData.append('booking_type', 'single');
}
const csrfToken = document.querySelector('meta[name="csrf-token"]') ? document.querySelector('meta[name="csrf-token"]').content : '';
// Submit with FormData instead of JSON
fetch('/plan_booking', {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken
},
body: formData // Remove the Content-Type header and JSON.stringify
})
.then(response => {
@@ -681,8 +711,12 @@ document.addEventListener('DOMContentLoaded', function() {
// Cancel booking
document.getElementById('cancel-booking').addEventListener('click', function() {
if (confirm('Möchten Sie diese Ausleihe wirklich stornieren?')) {
const csrfToken = document.querySelector('meta[name="csrf-token"]') ? document.querySelector('meta[name="csrf-token"]').content : '';
fetch('/cancel_booking/' + currentEventId, {
method: 'POST'
method: 'POST',
headers: {
'X-CSRFToken': csrfToken
}
})
.then(response => response.json())
.then(data => {
@@ -1131,8 +1165,12 @@ document.addEventListener('DOMContentLoaded', function() {
newFormData.get('end_date'));
// Submit this booking
const csrfToken = document.querySelector('meta[name="csrf-token"]') ? document.querySelector('meta[name="csrf-token"]').content : '';
fetch('/plan_booking', {
method: 'POST',
headers: {
'X-CSRFToken': csrfToken
},
body: newFormData
})
.then(response => response.json())
+7
View File
@@ -0,0 +1,7 @@
from app import app
with app.test_client() as client:
with client.session_transaction() as sess:
sess['username'] = 'admin'
sess['admin'] = True
resp = client.get('/terminplan')
print("Status:", resp.status_code)
+1
View File
@@ -0,0 +1 @@
394722
-81
View File
@@ -1,81 +0,0 @@
#!/bin/bash
# VAPID Key Generation Script for Web Push Notifications
# Generates VAPID (Voluntary Application Server Identification) keys required for push notifications
echo "==========================================="
echo "VAPID Key Generation for Inventarsystem"
echo "==========================================="
echo ""
# Check if Python is available
if ! command -v python3 &> /dev/null; then
echo "❌ Error: Python 3 is required but not installed."
exit 1
fi
# Check if pywebpush is installed
echo "Checking for pywebpush..."
python3 -c "import pywebpush" 2>/dev/null
if [ $? -ne 0 ]; then
echo "Installing pywebpush..."
pip3 install pywebpush
if [ $? -ne 0 ]; then
echo "❌ Error: Failed to install pywebpush"
echo "Please install manually: pip3 install pywebpush"
exit 1
fi
fi
echo "✓ pywebpush is installed"
echo ""
# Generate VAPID keys
echo "Generating VAPID keys..."
python3 << 'EOF'
from pywebpush import generate_keys
try:
keys = generate_keys()
print("✓ VAPID Keys generated successfully!")
print("")
print("PUBLIC KEY (share with browsers):")
print(keys['public_key'])
print("")
print("PRIVATE KEY (keep secret!):")
print(keys['private_key'])
print("")
print("==========================================="
print("Add these to your environment variables:")
print("==========================================="
print("")
print("export VAPID_PUBLIC_KEY='" + keys['public_key'] + "'")
print("export VAPID_PRIVATE_KEY='" + keys['private_key'] + "'")
print("export VAPID_SUBJECT='mailto:admin@yourdomain.com'")
print("")
print("Or add to your .env file:")
print("")
print("VAPID_PUBLIC_KEY=" + keys['public_key'])
print("VAPID_PRIVATE_KEY=" + keys['private_key'])
print("VAPID_SUBJECT=mailto:admin@yourdomain.com")
print("")
print("⚠️ IMPORTANT: Keep the PRIVATE KEY secret!")
print("==========================================="
except Exception as e:
print(f"❌ Error generating VAPID keys: {e}")
exit(1)
EOF
if [ $? -eq 0 ]; then
echo ""
echo "✓ Next steps:"
echo "1. Copy the PUBLIC KEY to your browser-side code"
echo "2. Set the PRIVATE KEY in your server environment"
echo "3. Update config.json with your email address"
echo "4. Test with: curl http://localhost:5000/api/push/vapid-key"
else
echo "❌ Error generating keys"
exit 1
fi
+1 -1
View File
@@ -13,4 +13,4 @@ reportlab
python-barcode
openpyxl
cryptography
pywebpush
pywebpush
+5
View File
@@ -0,0 +1,5 @@
from Web.app import app
with app.test_client() as client:
resp = client.get('/terminplan')
print(resp.status_code)
# print(resp.data.decode('utf-8')[:200])