feat(notify-bridge): phase 1 - project scaffolding
Set up the Notify Bridge project structure: - packages/core (notify_bridge_core) with provider, model, notification, template packages - packages/server (notify_bridge_server) with FastAPI skeleton and health endpoint - frontend with SvelteKit 2, Svelte 5, Tailwind CSS v4, static adapter - Root configs: .gitignore, README.md, CLAUDE.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+54
@@ -0,0 +1,54 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
# Claude Code
|
||||||
|
.claude/
|
||||||
|
|
||||||
|
# Data
|
||||||
|
test-data/
|
||||||
|
|
||||||
|
# Node
|
||||||
|
node_modules/
|
||||||
|
frontend/build/
|
||||||
|
frontend/.svelte-kit/
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Project Guidelines
|
||||||
|
|
||||||
|
## Development Servers
|
||||||
|
|
||||||
|
**IMPORTANT**: When the user requests it OR when backend code changes are made (files in `packages/server/`), you MUST restart the standalone server using this one-liner:
|
||||||
|
```bash
|
||||||
|
PID=$(netstat -ano 2>/dev/null | grep ':8420.*LISTENING' | awk '{print $5}' | head -1) && [ -n "$PID" ] && taskkill //F //PID $PID 2>/dev/null; sleep 1 && cd packages/server && pip install -e . 2>&1 | tail -1 && cd ../.. && NOTIFY_BRIDGE_DATA_DIR=./test-data NOTIFY_BRIDGE_SECRET_KEY=test-secret-key-minimum-32chars nohup python -m uvicorn notify_bridge_server.main:app --host 0.0.0.0 --port 8420 > /dev/null 2>&1 & sleep 3 && curl -s http://localhost:8420/api/health
|
||||||
|
```
|
||||||
|
|
||||||
|
**IMPORTANT**: Overlays (modals, dropdowns, pickers) MUST use `position: fixed` with inline styles and `z-index: 9999`. Tailwind CSS v4 `fixed`/`absolute` classes do NOT work reliably inside flex/overflow containers in this project. Always calculate position from `getBoundingClientRect()` for dropdowns, or use `top:0;left:0;right:0;bottom:0` for full-screen backdrops.
|
||||||
|
|
||||||
|
**IMPORTANT**: When the user requests it, restart the frontend dev server using this one-liner:
|
||||||
|
```bash
|
||||||
|
PID=$(netstat -ano 2>/dev/null | grep ':5173.*LISTENING' | awk '{print $5}' | head -1) && [ -n "$PID" ] && taskkill //F //PID $PID 2>/dev/null; sleep 1 && cd frontend && npx vite dev --port 5173 --host > /dev/null 2>&1 & sleep 4 && curl -s -o /dev/null -w "Frontend: %{http_code}" http://localhost:5173/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frontend Architecture Notes
|
||||||
|
|
||||||
|
- **i18n**: Uses `$state` rune in `.svelte.ts` file. Locale auto-detects from localStorage. `t()` is reactive via `$state`. `setLocale()` updates immediately without page reload.
|
||||||
|
- **Svelte 5 runes**: `$state` only works in `.svelte` and `.svelte.ts` files. Regular `.ts` files cannot use runes -- use plain variables instead.
|
||||||
|
- **Static adapter**: Frontend uses `@sveltejs/adapter-static` with SPA fallback. API calls proxied via Vite dev server config.
|
||||||
|
- **Auth flow**: After login/setup, use `window.location.href = '/'` (hard redirect), NOT `goto('/')`.
|
||||||
|
- **Tailwind CSS v4**: Uses `@theme` directive in `app.css` for CSS variables.
|
||||||
|
|
||||||
|
## Backend Architecture Notes
|
||||||
|
|
||||||
|
- **SQLAlchemy async + aiohttp**: Cannot nest `async with aiohttp.ClientSession()` inside a route that has an active SQLAlchemy async session -- greenlet context breaks. Eagerly load all DB data before entering aiohttp context.
|
||||||
|
- **Jinja2 SandboxedEnvironment**: All template rendering MUST use `from jinja2.sandbox import SandboxedEnvironment`.
|
||||||
|
- **System-owned entities**: `user_id=0` means system-owned (e.g. default templates).
|
||||||
|
- **FastAPI route ordering**: Static path routes MUST be registered BEFORE parameterized routes.
|
||||||
|
- **`__pycache__`**: Add to `.gitignore`. Never commit.
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Notify Bridge
|
||||||
|
|
||||||
|
A generic bridge between service providers and notification targets.
|
||||||
|
|
||||||
|
Notify Bridge monitors services (like Immich photo servers) for changes and dispatches
|
||||||
|
notifications to configurable targets (Telegram, webhooks) using customizable templates.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- **Service Providers** — Connectors to external services (Immich, more coming)
|
||||||
|
- **Trackers** — Monitor specific collections within a provider for changes
|
||||||
|
- **Tracking Configs** — Define what events to watch for and scheduling rules
|
||||||
|
- **Notification Targets** — Where to send notifications (Telegram chats, webhook URLs)
|
||||||
|
- **Template Configs** — Jinja2 templates that format notifications per provider type
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
packages/
|
||||||
|
core/ — Shared library: providers, models, notifications, templates
|
||||||
|
server/ — FastAPI REST server with SQLite database
|
||||||
|
frontend/ — SvelteKit dashboard (Svelte 5, Tailwind CSS v4)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backend
|
||||||
|
cd packages/server
|
||||||
|
pip install -e .
|
||||||
|
NOTIFY_BRIDGE_DATA_DIR=./test-data NOTIFY_BRIDGE_SECRET_KEY=your-secret-key-min-32chars \
|
||||||
|
python -m uvicorn notify_bridge_server.main:app --host 0.0.0.0 --port 8420
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Providers
|
||||||
|
|
||||||
|
- **Immich** — Photo/video server with album change detection
|
||||||
Generated
+4003
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "notify-bridge-frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite dev",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"prepare": "svelte-kit sync || echo ''",
|
||||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/adapter-auto": "^7.0.0",
|
||||||
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
|
"@sveltejs/kit": "^2.50.2",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||||
|
"@tailwindcss/vite": "^4.2.2",
|
||||||
|
"bits-ui": "^2.16.3",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-svelte": "^0.577.0",
|
||||||
|
"svelte": "^5.51.0",
|
||||||
|
"svelte-check": "^4.4.2",
|
||||||
|
"tailwind-merge": "^3.5.0",
|
||||||
|
"tailwind-variants": "^3.2.2",
|
||||||
|
"tailwindcss": "^4.2.2",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"vite": "^7.3.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/lang-html": "^6.4.11",
|
||||||
|
"@codemirror/language": "^6.12.2",
|
||||||
|
"@codemirror/state": "^6.6.0",
|
||||||
|
"@codemirror/theme-one-dark": "^6.1.3",
|
||||||
|
"@codemirror/view": "^6.40.0",
|
||||||
|
"@mdi/js": "^7.4.47",
|
||||||
|
"codemirror": "^6.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
@import 'tailwindcss';
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--color-background: #f8f9fb;
|
||||||
|
--color-foreground: #1a1a2e;
|
||||||
|
--color-muted: #eef0f4;
|
||||||
|
--color-muted-foreground: #6b7280;
|
||||||
|
--color-border: #e2e4ea;
|
||||||
|
--color-primary: #0d9488;
|
||||||
|
--color-primary-foreground: #ffffff;
|
||||||
|
--color-accent: #eef0f4;
|
||||||
|
--color-accent-foreground: #1a1a2e;
|
||||||
|
--color-destructive: #ef4444;
|
||||||
|
--color-card: #ffffff;
|
||||||
|
--color-card-foreground: #1a1a2e;
|
||||||
|
--color-success-bg: #ecfdf5;
|
||||||
|
--color-success-fg: #059669;
|
||||||
|
--color-warning-bg: #fffbeb;
|
||||||
|
--color-warning-fg: #d97706;
|
||||||
|
--color-error-bg: #fef2f2;
|
||||||
|
--color-error-fg: #dc2626;
|
||||||
|
--color-glow: rgba(13, 148, 136, 0.15);
|
||||||
|
--color-glow-strong: rgba(13, 148, 136, 0.3);
|
||||||
|
--color-sidebar: #ffffff;
|
||||||
|
--color-sidebar-active: rgba(13, 148, 136, 0.08);
|
||||||
|
--font-sans: 'DM Sans', ui-sans-serif, system-ui, sans-serif;
|
||||||
|
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
|
||||||
|
--radius: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark theme overrides */
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--color-background: #0c0e14;
|
||||||
|
--color-foreground: #e4e6ed;
|
||||||
|
--color-muted: #1a1d28;
|
||||||
|
--color-muted-foreground: #8b8fa4;
|
||||||
|
--color-border: #252836;
|
||||||
|
--color-primary: #14b8a6;
|
||||||
|
--color-primary-foreground: #0c0e14;
|
||||||
|
--color-accent: #1a1d28;
|
||||||
|
--color-accent-foreground: #e4e6ed;
|
||||||
|
--color-destructive: #f87171;
|
||||||
|
--color-card: #13151e;
|
||||||
|
--color-card-foreground: #e4e6ed;
|
||||||
|
--color-success-bg: #052e16;
|
||||||
|
--color-success-fg: #34d399;
|
||||||
|
--color-warning-bg: #422006;
|
||||||
|
--color-warning-fg: #fbbf24;
|
||||||
|
--color-error-bg: #450a0a;
|
||||||
|
--color-error-fg: #f87171;
|
||||||
|
--color-glow: rgba(20, 184, 166, 0.12);
|
||||||
|
--color-glow-strong: rgba(20, 184, 166, 0.25);
|
||||||
|
--color-sidebar: #10121a;
|
||||||
|
--color-sidebar-active: rgba(20, 184, 166, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
background-color: var(--color-background);
|
||||||
|
color: var(--color-foreground);
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subtle background pattern */
|
||||||
|
body::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: -1;
|
||||||
|
opacity: 0.4;
|
||||||
|
background-image: radial-gradient(circle at 1px 1px, var(--color-border) 0.5px, transparent 0);
|
||||||
|
background-size: 32px 32px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form controls */
|
||||||
|
input, select, textarea {
|
||||||
|
color: var(--color-foreground);
|
||||||
|
background-color: var(--color-background);
|
||||||
|
border-color: var(--color-border);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus-visible, select:focus-visible, textarea:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: 0 0 0 3px var(--color-glow), 0 0 12px var(--color-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 2px solid var(--color-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:focus-visible {
|
||||||
|
outline: 2px solid var(--color-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override browser autofill styles in dark mode */
|
||||||
|
[data-theme="dark"] input:-webkit-autofill,
|
||||||
|
[data-theme="dark"] input:-webkit-autofill:hover,
|
||||||
|
[data-theme="dark"] input:-webkit-autofill:focus,
|
||||||
|
[data-theme="dark"] select:-webkit-autofill {
|
||||||
|
-webkit-box-shadow: 0 0 0 1000px #13151e inset !important;
|
||||||
|
-webkit-text-fill-color: #e4e6ed !important;
|
||||||
|
caret-color: #e4e6ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Color scheme for native controls */
|
||||||
|
[data-theme="dark"] { color-scheme: dark; }
|
||||||
|
[data-theme="light"] { color-scheme: light; }
|
||||||
|
|
||||||
|
/* Scrollbar styling */
|
||||||
|
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||||
|
::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
::-webkit-scrollbar-thumb { background: var(--color-border); border-radius: 3px; }
|
||||||
|
::-webkit-scrollbar-thumb:hover { background: var(--color-muted-foreground); }
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes fadeSlideIn {
|
||||||
|
from { opacity: 0; transform: translateY(12px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { background-position: -200% 0; }
|
||||||
|
100% { background-position: 200% 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulseGlow {
|
||||||
|
0%, 100% { box-shadow: 0 0 4px var(--color-glow); }
|
||||||
|
50% { box-shadow: 0 0 16px var(--color-glow-strong); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes countUp {
|
||||||
|
from { opacity: 0; transform: translateY(8px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-slide-in {
|
||||||
|
animation: fadeSlideIn 0.4s ease-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-shimmer {
|
||||||
|
background: linear-gradient(90deg, var(--color-muted) 25%, var(--color-border) 50%, var(--color-muted) 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-pulse-glow {
|
||||||
|
animation: pulseGlow 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-count-up {
|
||||||
|
animation: countUp 0.5s ease-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stagger children utility */
|
||||||
|
.stagger-children > * {
|
||||||
|
animation: fadeSlideIn 0.4s ease-out both;
|
||||||
|
}
|
||||||
|
.stagger-children > *:nth-child(1) { animation-delay: 0ms; }
|
||||||
|
.stagger-children > *:nth-child(2) { animation-delay: 60ms; }
|
||||||
|
.stagger-children > *:nth-child(3) { animation-delay: 120ms; }
|
||||||
|
.stagger-children > *:nth-child(4) { animation-delay: 180ms; }
|
||||||
|
.stagger-children > *:nth-child(5) { animation-delay: 240ms; }
|
||||||
|
.stagger-children > *:nth-child(6) { animation-delay: 300ms; }
|
||||||
|
|
||||||
|
.font-mono {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300..700;1,9..40,300..700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
|
||||||
|
<title>Notify Bridge</title>
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script>
|
||||||
|
import '../app.css';
|
||||||
|
|
||||||
|
let { children } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{@render children()}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<script>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="min-h-screen flex items-center justify-center">
|
||||||
|
<div class="text-center">
|
||||||
|
<h1 class="text-4xl font-bold text-foreground mb-2">Notify Bridge</h1>
|
||||||
|
<p class="text-muted-foreground">Service-to-notification bridge</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import adapter from '@sveltejs/adapter-static';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
kit: {
|
||||||
|
adapter: adapter({
|
||||||
|
pages: 'build',
|
||||||
|
assets: 'build',
|
||||||
|
fallback: 'index.html'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
vitePlugin: {
|
||||||
|
dynamicCompileOptions: ({ filename }) =>
|
||||||
|
filename.includes('node_modules') ? undefined : { runes: true }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rewriteRelativeImportExtensions": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [tailwindcss(), sveltekit()],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': 'http://localhost:8420'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "notify-bridge-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Core library for Notify Bridge — service provider abstractions, models, notifications, and templates"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"aiohttp>=3.9",
|
||||||
|
"jinja2>=3.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8.0",
|
||||||
|
"pytest-asyncio>=0.23",
|
||||||
|
"aioresponses>=0.7",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["src/notify_bridge_core"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
asyncio_mode = "auto"
|
||||||
|
testpaths = ["tests"]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Notify Bridge Core — service provider abstractions, models, notifications, and templates."""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Core data models — events, media assets, collections."""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Notification dispatch — Telegram, webhooks, queue."""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Telegram notification client."""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Webhook notification client."""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Service provider abstractions and implementations."""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Immich service provider implementation."""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Template system — rendering, variables, validation."""
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "notify-bridge-server"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Standalone Notify Bridge server — FastAPI REST API with SQLite database"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"notify-bridge-core==0.1.0",
|
||||||
|
"fastapi>=0.115",
|
||||||
|
"uvicorn[standard]>=0.32",
|
||||||
|
"sqlmodel>=0.0.22",
|
||||||
|
"aiosqlite>=0.20",
|
||||||
|
"pyjwt>=2.9",
|
||||||
|
"bcrypt>=4.2",
|
||||||
|
"apscheduler>=3.10,<4",
|
||||||
|
"aiohttp>=3.9",
|
||||||
|
"anthropic>=0.42",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8.0",
|
||||||
|
"pytest-asyncio>=0.23",
|
||||||
|
"httpx>=0.27",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
notify-bridge = "notify_bridge_server.main:run"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["src/notify_bridge_server"]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Notify Bridge Server — FastAPI REST API with SQLite database."""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""API route modules."""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Authentication — JWT, login, user management."""
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
"""Server configuration — settings, data directory, secrets."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
DATA_DIR = Path(os.environ.get("NOTIFY_BRIDGE_DATA_DIR", "./data"))
|
||||||
|
SECRET_KEY = os.environ.get("NOTIFY_BRIDGE_SECRET_KEY", "")
|
||||||
|
DATABASE_URL = os.environ.get(
|
||||||
|
"NOTIFY_BRIDGE_DATABASE_URL",
|
||||||
|
f"sqlite+aiosqlite:///{DATA_DIR / 'notify_bridge.db'}",
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Database engine and models."""
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
"""Notify Bridge Server — FastAPI application entry point."""
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
app = FastAPI(title="Notify Bridge", version="0.1.0")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/health")
|
||||||
|
async def health():
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8420)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Business logic services — scheduler, watcher, notifier."""
|
||||||
Reference in New Issue
Block a user