Compare commits
11 Commits
feat/job-c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| b28fadb684 | |||
| bf2d939efe | |||
| 352212c0e0 | |||
| 5ffbc2e3d3 | |||
| 88ff0131b7 | |||
| af8c442bd1 | |||
| b305c4563f | |||
| 57003532be | |||
| 32225127ab | |||
| 3a16705614 | |||
| 3a50bb5299 |
@@ -11,6 +11,7 @@ RUN npm install
|
|||||||
COPY . .
|
COPY . .
|
||||||
# Use a temporary DB for generation
|
# Use a temporary DB for generation
|
||||||
ENV DATABASE_URL="file:./temp.db"
|
ENV DATABASE_URL="file:./temp.db"
|
||||||
|
|
||||||
RUN npx prisma generate
|
RUN npx prisma generate
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
|||||||
36
README.md
36
README.md
@@ -61,6 +61,31 @@ Am einfachsten via **Intergram**:
|
|||||||
2. Sende `/start`, um deine **Chat ID** zu erhalten.
|
2. Sende `/start`, um deine **Chat ID** zu erhalten.
|
||||||
3. Webhook-URL: `https://www.intergram.xyz/msg/DEINE_CHAT_ID`
|
3. Webhook-URL: `https://www.intergram.xyz/msg/DEINE_CHAT_ID`
|
||||||
|
|
||||||
|
---
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📲 Push Notifications (PWA)
|
||||||
|
|
||||||
|
Die App funktioniert als **Progressive Web App (PWA)** und unterstützt **Push-Benachrichtigungen** direkt auf dem Smartphone oder Desktop.
|
||||||
|
|
||||||
|
### Setup für Selbsthoster (VAPID Keys)
|
||||||
|
Damit Push funktioniert, müssen VAPID Keys in der Umgebung hinterlegt werden.
|
||||||
|
1. Generiere Keys: `npx web-push generate-vapid-keys`
|
||||||
|
2. Setze die Environment Variables (z.B. in `.env` oder Docker):
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Public Key (Wird vom Browser benötigt)
|
||||||
|
NEXT_PUBLIC_VAPID_PUBLIC_KEY="<Dein Public Key>"
|
||||||
|
|
||||||
|
# Private Key (Bleibt auf dem Server!)
|
||||||
|
VAPID_PRIVATE_KEY="<Dein Private Key>"
|
||||||
|
|
||||||
|
# Kontakt-Email für den Push-Service
|
||||||
|
VAPID_SUBJECT="mailto:admin@example.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Benutzer kann Push-Benachrichtigungen dann direkt im Dashboard über die **Einstellungen** aktivieren.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🐳 Deployment (Docker)
|
## 🐳 Deployment (Docker)
|
||||||
@@ -97,6 +122,17 @@ volumes:
|
|||||||
- /pfad/zum/host/uploads:/app/public/uploads
|
- /pfad/zum/host/uploads:/app/public/uploads
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏥 System Status
|
||||||
|
|
||||||
|
Die App stellt einen einfachen Healthcheck-Endpoint bereit, der von Docker oder externen Monitoring-Tools genutzt werden kann:
|
||||||
|
|
||||||
|
- **Endpoint**: `/health`
|
||||||
|
- **Method**: `GET`
|
||||||
|
- **Response**: `200 OK` `{"status":"ok"}`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Erstellt mit ❤️ für alle Dosenöffner.*
|
*Erstellt mit ❤️ für alle Dosenöffner.*
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import { format, eachDayOfInterval, isSameDay } from "date-fns"
|
import { format, eachDayOfInterval, isSameDay } from "date-fns"
|
||||||
import { de, enUS } from "date-fns/locale"
|
import { de, enUS } from "date-fns/locale"
|
||||||
import { CalendarIcon, User, Home, X, Info, Utensils, Trash2, Check } from "lucide-react"
|
import { CalendarIcon, User, Home, X, Info, Utensils, Trash2, Check, Share2 } from "lucide-react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
@@ -220,6 +220,25 @@ export function PlanDashboard({ plan, dict, settingsDict, lang }: PlanDashboardP
|
|||||||
dict={settingsDict}
|
dict={settingsDict}
|
||||||
lang={lang}
|
lang={lang}
|
||||||
/>
|
/>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => {
|
||||||
|
if (navigator.share) {
|
||||||
|
navigator.share({
|
||||||
|
title: plan.title,
|
||||||
|
text: dict.shareTitle,
|
||||||
|
url: window.location.href,
|
||||||
|
}).catch(() => {
|
||||||
|
// Fallback if share fails / is cancelled
|
||||||
|
navigator.clipboard.writeText(window.location.href)
|
||||||
|
toast.success(dict.copySuccess)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
navigator.clipboard.writeText(window.location.href)
|
||||||
|
toast.success(dict.copySuccess)
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<Share2 className="w-4 h-4 mr-2" />
|
||||||
|
{dict.share}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -43,3 +43,7 @@ export async function unsubscribeUser(endpoint: string) {
|
|||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getVapidPublicKey() {
|
||||||
|
return process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY;
|
||||||
|
}
|
||||||
|
|||||||
5
app/health/route.ts
Normal file
5
app/health/route.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
return NextResponse.json({ status: 'ok' }, { status: 200 });
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import { Bell, BellOff, Loader2, AlertTriangle } from "lucide-react"
|
import { Bell, BellOff, Loader2, AlertTriangle } from "lucide-react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { subscribeUser, unsubscribeUser } from "@/app/actions/subscription"
|
import { subscribeUser, unsubscribeUser, getVapidPublicKey } from "@/app/actions/subscription"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
|
|
||||||
function urlBase64ToUint8Array(base64String: string) {
|
function urlBase64ToUint8Array(base64String: string) {
|
||||||
@@ -31,12 +31,24 @@ export function PushSubscriptionSettings({ planId }: { planId: string }) {
|
|||||||
const [subscription, setSubscription] = useState<PushSubscription | null>(null)
|
const [subscription, setSubscription] = useState<PushSubscription | null>(null)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [debugInfo, setDebugInfo] = useState<string | null>(null)
|
const [debugInfo, setDebugInfo] = useState<string | null>(null)
|
||||||
|
const [vapidKey, setVapidKey] = useState<string | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
async function checkSupport() {
|
||||||
const checks = []
|
const checks = []
|
||||||
if (!('serviceWorker' in navigator)) checks.push("No Service Worker support")
|
if (!('serviceWorker' in navigator)) checks.push("No Service Worker support")
|
||||||
if (!('PushManager' in window)) checks.push("No PushManager support")
|
if (!('PushManager' in window)) checks.push("No PushManager support")
|
||||||
if (!process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY) checks.push("Missing VAPID Key")
|
|
||||||
|
try {
|
||||||
|
const key = await getVapidPublicKey()
|
||||||
|
if (!key) {
|
||||||
|
checks.push("Missing VAPID Key (Server)")
|
||||||
|
} else {
|
||||||
|
setVapidKey(key)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
checks.push("Failed to fetch VAPID Key")
|
||||||
|
}
|
||||||
|
|
||||||
if (checks.length === 0) {
|
if (checks.length === 0) {
|
||||||
setIsSupported(true)
|
setIsSupported(true)
|
||||||
@@ -45,11 +57,18 @@ export function PushSubscriptionSettings({ planId }: { planId: string }) {
|
|||||||
console.warn("Push not supported:", checks.join(", "))
|
console.warn("Push not supported:", checks.join(", "))
|
||||||
setDebugInfo(checks.join(", "))
|
setDebugInfo(checks.join(", "))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
checkSupport()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
async function registerServiceWorker() {
|
async function registerServiceWorker() {
|
||||||
try {
|
try {
|
||||||
const registration = await navigator.serviceWorker.ready
|
// Explicitly register the service worker
|
||||||
|
const registration = await navigator.serviceWorker.register('/push-sw.js')
|
||||||
|
|
||||||
|
// Wait for it to be ready
|
||||||
|
await navigator.serviceWorker.ready
|
||||||
|
|
||||||
const sub = await registration.pushManager.getSubscription()
|
const sub = await registration.pushManager.getSubscription()
|
||||||
setSubscription(sub)
|
setSubscription(sub)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -59,6 +78,10 @@ export function PushSubscriptionSettings({ planId }: { planId: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function subscribe() {
|
async function subscribe() {
|
||||||
|
if (!vapidKey) {
|
||||||
|
toast.error("VAPID Key missing")
|
||||||
|
return
|
||||||
|
}
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setDebugInfo(null)
|
setDebugInfo(null)
|
||||||
try {
|
try {
|
||||||
@@ -84,7 +107,7 @@ export function PushSubscriptionSettings({ planId }: { planId: string }) {
|
|||||||
|
|
||||||
const sub = await registration.pushManager.subscribe({
|
const sub = await registration.pushManager.subscribe({
|
||||||
userVisibleOnly: true,
|
userVisibleOnly: true,
|
||||||
applicationServerKey: urlBase64ToUint8Array(process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!)
|
applicationServerKey: urlBase64ToUint8Array(vapidKey)
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log("Subscribed locally:", sub)
|
console.log("Subscribed locally:", sub)
|
||||||
|
|||||||
@@ -38,6 +38,10 @@
|
|||||||
"instructionsTitle": "Katzenpflege-Anleitungen",
|
"instructionsTitle": "Katzenpflege-Anleitungen",
|
||||||
"export": "Exportieren",
|
"export": "Exportieren",
|
||||||
"settings": "Einstellungen",
|
"settings": "Einstellungen",
|
||||||
|
"share": "Teilen",
|
||||||
|
"shareTitle": "Plan teilen",
|
||||||
|
"copySuccess": "Link kopiert!",
|
||||||
|
"shareError": "Fehler beim Teilen.",
|
||||||
"feeding": "Füttern",
|
"feeding": "Füttern",
|
||||||
"litter": "Klo reinigen",
|
"litter": "Klo reinigen",
|
||||||
"ownerHome": "Besitzer zu Hause",
|
"ownerHome": "Besitzer zu Hause",
|
||||||
|
|||||||
@@ -38,6 +38,10 @@
|
|||||||
"instructionsTitle": "Cat Care Instructions",
|
"instructionsTitle": "Cat Care Instructions",
|
||||||
"export": "Export",
|
"export": "Export",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
|
"share": "Share",
|
||||||
|
"shareTitle": "Share Plan",
|
||||||
|
"copySuccess": "Link copied to clipboard!",
|
||||||
|
"shareError": "Could not share plan.",
|
||||||
"feeding": "Feeding",
|
"feeding": "Feeding",
|
||||||
"litter": "Clean litter",
|
"litter": "Clean litter",
|
||||||
"ownerHome": "Owner Home",
|
"ownerHome": "Owner Home",
|
||||||
|
|||||||
@@ -9,3 +9,8 @@ services:
|
|||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
- ./public/uploads:/app/public/uploads
|
- ./public/uploads:/app/public/uploads
|
||||||
restart: always
|
restart: always
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health" ]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export function middleware(request: NextRequest) {
|
|||||||
|
|
||||||
if (pathnameHasLocale) return;
|
if (pathnameHasLocale) return;
|
||||||
|
|
||||||
|
if (pathname.startsWith("/api") || pathname.includes("sw.js") || pathname.includes("workbox")) return NextResponse.next();
|
||||||
|
|
||||||
// Redirect if there is no locale
|
// Redirect if there is no locale
|
||||||
const locale = defaultLocale; // For simplicity, we default to 'de' as requested
|
const locale = defaultLocale; // For simplicity, we default to 'de' as requested
|
||||||
request.nextUrl.pathname = `/${locale}${pathname}`;
|
request.nextUrl.pathname = `/${locale}${pathname}`;
|
||||||
@@ -25,6 +27,6 @@ export function middleware(request: NextRequest) {
|
|||||||
export const config = {
|
export const config = {
|
||||||
matcher: [
|
matcher: [
|
||||||
// Skip all internal paths (_next)
|
// Skip all internal paths (_next)
|
||||||
"/((?!_next|api|public|manifest|icon|file|globe|next|vercel|window).*)",
|
"/((?!_next|api|public|manifest|icon|file|globe|next|vercel|window|push-sw.js|sw.js|workbox|health).*)",
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user