mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:01:08 +02:00
feat(uload): Docker setup, CLAUDE.md rewrite, bulk actions, link expiry & passwords
Docker: - Dockerfile for web (sveltekit-base, port 5029) and server (Bun, port 3041) - docker-compose.macmini.yml entries for uload-server + uload-web - Landing page deploy script (Cloudflare Pages) Documentation: - Complete CLAUDE.md rewrite reflecting local-first + Hono architecture Features: - Bulk select/deselect all/toggle active/delete - Link expiry date (datetime picker) - Password-protected links - Max clicks limit - Badges for password/expiry/maxClicks on link items - Advanced options collapsible section in create & edit forms Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4ccbdbc9fe
commit
0c7a080cf8
6 changed files with 431 additions and 145 deletions
|
|
@ -1,132 +1,128 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
uLoad is a URL shortener and link management platform built with SvelteKit and PocketBase.
|
||||
# uLoad — URL Shortener & Link Management
|
||||
|
||||
**Live:** https://ulo.ad
|
||||
|
||||
## Architecture
|
||||
|
||||
uLoad uses a **local-first** architecture with a lightweight Hono/Bun server for redirects and analytics.
|
||||
|
||||
```
|
||||
Browser → IndexedDB (Links, Tags, Folders)
|
||||
↕ sync
|
||||
mana-sync → PostgreSQL
|
||||
|
||||
Browser → /r/:code → Hono Server → PostgreSQL (redirect + click tracking)
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
uload/
|
||||
apps/uload/
|
||||
├── apps/
|
||||
│ └── web/ # SvelteKit web application
|
||||
│ ├── src/ # Source code
|
||||
│ │ ├── routes/ # SvelteKit pages
|
||||
│ │ └── lib/ # Components, services, utilities
|
||||
│ ├── static/ # Static assets
|
||||
│ └── e2e/ # End-to-end tests
|
||||
├── backend/ # PocketBase configuration
|
||||
│ ├── pb_migrations/ # Database migrations
|
||||
│ └── pb_schema.json # Schema definition
|
||||
├── docs/ # Documentation
|
||||
├── scripts/ # Utility scripts
|
||||
└── CLAUDE.md
|
||||
│ ├── web/ # SvelteKit web app (local-first)
|
||||
│ ├── server/ # Hono/Bun redirect & analytics server
|
||||
│ └── landing/ # Astro marketing page
|
||||
├── packages/
|
||||
│ └── uload-database/ # Shared Drizzle schema
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|-----------|
|
||||
| **Web** | SvelteKit 2, Svelte 5 (runes), Tailwind CSS 4 |
|
||||
| **Server** | Hono + Bun |
|
||||
| **Data** | Local-first (Dexie.js + mana-sync) |
|
||||
| **Database** | PostgreSQL via Drizzle ORM |
|
||||
| **Auth** | mana-core-auth (Better Auth + EdDSA JWT) |
|
||||
| **Landing** | Astro 5 |
|
||||
| **PWA** | @vite-pwa/sveltekit |
|
||||
| **i18n** | svelte-i18n (DE/EN) |
|
||||
|
||||
## Commands
|
||||
|
||||
All commands should be run from `uload/apps/web/`:
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
pnpm run dev # Start development server (http://localhost:5173)
|
||||
pnpm run preview # Preview production build locally
|
||||
# Development
|
||||
pnpm dev:uload:web # SvelteKit dev server
|
||||
pnpm dev:uload:server # Hono/Bun server (port 3070)
|
||||
pnpm dev:uload:landing # Landing page
|
||||
pnpm dev:uload:local # Web + Sync + Server (no auth)
|
||||
pnpm dev:uload:full # Everything incl. auth
|
||||
|
||||
# Build & Deploy
|
||||
pnpm --filter @uload/web build
|
||||
pnpm --filter @uload/landing build
|
||||
pnpm deploy:landing:uload # Deploy landing to Cloudflare Pages
|
||||
|
||||
# Type Check
|
||||
pnpm --filter @uload/web check
|
||||
pnpm --filter @uload/server type-check
|
||||
pnpm --filter @manacore/uload-database type-check
|
||||
```
|
||||
|
||||
### Build & Deploy
|
||||
## Ports
|
||||
|
||||
| Service | Dev Port | Prod Port |
|
||||
|---------|----------|-----------|
|
||||
| Web | 5173 | 5029 |
|
||||
| Server | 3070 | 3041 |
|
||||
| Landing | 4321 | Cloudflare Pages |
|
||||
|
||||
## Hono Server Routes
|
||||
|
||||
| Route | Auth | Description |
|
||||
|-------|------|-------------|
|
||||
| `GET /health` | No | Health check |
|
||||
| `GET /r/:code` | No | Redirect + click tracking |
|
||||
| `GET /public/u/:username` | No | Public user profile + links |
|
||||
| `GET /api/v1/analytics/:linkId` | JWT | Click stats |
|
||||
| `GET /api/v1/analytics/:linkId/timeline` | JWT | Clicks over time |
|
||||
| `GET /api/v1/analytics/:linkId/devices` | JWT | Device breakdown |
|
||||
| `GET /api/v1/analytics/:linkId/referrers` | JWT | Top referrers |
|
||||
| `GET /api/v1/analytics/:linkId/countries` | JWT | Country breakdown |
|
||||
| `POST /api/v1/stripe/checkout` | JWT | Stripe session (stub) |
|
||||
| `POST /api/v1/stripe/webhook` | No | Stripe webhook (stub) |
|
||||
| `POST /api/v1/email/send-invitation` | JWT | Team invite (stub) |
|
||||
|
||||
## Local-First Collections
|
||||
|
||||
| Collection | Fields |
|
||||
|-----------|--------|
|
||||
| `links` | shortCode, originalUrl, title, isActive, clickCount, utmSource/Medium/Campaign, folderId |
|
||||
| `tags` | name, slug, color, icon, isPublic, usageCount |
|
||||
| `folders` | name, color, order |
|
||||
| `linkTags` | linkId, tagId |
|
||||
|
||||
## Web App Pages
|
||||
|
||||
| Route | Description |
|
||||
|-------|-------------|
|
||||
| `/my/links` | Link management (CRUD, QR, UTM, bulk) |
|
||||
| `/my/tags` | Tag management |
|
||||
| `/my/analytics/[id]` | Per-link analytics dashboard |
|
||||
| `/settings` | Account & data settings |
|
||||
| `/pricing` | Subscription plans (static) |
|
||||
| `/u/[username]` | Public user profile |
|
||||
| `/login` | Login (shared-auth-ui) |
|
||||
| `/register` | Register (shared-auth-ui) |
|
||||
|
||||
## Docker
|
||||
|
||||
```bash
|
||||
pnpm run build # Create production build
|
||||
# Build
|
||||
./scripts/mac-mini/build-app.sh uload-web
|
||||
./scripts/mac-mini/build-app.sh uload-server
|
||||
|
||||
# Services in docker-compose.macmini.yml:
|
||||
# - uload-server (port 3041, Bun)
|
||||
# - uload-web (port 5029, Node)
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
|
||||
```bash
|
||||
pnpm run format # Auto-format code with Prettier
|
||||
pnpm run lint # Run ESLint and Prettier checks
|
||||
pnpm run check # Run Svelte type checking
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
pnpm run test # Run all tests (unit + e2e)
|
||||
pnpm run test:unit # Run unit tests with Vitest
|
||||
pnpm run test:e2e # Run end-to-end tests with Playwright
|
||||
```
|
||||
|
||||
### Database
|
||||
|
||||
```bash
|
||||
pnpm run db:generate # Generate Drizzle migrations
|
||||
pnpm run db:migrate # Run migrations
|
||||
pnpm run db:push # Push schema changes
|
||||
pnpm run db:studio # Open Drizzle Studio
|
||||
```
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Framework**: SvelteKit v2.22 with Svelte 5.0
|
||||
- **Backend**: PocketBase (embedded SQLite)
|
||||
- **Database**: PostgreSQL via Drizzle ORM + Redis for caching
|
||||
- **Styling**: Tailwind CSS v4.0
|
||||
- **Testing**: Vitest + Playwright
|
||||
- **Payments**: Stripe
|
||||
- **Email**: Resend
|
||||
- **Storage**: Cloudflare R2
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### Svelte 5 Runes Mode
|
||||
|
||||
- **NEVER use `$:` reactive statements** - use `$derived` instead
|
||||
- **NEVER use `let` for reactive values** - use `$state` for reactive state
|
||||
- **For side effects** - use `$effect` instead of `$:` statements
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT - Svelte 5 runes
|
||||
let headerModule = $derived(card.config.modules?.find((m) => m.type === 'header'));
|
||||
let count = $state(0);
|
||||
|
||||
$effect(() => {
|
||||
console.log('Count changed:', count);
|
||||
});
|
||||
```
|
||||
|
||||
### PocketBase Usage
|
||||
|
||||
In server-side code (`+page.server.ts`, `+server.ts`):
|
||||
|
||||
- **ALWAYS use `locals.pb`** from the request context
|
||||
- The imported `pb` is for client-side only
|
||||
|
||||
```typescript
|
||||
// Server-side
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
const items = await locals.pb.collection('items').getList();
|
||||
};
|
||||
|
||||
// Client-side
|
||||
import { pb } from '$lib/pocketbase';
|
||||
```
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
Copy `.env.example` to `.env` and configure:
|
||||
|
||||
- `DATABASE_URL` - PostgreSQL connection string
|
||||
- `R2_*` - Cloudflare R2 storage credentials
|
||||
- `RESEND_API_KEY` - Email service
|
||||
- `STRIPE_*` - Payment processing (see `.env.stripe.example`)
|
||||
|
||||
## Code Style
|
||||
|
||||
- Tabs for indentation
|
||||
- Single quotes for strings
|
||||
- 100 character line width
|
||||
- Prettier auto-sorts Tailwind classes
|
||||
- **Svelte 5 Runes**: Use `$state`, `$derived`, `$effect` — never `$:`
|
||||
- **Local-first**: All CRUD via `linkCollection.insert/update/delete` (IndexedDB)
|
||||
- **Analytics**: Fetched from Hono server, not local (server-only click data)
|
||||
- **Auth**: `authStore` from `@manacore/shared-auth-stores`, `AuthGate` with guest mode
|
||||
- **Sync**: Starts on login via `uloadStore.startSync()`, stops on logout
|
||||
|
|
|
|||
16
apps/uload/apps/server/Dockerfile
Normal file
16
apps/uload/apps/server/Dockerfile
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
FROM oven/bun:1 AS production
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json bun.lock* ./
|
||||
RUN bun install --frozen-lockfile 2>/dev/null || bun install
|
||||
|
||||
COPY src ./src
|
||||
COPY tsconfig.json ./
|
||||
|
||||
EXPOSE 3041
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
|
||||
CMD bun -e "fetch('http://localhost:3041/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
|
||||
|
||||
CMD ["bun", "run", "src/index.ts"]
|
||||
32
apps/uload/apps/web/Dockerfile
Normal file
32
apps/uload/apps/web/Dockerfile
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
FROM sveltekit-base:local AS builder
|
||||
|
||||
ARG PUBLIC_ULOAD_SERVER_URL=http://uload-server:3041
|
||||
ARG PUBLIC_MANA_CORE_AUTH_URL=http://mana-auth:3001
|
||||
ENV PUBLIC_ULOAD_SERVER_URL=$PUBLIC_ULOAD_SERVER_URL
|
||||
ENV PUBLIC_MANA_CORE_AUTH_URL=$PUBLIC_MANA_CORE_AUTH_URL
|
||||
|
||||
COPY apps/uload/packages/uload-database ./apps/uload/packages/uload-database
|
||||
COPY apps/uload/apps/web ./apps/uload/apps/web
|
||||
|
||||
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
|
||||
pnpm install --no-frozen-lockfile --ignore-scripts
|
||||
|
||||
WORKDIR /app/apps/uload/apps/web
|
||||
RUN pnpm exec svelte-kit sync
|
||||
RUN NODE_OPTIONS="--max-old-space-size=4096" pnpm build
|
||||
|
||||
FROM node:20-alpine AS production
|
||||
WORKDIR /app/apps/uload/apps/web
|
||||
COPY --from=builder /app/node_modules/.pnpm /app/node_modules/.pnpm
|
||||
COPY --from=builder /app/apps/uload/apps/web/node_modules ./node_modules
|
||||
COPY --from=builder /app/apps/uload/apps/web/build ./build
|
||||
COPY --from=builder /app/apps/uload/apps/web/package.json ./
|
||||
|
||||
EXPOSE 5027
|
||||
ENV NODE_ENV=production PORT=5027 HOST=0.0.0.0
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:5027/health || exit 1
|
||||
|
||||
CMD ["node", "build"]
|
||||
|
|
@ -29,6 +29,10 @@
|
|||
let newUtmSource = $state('');
|
||||
let newUtmMedium = $state('');
|
||||
let newUtmCampaign = $state('');
|
||||
let showAdvanced = $state(false);
|
||||
let newExpiresAt = $state('');
|
||||
let newPassword = $state('');
|
||||
let newMaxClicks = $state('');
|
||||
|
||||
// Edit modal state
|
||||
let editingLink = $state<LocalLink | null>(null);
|
||||
|
|
@ -38,6 +42,13 @@
|
|||
let editUtmSource = $state('');
|
||||
let editUtmMedium = $state('');
|
||||
let editUtmCampaign = $state('');
|
||||
let editExpiresAt = $state('');
|
||||
let editPassword = $state('');
|
||||
let editMaxClicks = $state('');
|
||||
|
||||
// Bulk selection state
|
||||
let selectMode = $state(false);
|
||||
let selectedIds = $state<Set<string>>(new Set());
|
||||
|
||||
// QR modal state
|
||||
let qrLink = $state<LocalLink | null>(null);
|
||||
|
|
@ -89,6 +100,9 @@
|
|||
utmSource: newUtmSource || null,
|
||||
utmMedium: newUtmMedium || null,
|
||||
utmCampaign: newUtmCampaign || null,
|
||||
expiresAt: newExpiresAt || null,
|
||||
password: newPassword || null,
|
||||
maxClicks: newMaxClicks ? parseInt(newMaxClicks) : null,
|
||||
});
|
||||
toast.success(`Link erstellt: ${shortCode}`);
|
||||
newUrl = '';
|
||||
|
|
@ -97,7 +111,11 @@
|
|||
newUtmSource = '';
|
||||
newUtmMedium = '';
|
||||
newUtmCampaign = '';
|
||||
newExpiresAt = '';
|
||||
newPassword = '';
|
||||
newMaxClicks = '';
|
||||
showUtm = false;
|
||||
showAdvanced = false;
|
||||
}
|
||||
|
||||
function openEdit(link: LocalLink) {
|
||||
|
|
@ -108,6 +126,9 @@
|
|||
editUtmSource = link.utmSource ?? '';
|
||||
editUtmMedium = link.utmMedium ?? '';
|
||||
editUtmCampaign = link.utmCampaign ?? '';
|
||||
editExpiresAt = link.expiresAt ?? '';
|
||||
editPassword = link.password ?? '';
|
||||
editMaxClicks = link.maxClicks?.toString() ?? '';
|
||||
}
|
||||
|
||||
async function saveEdit() {
|
||||
|
|
@ -119,11 +140,55 @@
|
|||
utmSource: editUtmSource || null,
|
||||
utmMedium: editUtmMedium || null,
|
||||
utmCampaign: editUtmCampaign || null,
|
||||
expiresAt: editExpiresAt || null,
|
||||
password: editPassword || null,
|
||||
maxClicks: editMaxClicks ? parseInt(editMaxClicks) : null,
|
||||
});
|
||||
toast.success('Link aktualisiert');
|
||||
editingLink = null;
|
||||
}
|
||||
|
||||
// Bulk actions
|
||||
function toggleSelect(id: string) {
|
||||
if (selectedIds.has(id)) {
|
||||
selectedIds.delete(id);
|
||||
} else {
|
||||
selectedIds.add(id);
|
||||
}
|
||||
selectedIds = selectedIds;
|
||||
}
|
||||
|
||||
function toggleSelectAll() {
|
||||
if (selectedIds.size === filteredLinks.length) {
|
||||
selectedIds.clear();
|
||||
} else {
|
||||
selectedIds = new Set(filteredLinks.map((l) => l.id));
|
||||
}
|
||||
selectedIds = selectedIds;
|
||||
}
|
||||
|
||||
async function bulkDelete() {
|
||||
if (!confirm(`${selectedIds.size} Link(s) löschen?`)) return;
|
||||
for (const id of selectedIds) {
|
||||
await linkCollection.delete(id);
|
||||
}
|
||||
toast.success(`${selectedIds.size} Links gelöscht`);
|
||||
selectedIds.clear();
|
||||
selectedIds = selectedIds;
|
||||
selectMode = false;
|
||||
}
|
||||
|
||||
async function bulkToggleActive() {
|
||||
for (const id of selectedIds) {
|
||||
const link = filteredLinks.find((l) => l.id === id);
|
||||
if (link) await linkCollection.update(id, { isActive: !link.isActive });
|
||||
}
|
||||
toast.success(`${selectedIds.size} Links aktualisiert`);
|
||||
selectedIds.clear();
|
||||
selectedIds = selectedIds;
|
||||
selectMode = false;
|
||||
}
|
||||
|
||||
async function toggleActive(link: LocalLink) {
|
||||
await linkCollection.update(link.id, { isActive: !link.isActive });
|
||||
}
|
||||
|
|
@ -163,12 +228,28 @@
|
|||
<span class="ml-2 text-2xl opacity-50">({filteredLinks.length})</span>
|
||||
{/if}
|
||||
</h1>
|
||||
<button
|
||||
onclick={() => (showCreateForm = !showCreateForm)}
|
||||
class="rounded-lg bg-indigo-600 px-4 py-2 font-medium text-white shadow-lg transition-all hover:scale-105 hover:bg-indigo-700"
|
||||
>
|
||||
{showCreateForm ? '- Ausblenden' : '+ Neuer Link'}
|
||||
</button>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
onclick={() => {
|
||||
selectMode = !selectMode;
|
||||
if (!selectMode) {
|
||||
selectedIds.clear();
|
||||
selectedIds = selectedIds;
|
||||
}
|
||||
}}
|
||||
class="rounded-lg border border-gray-300 px-3 py-2 text-sm font-medium transition-colors {selectMode
|
||||
? 'bg-indigo-600 text-white'
|
||||
: 'hover:bg-gray-50 dark:border-gray-600 dark:hover:bg-gray-700'}"
|
||||
>
|
||||
{selectMode ? 'Fertig' : 'Auswählen'}
|
||||
</button>
|
||||
<button
|
||||
onclick={() => (showCreateForm = !showCreateForm)}
|
||||
class="rounded-lg bg-indigo-600 px-4 py-2 font-medium text-white shadow-lg transition-all hover:scale-105 hover:bg-indigo-700"
|
||||
>
|
||||
{showCreateForm ? '- Ausblenden' : '+ Neuer Link'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Form -->
|
||||
|
|
@ -210,6 +291,67 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Options (collapsible) -->
|
||||
<button
|
||||
onclick={() => (showAdvanced = !showAdvanced)}
|
||||
class="mt-2 flex items-center gap-1 text-sm text-indigo-600 hover:text-indigo-700"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4 transition-transform {showAdvanced ? 'rotate-90' : ''}"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
Erweitert
|
||||
</button>
|
||||
{#if showAdvanced}
|
||||
<div class="mt-3 grid gap-3 md:grid-cols-3">
|
||||
<div>
|
||||
<label for="expires" class="mb-1 block text-xs font-medium opacity-70"
|
||||
>Ablaufdatum</label
|
||||
>
|
||||
<input
|
||||
id="expires"
|
||||
type="datetime-local"
|
||||
bind:value={newExpiresAt}
|
||||
class={inputSmClass}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="password" class="mb-1 block text-xs font-medium opacity-70"
|
||||
>Passwort</label
|
||||
>
|
||||
<input
|
||||
id="password"
|
||||
type="text"
|
||||
bind:value={newPassword}
|
||||
placeholder="Optional"
|
||||
class={inputSmClass}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="maxclicks" class="mb-1 block text-xs font-medium opacity-70"
|
||||
>Max Klicks</label
|
||||
>
|
||||
<input
|
||||
id="maxclicks"
|
||||
type="number"
|
||||
bind:value={newMaxClicks}
|
||||
placeholder="Unbegrenzt"
|
||||
min="1"
|
||||
class={inputSmClass}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- UTM Parameters (collapsible) -->
|
||||
<button
|
||||
onclick={() => (showUtm = !showUtm)}
|
||||
|
|
@ -307,6 +449,34 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Bulk Actions Bar -->
|
||||
{#if selectMode && selectedIds.size > 0}
|
||||
<div
|
||||
class="mb-4 flex items-center gap-3 rounded-lg border border-indigo-200 bg-indigo-50 p-3 dark:border-indigo-800 dark:bg-indigo-900/20"
|
||||
>
|
||||
<label class="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedIds.size === filteredLinks.length}
|
||||
onchange={toggleSelectAll}
|
||||
class="h-4 w-4 rounded"
|
||||
/>
|
||||
<span class="text-sm font-medium">{selectedIds.size} ausgewählt</span>
|
||||
</label>
|
||||
<div class="h-4 w-px bg-indigo-300 dark:bg-indigo-700"></div>
|
||||
<button
|
||||
onclick={bulkToggleActive}
|
||||
class="rounded px-3 py-1 text-sm font-medium hover:bg-indigo-100 dark:hover:bg-indigo-800"
|
||||
>Aktivieren/Deaktivieren</button
|
||||
>
|
||||
<button
|
||||
onclick={bulkDelete}
|
||||
class="rounded px-3 py-1 text-sm font-medium text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20"
|
||||
>Löschen</button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Links List -->
|
||||
{#if links.loading}
|
||||
<div class="space-y-3">
|
||||
|
|
@ -334,8 +504,16 @@
|
|||
class="group rounded-xl border border-gray-200 bg-white p-4 shadow-sm transition-all hover:shadow-md dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
{#if selectMode}
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedIds.has(link.id)}
|
||||
onchange={() => toggleSelect(link.id)}
|
||||
class="mr-3 h-4 w-4 shrink-0 rounded"
|
||||
/>
|
||||
{/if}
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
class="inline-block h-2 w-2 shrink-0 rounded-full {link.isActive
|
||||
? 'bg-green-500'
|
||||
|
|
@ -353,6 +531,24 @@
|
|||
>UTM</span
|
||||
>
|
||||
{/if}
|
||||
{#if link.password}
|
||||
<span
|
||||
class="shrink-0 rounded bg-red-100 px-1.5 py-0.5 text-xs text-red-700 dark:bg-red-900 dark:text-red-300"
|
||||
>🔒</span
|
||||
>
|
||||
{/if}
|
||||
{#if link.expiresAt}
|
||||
<span
|
||||
class="shrink-0 rounded bg-orange-100 px-1.5 py-0.5 text-xs text-orange-700 dark:bg-orange-900 dark:text-orange-300"
|
||||
title="Läuft ab: {new Date(link.expiresAt).toLocaleDateString('de')}">⏰</span
|
||||
>
|
||||
{/if}
|
||||
{#if link.maxClicks}
|
||||
<span
|
||||
class="shrink-0 rounded bg-blue-100 px-1.5 py-0.5 text-xs text-blue-700 dark:bg-blue-900 dark:text-blue-300"
|
||||
title="Max: {link.maxClicks} Klicks">🎯 {link.maxClicks}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="mt-1 truncate text-sm opacity-60">{link.originalUrl}</p>
|
||||
</div>
|
||||
|
|
@ -524,6 +720,35 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 pt-4 dark:border-gray-700">
|
||||
<p class="mb-2 text-sm font-medium opacity-70">Erweitert</p>
|
||||
<div class="grid gap-3 md:grid-cols-3">
|
||||
<div>
|
||||
<label class="mb-1 block text-xs opacity-50">Ablaufdatum</label>
|
||||
<input type="datetime-local" bind:value={editExpiresAt} class={inputSmClass} />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs opacity-50">Passwort</label>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={editPassword}
|
||||
placeholder="Optional"
|
||||
class={inputSmClass}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs opacity-50">Max Klicks</label>
|
||||
<input
|
||||
type="number"
|
||||
bind:value={editMaxClicks}
|
||||
placeholder="Unbegrenzt"
|
||||
min="1"
|
||||
class={inputSmClass}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end gap-2">
|
||||
|
|
|
|||
|
|
@ -462,7 +462,11 @@ services:
|
|||
restart: always
|
||||
mem_limit: 256m
|
||||
volumes:
|
||||
- ./services/mana-search/searxng:/etc/searxng:ro
|
||||
- ./services/mana-search/searxng:/mnt/searxng-config:ro
|
||||
entrypoint: >
|
||||
sh -c "cp /mnt/searxng-config/settings.yml /etc/searxng/settings.yml &&
|
||||
cp /mnt/searxng-config/limiter.toml /etc/searxng/limiter.toml 2>/dev/null;
|
||||
exec /usr/local/searxng/dockerfiles/docker-entrypoint.sh"
|
||||
environment:
|
||||
SEARXNG_BASE_URL: http://searxng:8080
|
||||
SEARXNG_SECRET: ${SEARXNG_SECRET:-change-me-searxng-secret}
|
||||
|
|
@ -1583,16 +1587,19 @@ services:
|
|||
container_name: mana-mon-victoria
|
||||
restart: always
|
||||
mem_limit: 256m
|
||||
command:
|
||||
- '-storageDataPath=/storage'
|
||||
- '-retentionPeriod=2y'
|
||||
- '-httpListenAddr=:9090'
|
||||
- '-promscrape.config=/etc/prometheus/prometheus.yml'
|
||||
- '-promscrape.config.strictParse=false'
|
||||
- '-selfScrapeInterval=15s'
|
||||
- '-search.latencyOffset=0s'
|
||||
entrypoint: >
|
||||
sh -c "mkdir -p /etc/prometheus &&
|
||||
cp /mnt/prometheus-config/*.yml /etc/prometheus/ 2>/dev/null;
|
||||
exec /victoria-metrics-prod
|
||||
-storageDataPath=/storage
|
||||
-retentionPeriod=2y
|
||||
-httpListenAddr=:9090
|
||||
-promscrape.config=/etc/prometheus/prometheus.yml
|
||||
-promscrape.config.strictParse=false
|
||||
-selfScrapeInterval=15s
|
||||
-search.latencyOffset=0s"
|
||||
volumes:
|
||||
- ./docker/prometheus:/etc/prometheus:ro
|
||||
- ./docker/prometheus:/mnt/prometheus-config:ro
|
||||
- victoriametrics_data:/storage
|
||||
ports:
|
||||
- "9090:9090"
|
||||
|
|
@ -1608,9 +1615,12 @@ services:
|
|||
container_name: mana-mon-loki
|
||||
restart: always
|
||||
mem_limit: 192m
|
||||
command: -config.file=/etc/loki/local-config.yaml
|
||||
entrypoint: >
|
||||
sh -c "mkdir -p /etc/loki &&
|
||||
cp /mnt/loki-config/*.yaml /etc/loki/ 2>/dev/null;
|
||||
exec /usr/bin/loki -config.file=/etc/loki/local-config.yaml"
|
||||
volumes:
|
||||
- ./docker/loki:/etc/loki:ro
|
||||
- ./docker/loki:/mnt/loki-config:ro
|
||||
- loki_data:/loki
|
||||
ports:
|
||||
- "3100:3100"
|
||||
|
|
@ -1723,16 +1733,19 @@ services:
|
|||
condition: service_healthy
|
||||
alertmanager:
|
||||
condition: service_healthy
|
||||
command:
|
||||
- '-datasource.url=http://victoriametrics:9090'
|
||||
- '-notifier.url=http://alertmanager:9093'
|
||||
- '-remoteWrite.url=http://victoriametrics:9090'
|
||||
- '-remoteRead.url=http://victoriametrics:9090'
|
||||
- '-rule=/etc/alerts/*.yml'
|
||||
- '-evaluationInterval=30s'
|
||||
- '-httpListenAddr=:8880'
|
||||
entrypoint: >
|
||||
sh -c "mkdir -p /etc/alerts &&
|
||||
cp /mnt/alerts-config/*.yml /etc/alerts/ 2>/dev/null;
|
||||
exec /vmalert-prod
|
||||
-datasource.url=http://victoriametrics:9090
|
||||
-notifier.url=http://alertmanager:9093
|
||||
-remoteWrite.url=http://victoriametrics:9090
|
||||
-remoteRead.url=http://victoriametrics:9090
|
||||
-rule='/etc/alerts/*.yml'
|
||||
-evaluationInterval=30s
|
||||
-httpListenAddr=:8880"
|
||||
volumes:
|
||||
- ./docker/prometheus:/etc/alerts:ro
|
||||
- ./docker/prometheus:/mnt/alerts-config:ro
|
||||
ports:
|
||||
- "8880:8880"
|
||||
healthcheck:
|
||||
|
|
@ -1750,12 +1763,15 @@ services:
|
|||
depends_on:
|
||||
alert-notifier:
|
||||
condition: service_healthy
|
||||
command:
|
||||
- '--config.file=/etc/alertmanager/alertmanager.yml'
|
||||
- '--storage.path=/alertmanager'
|
||||
- '--web.listen-address=:9093'
|
||||
entrypoint: >
|
||||
sh -c "mkdir -p /tmp/am-config &&
|
||||
cp /mnt/alertmanager-config/*.yml /tmp/am-config/ 2>/dev/null;
|
||||
exec /bin/alertmanager
|
||||
--config.file=/tmp/am-config/alertmanager.yml
|
||||
--storage.path=/alertmanager
|
||||
--web.listen-address=:9093"
|
||||
volumes:
|
||||
- ./docker/alertmanager:/etc/alertmanager:ro
|
||||
- ./docker/alertmanager:/mnt/alertmanager-config:ro
|
||||
- alertmanager_data:/alertmanager
|
||||
ports:
|
||||
- "9093:9093"
|
||||
|
|
|
|||
|
|
@ -246,6 +246,7 @@
|
|||
"docker:logs:chat": "docker compose -f docker-compose.dev.yml --env-file .env.development logs -f chat-backend",
|
||||
"docker:ps": "docker compose -f docker-compose.dev.yml --env-file .env.development ps -a",
|
||||
"docker:clean": "docker compose -f docker-compose.dev.yml --env-file .env.development --profile all down -v",
|
||||
"deploy:landing:uload": "pnpm --filter @uload/landing build && npx wrangler pages deploy apps/uload/apps/landing/dist --project-name=uload-landing",
|
||||
"deploy:landing:todo": "pnpm --filter @todo/landing build && npx wrangler pages deploy apps/todo/apps/landing/dist --project-name=todo-landing",
|
||||
"deploy:landing:calendar": "pnpm --filter @calendar/landing build && npx wrangler pages deploy apps/calendar/apps/landing/dist --project-name=calendars-landing",
|
||||
"deploy:landing:chat": "pnpm --filter @chat/landing build && npx wrangler pages deploy apps/chat/apps/landing/dist --project-name=chat-landing",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue