mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-27 10:57:43 +02:00
fix(infra): remove n8n and increase health check intervals to fix port exhaustion
Mac Mini had 25k+ TIME_WAIT sockets exhausting the 16k ephemeral port range, blocking all outgoing TCP connections. Root cause: ~50 health checks at 30s intervals + n8n automation creating excessive short-lived connections. - Remove n8n service and volume (no longer needed) - Increase health check intervals: 30s → 120s (app services), 10s → 30s (infra) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
490f8220dd
commit
6cab9a3c24
9 changed files with 237 additions and 129 deletions
|
|
@ -6,6 +6,7 @@ export interface LookupResult {
|
|||
description: string;
|
||||
address?: string;
|
||||
category?: string;
|
||||
imageUrl?: string;
|
||||
sources: { url: string; title: string }[];
|
||||
}
|
||||
|
||||
|
|
@ -82,11 +83,15 @@ export class LocationLookupService {
|
|||
// Build a description from the best snippet or extracted text
|
||||
const description = this.buildDescription(snippets, extractedTexts);
|
||||
|
||||
// Try to find an image URL from search results
|
||||
const imageUrl = this.extractImageUrl(results);
|
||||
|
||||
return {
|
||||
name: query,
|
||||
description,
|
||||
address,
|
||||
category,
|
||||
imageUrl,
|
||||
sources: results.slice(0, 5).map((r: any) => ({
|
||||
url: r.url,
|
||||
title: r.title,
|
||||
|
|
@ -140,6 +145,28 @@ export class LocationLookupService {
|
|||
return 'sight';
|
||||
}
|
||||
|
||||
private extractImageUrl(results: any[]): string | undefined {
|
||||
for (const result of results) {
|
||||
// SearXNG results may include img_src or thumbnail
|
||||
if (result.img_src && this.isValidImageUrl(result.img_src)) {
|
||||
return result.img_src;
|
||||
}
|
||||
if (result.thumbnail && this.isValidImageUrl(result.thumbnail)) {
|
||||
return result.thumbnail;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private isValidImageUrl(url: string): boolean {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
return parsed.protocol === 'https:' && /\.(jpg|jpeg|png|webp)/i.test(parsed.pathname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private buildDescription(snippets: string[], extractedTexts: string[]): string {
|
||||
// Prefer extracted text (more detailed)
|
||||
if (extractedTexts.length > 0) {
|
||||
|
|
|
|||
|
|
@ -96,7 +96,13 @@
|
|||
"submit": "Ort einreichen",
|
||||
"submitting": "Wird eingereicht...",
|
||||
"loginRequired": "Melde dich an, um Orte hinzuzufügen.",
|
||||
"error": "Fehler beim Einreichen. Bitte versuche es erneut."
|
||||
"error": "Fehler beim Einreichen. Bitte versuche es erneut.",
|
||||
"imageUrl": "Bild-URL (optional)",
|
||||
"imageUrlPlaceholder": "https://example.com/bild.jpg",
|
||||
"imagePreview": "Bildvorschau",
|
||||
"imageLoadError": "Bild konnte nicht geladen werden.",
|
||||
"geocoding": "Koordinaten werden ermittelt...",
|
||||
"coordinatesFound": "Koordinaten gefunden"
|
||||
},
|
||||
"offline": {
|
||||
"title": "Keine Verbindung",
|
||||
|
|
|
|||
|
|
@ -96,7 +96,13 @@
|
|||
"submit": "Submit place",
|
||||
"submitting": "Submitting...",
|
||||
"loginRequired": "Sign in to add places.",
|
||||
"error": "Failed to submit. Please try again."
|
||||
"error": "Failed to submit. Please try again.",
|
||||
"imageUrl": "Image URL (optional)",
|
||||
"imageUrlPlaceholder": "https://example.com/image.jpg",
|
||||
"imagePreview": "Image preview",
|
||||
"imageLoadError": "Image could not be loaded.",
|
||||
"geocoding": "Finding coordinates...",
|
||||
"coordinatesFound": "Coordinates found"
|
||||
},
|
||||
"offline": {
|
||||
"title": "No connection",
|
||||
|
|
|
|||
|
|
@ -53,9 +53,20 @@
|
|||
<title>{$_('app.name')} - {$_('app.tagline')}</title>
|
||||
</svelte:head>
|
||||
|
||||
<header class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-foreground">{$_('home.title')}</h1>
|
||||
<p class="text-foreground-secondary">{$_('home.subtitle')}</p>
|
||||
<header class="mb-6 flex items-start justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-foreground">{$_('home.title')}</h1>
|
||||
<p class="text-foreground-secondary">{$_('home.subtitle')}</p>
|
||||
</div>
|
||||
<a
|
||||
href="/add"
|
||||
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-primary text-white shadow-md transition-all hover:bg-primary/90 hover:shadow-lg"
|
||||
title={$_('add.title')}
|
||||
>
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
</a>
|
||||
</header>
|
||||
|
||||
<div class="mb-6 flex flex-wrap gap-2">
|
||||
|
|
|
|||
|
|
@ -15,8 +15,13 @@
|
|||
let category = $state<string>('sight');
|
||||
let description = $state('');
|
||||
let address = $state('');
|
||||
let imageUrl = $state('');
|
||||
let latitude = $state<number | undefined>(undefined);
|
||||
let longitude = $state<number | undefined>(undefined);
|
||||
let submitting = $state(false);
|
||||
let error = $state('');
|
||||
let geocoding = $state(false);
|
||||
let imageError = $state(false);
|
||||
|
||||
const categories = [
|
||||
{ value: 'sight', labelKey: 'category.sight' },
|
||||
|
|
@ -46,13 +51,25 @@
|
|||
address = data.result.address || '';
|
||||
category = data.result.category || 'sight';
|
||||
sources = data.result.sources || [];
|
||||
if (data.result.imageUrl) {
|
||||
imageUrl = data.result.imageUrl;
|
||||
imageError = false;
|
||||
}
|
||||
if (data.result.latitude && data.result.longitude) {
|
||||
latitude = data.result.latitude;
|
||||
longitude = data.result.longitude;
|
||||
}
|
||||
} else {
|
||||
name = searchQuery.trim();
|
||||
}
|
||||
|
||||
lookupDone = true;
|
||||
|
||||
// Auto-geocode if we got an address but no coordinates
|
||||
if (address && !latitude) {
|
||||
geocodeAddress();
|
||||
}
|
||||
} catch {
|
||||
// Fallback: just use the search query as name
|
||||
name = searchQuery.trim();
|
||||
lookupDone = true;
|
||||
} finally {
|
||||
|
|
@ -71,9 +88,46 @@
|
|||
name = '';
|
||||
description = '';
|
||||
address = '';
|
||||
imageUrl = '';
|
||||
category = 'sight';
|
||||
latitude = undefined;
|
||||
longitude = undefined;
|
||||
sources = [];
|
||||
error = '';
|
||||
imageError = false;
|
||||
}
|
||||
|
||||
async function geocodeAddress() {
|
||||
const addr = address.trim();
|
||||
if (!addr) return;
|
||||
|
||||
geocoding = true;
|
||||
try {
|
||||
const q = addr.includes('Konstanz') ? addr : `${addr}, Konstanz`;
|
||||
const res = await fetch(
|
||||
`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(q)}&limit=1`,
|
||||
{ headers: { 'User-Agent': 'CityCorners/1.0' } }
|
||||
);
|
||||
const results = await res.json();
|
||||
if (results.length > 0) {
|
||||
latitude = parseFloat(results[0].lat);
|
||||
longitude = parseFloat(results[0].lon);
|
||||
}
|
||||
} catch {
|
||||
// Geocoding is best-effort
|
||||
} finally {
|
||||
geocoding = false;
|
||||
}
|
||||
}
|
||||
|
||||
let geocodeTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||
function handleAddressInput() {
|
||||
clearTimeout(geocodeTimeout);
|
||||
geocodeTimeout = setTimeout(() => {
|
||||
if (address.trim().length > 5) {
|
||||
geocodeAddress();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
|
|
@ -89,18 +143,25 @@
|
|||
return;
|
||||
}
|
||||
|
||||
const body: Record<string, unknown> = {
|
||||
name: name.trim(),
|
||||
category,
|
||||
description: description.trim(),
|
||||
};
|
||||
if (address.trim()) body.address = address.trim();
|
||||
if (imageUrl.trim() && !imageError) body.imageUrl = imageUrl.trim();
|
||||
if (latitude !== undefined && longitude !== undefined) {
|
||||
body.latitude = latitude;
|
||||
body.longitude = longitude;
|
||||
}
|
||||
|
||||
const res = await fetch(api('/locations'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: name.trim(),
|
||||
category,
|
||||
description: description.trim(),
|
||||
address: address.trim() || undefined,
|
||||
}),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
|
|
@ -261,9 +322,42 @@
|
|||
id="address"
|
||||
type="text"
|
||||
bind:value={address}
|
||||
oninput={handleAddressInput}
|
||||
placeholder={$_('add.addressPlaceholder')}
|
||||
class="w-full rounded-lg border border-border bg-background px-4 py-2.5 text-foreground placeholder:text-foreground-secondary/50 focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
/>
|
||||
{#if geocoding}
|
||||
<p class="mt-1 text-xs text-foreground-secondary/60">{$_('add.geocoding')}</p>
|
||||
{:else if latitude !== undefined && longitude !== undefined}
|
||||
<p class="mt-1 text-xs text-green-600 dark:text-green-400">{$_('add.coordinatesFound')}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Image URL -->
|
||||
<div>
|
||||
<label for="imageUrl" class="mb-1 block text-sm font-medium text-foreground"
|
||||
>{$_('add.imageUrl')}</label
|
||||
>
|
||||
<input
|
||||
id="imageUrl"
|
||||
type="url"
|
||||
bind:value={imageUrl}
|
||||
oninput={() => (imageError = false)}
|
||||
placeholder={$_('add.imageUrlPlaceholder')}
|
||||
class="w-full rounded-lg border border-border bg-background px-4 py-2.5 text-foreground placeholder:text-foreground-secondary/50 focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
/>
|
||||
{#if imageUrl.trim() && !imageError}
|
||||
<div class="mt-2 overflow-hidden rounded-lg border border-border">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={$_('add.imagePreview')}
|
||||
class="h-40 w-full object-cover"
|
||||
onerror={() => (imageError = true)}
|
||||
/>
|
||||
</div>
|
||||
{:else if imageError}
|
||||
<p class="mt-1 text-xs text-red-500">{$_('add.imageLoadError')}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import { de } from 'date-fns/locale';
|
||||
import { Sparkle, ArrowDown } from '@manacore/shared-icons';
|
||||
import { tasksStore } from '$lib/stores/tasks.svelte';
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { viewStore } from '$lib/stores/view.svelte';
|
||||
import { applyTaskFilters } from '$lib/utils/task-filters';
|
||||
import TaskList from '$lib/components/TaskList.svelte';
|
||||
|
|
@ -29,6 +30,11 @@
|
|||
onMount(async () => {
|
||||
viewStore.setToday();
|
||||
|
||||
// Wait for auth to be initialized (layout onMount runs after page onMount in Svelte)
|
||||
while (!authStore.initialized) {
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch tasks (works in both guest and authenticated mode)
|
||||
await tasksStore.fetchAllTasks();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue