@cachly-dev/mcp-server 0.10.65 → 0.10.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/embeddings.js +10 -0
- package/dist/src/embeddings.js.map +1 -1
- package/dist/src/handlers/brain.js +2 -1
- package/dist/src/handlers/brain.js.map +1 -1
- package/dist/src/handlers/cache.js +4 -0
- package/dist/src/handlers/cache.js.map +1 -1
- package/dist/src/handlers/tco.js +1 -0
- package/dist/src/handlers/tco.js.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/package.json +7 -1
- package/server.json +2 -2
- package/.env.test.example +0 -23
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -50
- package/.github/ISSUE_TEMPLATE/config.yml +0 -8
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
- package/.github/workflows/eas-build-android.yml +0 -58
- package/.github/workflows/eas-update.yml +0 -55
- package/.github/workflows/publish.yml +0 -72
- package/BENCH.md +0 -113
- package/CHANGELOG.md +0 -653
- package/Dockerfile +0 -16
- package/LAUNCH_TEXTS.md +0 -182
- package/PROGRESS.md +0 -141
- package/STRATEGY.md +0 -228
- package/VISION_10X.md +0 -133
- package/apps/travel-chaos-organizer/.env.example +0 -49
- package/apps/travel-chaos-organizer/DEPLOY.md +0 -328
- package/apps/travel-chaos-organizer/Makefile +0 -57
- package/apps/travel-chaos-organizer/README.md +0 -159
- package/apps/travel-chaos-organizer/backend/.env.example +0 -10
- package/apps/travel-chaos-organizer/backend/Dockerfile +0 -25
- package/apps/travel-chaos-organizer/backend/alembic/env.py +0 -56
- package/apps/travel-chaos-organizer/backend/alembic/script.py.mako +0 -24
- package/apps/travel-chaos-organizer/backend/alembic/versions/.gitkeep +0 -0
- package/apps/travel-chaos-organizer/backend/alembic/versions/0001_stripe_customer_id.py +0 -24
- package/apps/travel-chaos-organizer/backend/alembic/versions/0002_telegram_chat_id.py +0 -24
- package/apps/travel-chaos-organizer/backend/alembic.ini +0 -39
- package/apps/travel-chaos-organizer/backend/app/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/auth/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/auth/keycloak.py +0 -81
- package/apps/travel-chaos-organizer/backend/app/config.py +0 -66
- package/apps/travel-chaos-organizer/backend/app/db/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/db/database.py +0 -20
- package/apps/travel-chaos-organizer/backend/app/db/migrations/001_init.sql +0 -71
- package/apps/travel-chaos-organizer/backend/app/db/migrations/002_telemetry_freemium.sql +0 -31
- package/apps/travel-chaos-organizer/backend/app/db/migrations/003_telegram_chat_id.sql +0 -4
- package/apps/travel-chaos-organizer/backend/app/db/migrations/004_stripe_and_email.sql +0 -5
- package/apps/travel-chaos-organizer/backend/app/limiter.py +0 -4
- package/apps/travel-chaos-organizer/backend/app/logging_config.py +0 -25
- package/apps/travel-chaos-organizer/backend/app/main.py +0 -77
- package/apps/travel-chaos-organizer/backend/app/models/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/models/schemas.py +0 -142
- package/apps/travel-chaos-organizer/backend/app/routers/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/routers/admin.py +0 -237
- package/apps/travel-chaos-organizer/backend/app/routers/cache.py +0 -13
- package/apps/travel-chaos-organizer/backend/app/routers/events.py +0 -29
- package/apps/travel-chaos-organizer/backend/app/routers/export.py +0 -122
- package/apps/travel-chaos-organizer/backend/app/routers/inbox.py +0 -122
- package/apps/travel-chaos-organizer/backend/app/routers/items.py +0 -130
- package/apps/travel-chaos-organizer/backend/app/routers/mail.py +0 -108
- package/apps/travel-chaos-organizer/backend/app/routers/parse.py +0 -303
- package/apps/travel-chaos-organizer/backend/app/routers/payments.py +0 -179
- package/apps/travel-chaos-organizer/backend/app/routers/trips.py +0 -134
- package/apps/travel-chaos-organizer/backend/app/routers/users.py +0 -169
- package/apps/travel-chaos-organizer/backend/app/routers/waitlist.py +0 -37
- package/apps/travel-chaos-organizer/backend/app/services/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/app/services/cache.py +0 -131
- package/apps/travel-chaos-organizer/backend/app/services/drip.py +0 -128
- package/apps/travel-chaos-organizer/backend/app/services/email.py +0 -61
- package/apps/travel-chaos-organizer/backend/app/services/errors.py +0 -44
- package/apps/travel-chaos-organizer/backend/app/services/notifier.py +0 -10
- package/apps/travel-chaos-organizer/backend/app/services/ollama.py +0 -101
- package/apps/travel-chaos-organizer/backend/app/services/parser.py +0 -22
- package/apps/travel-chaos-organizer/backend/app/services/quota.py +0 -85
- package/apps/travel-chaos-organizer/backend/app/services/sentry.py +0 -11
- package/apps/travel-chaos-organizer/backend/app/services/telemetry.py +0 -35
- package/apps/travel-chaos-organizer/backend/pytest.ini +0 -4
- package/apps/travel-chaos-organizer/backend/requirements-dev.txt +0 -6
- package/apps/travel-chaos-organizer/backend/requirements.txt +0 -25
- package/apps/travel-chaos-organizer/backend/tests/__init__.py +0 -0
- package/apps/travel-chaos-organizer/backend/tests/conftest.py +0 -223
- package/apps/travel-chaos-organizer/backend/tests/test_admin.py +0 -99
- package/apps/travel-chaos-organizer/backend/tests/test_cache.py +0 -142
- package/apps/travel-chaos-organizer/backend/tests/test_drip.py +0 -32
- package/apps/travel-chaos-organizer/backend/tests/test_e2e_chain.py +0 -121
- package/apps/travel-chaos-organizer/backend/tests/test_export.py +0 -48
- package/apps/travel-chaos-organizer/backend/tests/test_inbox.py +0 -107
- package/apps/travel-chaos-organizer/backend/tests/test_items.py +0 -138
- package/apps/travel-chaos-organizer/backend/tests/test_mail.py +0 -113
- package/apps/travel-chaos-organizer/backend/tests/test_ollama.py +0 -145
- package/apps/travel-chaos-organizer/backend/tests/test_parse_url.py +0 -98
- package/apps/travel-chaos-organizer/backend/tests/test_parser.py +0 -72
- package/apps/travel-chaos-organizer/backend/tests/test_payments.py +0 -163
- package/apps/travel-chaos-organizer/backend/tests/test_quota.py +0 -46
- package/apps/travel-chaos-organizer/backend/tests/test_search.py +0 -67
- package/apps/travel-chaos-organizer/backend/tests/test_trips.py +0 -123
- package/apps/travel-chaos-organizer/backend/tests/test_users.py +0 -52
- package/apps/travel-chaos-organizer/backups/.gitkeep +0 -0
- package/apps/travel-chaos-organizer/caddy/Caddyfile +0 -27
- package/apps/travel-chaos-organizer/docker-compose.node1.yml +0 -31
- package/apps/travel-chaos-organizer/docker-compose.override.yml +0 -14
- package/apps/travel-chaos-organizer/docker-compose.yml +0 -128
- package/apps/travel-chaos-organizer/frontend/.env.example +0 -13
- package/apps/travel-chaos-organizer/frontend/app/(app)/_layout.tsx +0 -80
- package/apps/travel-chaos-organizer/frontend/app/(app)/inbox.tsx +0 -166
- package/apps/travel-chaos-organizer/frontend/app/(app)/search.tsx +0 -130
- package/apps/travel-chaos-organizer/frontend/app/(app)/settings.tsx +0 -381
- package/apps/travel-chaos-organizer/frontend/app/(app)/trips/[id].tsx +0 -346
- package/apps/travel-chaos-organizer/frontend/app/(app)/trips.tsx +0 -363
- package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/cancel.tsx +0 -34
- package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/success.tsx +0 -35
- package/apps/travel-chaos-organizer/frontend/app/(auth)/login.tsx +0 -91
- package/apps/travel-chaos-organizer/frontend/app/_layout.tsx +0 -71
- package/apps/travel-chaos-organizer/frontend/app/auth.tsx +0 -29
- package/apps/travel-chaos-organizer/frontend/app/index.tsx +0 -16
- package/apps/travel-chaos-organizer/frontend/app/onboarding.tsx +0 -119
- package/apps/travel-chaos-organizer/frontend/app.config.ts +0 -54
- package/apps/travel-chaos-organizer/frontend/assets/adaptive-icon.svg +0 -66
- package/apps/travel-chaos-organizer/frontend/assets/icon.svg +0 -79
- package/apps/travel-chaos-organizer/frontend/assets/splash.svg +0 -15
- package/apps/travel-chaos-organizer/frontend/components/BottomSheet.tsx +0 -102
- package/apps/travel-chaos-organizer/frontend/components/ConfettiCannon.tsx +0 -107
- package/apps/travel-chaos-organizer/frontend/components/ConfirmDialog.tsx +0 -109
- package/apps/travel-chaos-organizer/frontend/components/ErrorBoundary.tsx +0 -45
- package/apps/travel-chaos-organizer/frontend/components/FileUploadButton.tsx +0 -19
- package/apps/travel-chaos-organizer/frontend/components/ImportSheet.tsx +0 -155
- package/apps/travel-chaos-organizer/frontend/components/OfflineBadge.tsx +0 -55
- package/apps/travel-chaos-organizer/frontend/components/QRCodeViewer.tsx +0 -80
- package/apps/travel-chaos-organizer/frontend/components/ScoreRing.tsx +0 -39
- package/apps/travel-chaos-organizer/frontend/components/Screen.tsx +0 -39
- package/apps/travel-chaos-organizer/frontend/components/Skeleton.tsx +0 -70
- package/apps/travel-chaos-organizer/frontend/components/SwipeableInboxItem.tsx +0 -121
- package/apps/travel-chaos-organizer/frontend/components/SwipeableTimelineItem.tsx +0 -70
- package/apps/travel-chaos-organizer/frontend/components/TimelineItem.tsx +0 -73
- package/apps/travel-chaos-organizer/frontend/components/TimelineItemDetail.tsx +0 -130
- package/apps/travel-chaos-organizer/frontend/components/Toast.tsx +0 -60
- package/apps/travel-chaos-organizer/frontend/components/ToastContext.tsx +0 -42
- package/apps/travel-chaos-organizer/frontend/components/TripCard.tsx +0 -146
- package/apps/travel-chaos-organizer/frontend/components/TripQRModal.tsx +0 -70
- package/apps/travel-chaos-organizer/frontend/components/UpgradeBanner.tsx +0 -44
- package/apps/travel-chaos-organizer/frontend/eas.json +0 -22
- package/apps/travel-chaos-organizer/frontend/hooks/useInbox.ts +0 -24
- package/apps/travel-chaos-organizer/frontend/hooks/useNetworkSync.ts +0 -60
- package/apps/travel-chaos-organizer/frontend/hooks/useTrips.ts +0 -112
- package/apps/travel-chaos-organizer/frontend/lib/api.ts +0 -223
- package/apps/travel-chaos-organizer/frontend/lib/auth.ts +0 -93
- package/apps/travel-chaos-organizer/frontend/lib/db.ts +0 -86
- package/apps/travel-chaos-organizer/frontend/lib/fileCache.ts +0 -54
- package/apps/travel-chaos-organizer/frontend/lib/haptics.ts +0 -13
- package/apps/travel-chaos-organizer/frontend/lib/notifications.ts +0 -112
- package/apps/travel-chaos-organizer/frontend/lib/offlineQueue.ts +0 -145
- package/apps/travel-chaos-organizer/frontend/lib/quota.tsx +0 -43
- package/apps/travel-chaos-organizer/frontend/lib/sentry.ts +0 -28
- package/apps/travel-chaos-organizer/frontend/lib/shareIntent.ts +0 -44
- package/apps/travel-chaos-organizer/frontend/package-lock.json +0 -13221
- package/apps/travel-chaos-organizer/frontend/package.json +0 -44
- package/apps/travel-chaos-organizer/frontend/tsconfig.json +0 -23
- package/apps/travel-chaos-organizer/keycloak/realm-export.json +0 -59
- package/apps/travel-chaos-organizer/landing/index.html +0 -773
- package/apps/travel-chaos-organizer/scripts/backup.sh +0 -14
- package/apps/travel-chaos-organizer/scripts/configure-env.sh +0 -82
- package/apps/travel-chaos-organizer/scripts/deploy.sh +0 -54
- package/apps/travel-chaos-organizer/scripts/health-check.sh +0 -36
- package/apps/travel-chaos-organizer/scripts/migrate.sh +0 -7
- package/apps/travel-chaos-organizer/scripts/server-setup.sh +0 -85
- package/apps/travel-chaos-organizer/scripts/smoke-test.sh +0 -29
- package/apps/travel-chaos-organizer/scripts/verify-tracking.sh +0 -71
- package/apps/travel-chaos-organizer/telegram-bot/.env.example +0 -3
- package/apps/travel-chaos-organizer/telegram-bot/Dockerfile +0 -6
- package/apps/travel-chaos-organizer/telegram-bot/bot.py +0 -504
- package/apps/travel-chaos-organizer/telegram-bot/requirements.txt +0 -3
- package/dist/src/__tests__/auth.test.d.ts +0 -1
- package/dist/src/__tests__/auth.test.js +0 -107
- package/dist/src/__tests__/auth.test.js.map +0 -1
- package/dist/src/__tests__/brain-flow.test.d.ts +0 -12
- package/dist/src/__tests__/brain-flow.test.js +0 -1241
- package/dist/src/__tests__/brain-flow.test.js.map +0 -1
- package/dist/src/__tests__/confidence.test.d.ts +0 -13
- package/dist/src/__tests__/confidence.test.js +0 -233
- package/dist/src/__tests__/confidence.test.js.map +0 -1
- package/dist/src/__tests__/e2e/api-auth.test.d.ts +0 -7
- package/dist/src/__tests__/e2e/api-auth.test.js +0 -162
- package/dist/src/__tests__/e2e/api-auth.test.js.map +0 -1
- package/dist/src/__tests__/e2e/api-health.test.d.ts +0 -7
- package/dist/src/__tests__/e2e/api-health.test.js +0 -95
- package/dist/src/__tests__/e2e/api-health.test.js.map +0 -1
- package/dist/src/__tests__/e2e/cli.test.d.ts +0 -10
- package/dist/src/__tests__/e2e/cli.test.js +0 -204
- package/dist/src/__tests__/e2e/cli.test.js.map +0 -1
- package/dist/src/__tests__/e2e/config.d.ts +0 -35
- package/dist/src/__tests__/e2e/config.js +0 -53
- package/dist/src/__tests__/e2e/config.js.map +0 -1
- package/dist/src/__tests__/e2e/funnel.test.d.ts +0 -13
- package/dist/src/__tests__/e2e/funnel.test.js +0 -90
- package/dist/src/__tests__/e2e/funnel.test.js.map +0 -1
- package/dist/src/__tests__/e2e/mcp-tools.test.d.ts +0 -7
- package/dist/src/__tests__/e2e/mcp-tools.test.js +0 -129
- package/dist/src/__tests__/e2e/mcp-tools.test.js.map +0 -1
- package/dist/src/__tests__/e2e/provisioning.test.d.ts +0 -15
- package/dist/src/__tests__/e2e/provisioning.test.js +0 -123
- package/dist/src/__tests__/e2e/provisioning.test.js.map +0 -1
- package/dist/src/__tests__/handlers-unit.test.d.ts +0 -12
- package/dist/src/__tests__/handlers-unit.test.js +0 -505
- package/dist/src/__tests__/handlers-unit.test.js.map +0 -1
- package/dist/src/__tests__/namespace.test.d.ts +0 -1
- package/dist/src/__tests__/namespace.test.js +0 -114
- package/dist/src/__tests__/namespace.test.js.map +0 -1
- package/dist/src/__tests__/rerank.test.d.ts +0 -1
- package/dist/src/__tests__/rerank.test.js +0 -125
- package/dist/src/__tests__/rerank.test.js.map +0 -1
- package/dist/src/__tests__/utils.test.d.ts +0 -1
- package/dist/src/__tests__/utils.test.js +0 -83
- package/dist/src/__tests__/utils.test.js.map +0 -1
- package/dist/src/bench/cachly-bench.d.ts +0 -39
- package/dist/src/bench/cachly-bench.js +0 -223
- package/dist/src/bench/cachly-bench.js.map +0 -1
- package/dist/src/bench/fixtures.d.ts +0 -37
- package/dist/src/bench/fixtures.js +0 -225
- package/dist/src/bench/fixtures.js.map +0 -1
- package/dist/src/search.test.d.ts +0 -1
- package/dist/src/search.test.js +0 -713
- package/dist/src/search.test.js.map +0 -1
- package/eslint.config.js +0 -16
- package/glama.json +0 -36
- package/llms.txt +0 -155
- package/packages/telegram-notify/SPLIT_PLAN.md +0 -106
- package/packages/telegram-notify/client.ts +0 -88
- package/packages/telegram-notify/emojis.json +0 -22
- package/packages/telegram-notify/pyproject.toml +0 -17
- package/packages/telegram-notify/telegram_notify/__init__.py +0 -3
- package/packages/telegram-notify/telegram_notify/client.py +0 -87
- package/packages/telegram-notify/telegram_notify/emojis.json +0 -22
- package/scripts/e2e-run.sh +0 -160
- package/scripts/generate-icons.js +0 -28
- package/smithery.yaml +0 -45
- package/src/__tests__/auth.test.ts +0 -147
- package/src/__tests__/brain-flow.test.ts +0 -1472
- package/src/__tests__/confidence.test.ts +0 -285
- package/src/__tests__/e2e/api-auth.test.ts +0 -182
- package/src/__tests__/e2e/api-health.test.ts +0 -103
- package/src/__tests__/e2e/cli.test.ts +0 -228
- package/src/__tests__/e2e/config.ts +0 -53
- package/src/__tests__/e2e/funnel.test.ts +0 -99
- package/src/__tests__/e2e/mcp-tools.test.ts +0 -191
- package/src/__tests__/e2e/provisioning.test.ts +0 -128
- package/src/__tests__/handlers-unit.test.ts +0 -677
- package/src/__tests__/namespace.test.ts +0 -126
- package/src/__tests__/rerank.test.ts +0 -141
- package/src/__tests__/utils.test.ts +0 -81
- package/src/auth.ts +0 -46
- package/src/bench/cachly-bench.ts +0 -263
- package/src/bench/fixtures.ts +0 -256
- package/src/ckg.ts +0 -103
- package/src/confidence.ts +0 -38
- package/src/embeddings.ts +0 -217
- package/src/handlers/advanced.ts +0 -647
- package/src/handlers/brain.ts +0 -2622
- package/src/handlers/cache.ts +0 -633
- package/src/handlers/context.ts +0 -222
- package/src/handlers/fedbrain.ts +0 -1232
- package/src/handlers/instances.ts +0 -180
- package/src/handlers/roadmap.ts +0 -201
- package/src/handlers/syndicate.ts +0 -460
- package/src/handlers/tco.ts +0 -345
- package/src/handlers/team.ts +0 -1249
- package/src/index.ts +0 -2815
- package/src/namespace.ts +0 -128
- package/src/notifier.ts +0 -5
- package/src/rerank.ts +0 -148
- package/src/search.test.ts +0 -848
- package/src/search.ts +0 -1781
- package/src/tools.ts +0 -2056
- package/src/utils.ts +0 -90
- package/test-features.cjs +0 -23
- package/tsconfig.json +0 -16
- package/vitest.config.ts +0 -14
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import sys
|
|
3
|
-
import json
|
|
4
|
-
from datetime import datetime, timezone
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class JsonFormatter(logging.Formatter):
|
|
8
|
-
def format(self, record: logging.LogRecord) -> str:
|
|
9
|
-
return json.dumps({
|
|
10
|
-
"ts": datetime.now(timezone.utc).isoformat(),
|
|
11
|
-
"level": record.levelname,
|
|
12
|
-
"logger": record.name,
|
|
13
|
-
"msg": record.getMessage(),
|
|
14
|
-
**({"exc": self.formatException(record.exc_info)} if record.exc_info else {}),
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def setup_logging(debug: bool = False) -> None:
|
|
19
|
-
level = logging.DEBUG if debug else logging.INFO
|
|
20
|
-
handler = logging.StreamHandler(sys.stdout)
|
|
21
|
-
handler.setFormatter(JsonFormatter())
|
|
22
|
-
logging.basicConfig(level=level, handlers=[handler], force=True)
|
|
23
|
-
# Silence noisy libraries
|
|
24
|
-
for name in ("uvicorn.access", "sqlalchemy.engine", "asyncio"):
|
|
25
|
-
logging.getLogger(name).setLevel(logging.WARNING)
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
from contextlib import asynccontextmanager
|
|
2
|
-
import os
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from fastapi import FastAPI
|
|
5
|
-
from fastapi.middleware.cors import CORSMiddleware
|
|
6
|
-
from app.config import get_settings
|
|
7
|
-
from app.routers import trips, items, parse, inbox, mail, events as events_router, waitlist as waitlist_router
|
|
8
|
-
from app.routers import admin as admin_router
|
|
9
|
-
from app.routers import users as users_router
|
|
10
|
-
from app.routers import payments as payments_router
|
|
11
|
-
from app.routers import export as export_router
|
|
12
|
-
from app.routers import cache as cache_router
|
|
13
|
-
from app.services.cache import cachly_configured
|
|
14
|
-
from app.limiter import limiter
|
|
15
|
-
from slowapi import _rate_limit_exceeded_handler
|
|
16
|
-
from slowapi.errors import RateLimitExceeded
|
|
17
|
-
from slowapi.middleware import SlowAPIMiddleware
|
|
18
|
-
|
|
19
|
-
settings = get_settings()
|
|
20
|
-
|
|
21
|
-
APP_VERSION = "0.1.0"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@asynccontextmanager
|
|
25
|
-
async def lifespan(app: FastAPI):
|
|
26
|
-
from app.logging_config import setup_logging
|
|
27
|
-
setup_logging(debug=settings.debug)
|
|
28
|
-
from app.services import sentry as sentry_svc
|
|
29
|
-
sentry_svc.init(settings.sentry_dsn, settings.environment, release=APP_VERSION)
|
|
30
|
-
Path(settings.upload_dir).mkdir(parents=True, exist_ok=True)
|
|
31
|
-
import sys
|
|
32
|
-
if settings.admin_password == "changeme":
|
|
33
|
-
print("⚠️ WARNING: ADMIN_PASSWORD is still 'changeme'. Change it before going live!", file=sys.stderr)
|
|
34
|
-
yield
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
app = FastAPI(
|
|
38
|
-
title=settings.app_name,
|
|
39
|
-
version=APP_VERSION,
|
|
40
|
-
lifespan=lifespan,
|
|
41
|
-
docs_url="/docs" if settings.debug else None,
|
|
42
|
-
redoc_url="/redoc" if settings.debug else None,
|
|
43
|
-
openapi_url="/openapi.json" if settings.debug else None,
|
|
44
|
-
)
|
|
45
|
-
app.state.limiter = limiter
|
|
46
|
-
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
|
47
|
-
app.add_middleware(SlowAPIMiddleware)
|
|
48
|
-
|
|
49
|
-
app.add_middleware(
|
|
50
|
-
CORSMiddleware,
|
|
51
|
-
allow_origins=settings.cors_origins.split(",") if settings.cors_origins != "*" else ["*"],
|
|
52
|
-
allow_credentials=True,
|
|
53
|
-
allow_methods=["*"],
|
|
54
|
-
allow_headers=["*"],
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
app.include_router(trips.router, prefix="/api/v1")
|
|
58
|
-
app.include_router(items.router, prefix="/api/v1")
|
|
59
|
-
app.include_router(parse.router, prefix="/api/v1")
|
|
60
|
-
app.include_router(inbox.router, prefix="/api/v1")
|
|
61
|
-
app.include_router(mail.router, prefix="/api/v1")
|
|
62
|
-
app.include_router(events_router.router, prefix="/api/v1")
|
|
63
|
-
app.include_router(waitlist_router.router, prefix="/api/v1")
|
|
64
|
-
app.include_router(admin_router.router) # no /api/v1 prefix — admin is at /admin
|
|
65
|
-
app.include_router(users_router.router, prefix="/api/v1")
|
|
66
|
-
app.include_router(payments_router.router, prefix="/api/v1")
|
|
67
|
-
app.include_router(export_router.router, prefix="/api/v1")
|
|
68
|
-
app.include_router(cache_router.router, prefix="/api/v1")
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
@app.get("/health")
|
|
72
|
-
async def health():
|
|
73
|
-
return {
|
|
74
|
-
"status": "ok",
|
|
75
|
-
"version": APP_VERSION,
|
|
76
|
-
"cachly_cache": "enabled" if cachly_configured() else "disabled",
|
|
77
|
-
}
|
|
File without changes
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
from datetime import date, datetime
|
|
3
|
-
from typing import Any, Literal
|
|
4
|
-
from uuid import UUID
|
|
5
|
-
from pydantic import BaseModel, Field
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
ItemType = Literal[
|
|
9
|
-
"flight", "train", "bus", "hotel", "rental_car",
|
|
10
|
-
"activity", "transfer", "document", "other"
|
|
11
|
-
]
|
|
12
|
-
|
|
13
|
-
InboxStatus = Literal["pending", "processed", "rejected", "assigned"]
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
# ── Trips ──────────────────────────────────────────────────────────────────
|
|
17
|
-
|
|
18
|
-
class TripCreate(BaseModel):
|
|
19
|
-
name: str = Field(..., min_length=1, max_length=200)
|
|
20
|
-
description: str | None = None
|
|
21
|
-
start_date: date | None = None
|
|
22
|
-
end_date: date | None = None
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class TripUpdate(BaseModel):
|
|
26
|
-
name: str | None = Field(None, min_length=1, max_length=200)
|
|
27
|
-
description: str | None = None
|
|
28
|
-
start_date: date | None = None
|
|
29
|
-
end_date: date | None = None
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class TripOut(BaseModel):
|
|
33
|
-
id: UUID
|
|
34
|
-
user_id: str
|
|
35
|
-
name: str
|
|
36
|
-
description: str | None
|
|
37
|
-
start_date: date | None
|
|
38
|
-
end_date: date | None
|
|
39
|
-
created_at: datetime
|
|
40
|
-
updated_at: datetime
|
|
41
|
-
|
|
42
|
-
model_config = {"from_attributes": True}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
# ── Trip Items ─────────────────────────────────────────────────────────────
|
|
46
|
-
|
|
47
|
-
class TripItemCreate(BaseModel):
|
|
48
|
-
type: ItemType = "other"
|
|
49
|
-
title: str = Field(..., min_length=1, max_length=300)
|
|
50
|
-
raw_text: str | None = None
|
|
51
|
-
parsed_data: dict[str, Any] | None = None
|
|
52
|
-
event_at: datetime | None = None
|
|
53
|
-
event_end_at: datetime | None = None
|
|
54
|
-
booking_ref: str | None = None
|
|
55
|
-
provider: str | None = None
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
class TripItemUpdate(BaseModel):
|
|
59
|
-
type: ItemType | None = None
|
|
60
|
-
title: str | None = Field(None, min_length=1, max_length=300)
|
|
61
|
-
raw_text: str | None = None
|
|
62
|
-
parsed_data: dict[str, Any] | None = None
|
|
63
|
-
event_at: datetime | None = None
|
|
64
|
-
event_end_at: datetime | None = None
|
|
65
|
-
booking_ref: str | None = None
|
|
66
|
-
provider: str | None = None
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
class TripItemOut(BaseModel):
|
|
70
|
-
id: UUID
|
|
71
|
-
trip_id: UUID
|
|
72
|
-
user_id: str
|
|
73
|
-
type: str
|
|
74
|
-
title: str
|
|
75
|
-
raw_text: str | None
|
|
76
|
-
parsed_data: dict[str, Any] | None
|
|
77
|
-
event_at: datetime | None
|
|
78
|
-
event_end_at: datetime | None
|
|
79
|
-
booking_ref: str | None
|
|
80
|
-
provider: str | None
|
|
81
|
-
created_at: datetime
|
|
82
|
-
updated_at: datetime
|
|
83
|
-
|
|
84
|
-
model_config = {"from_attributes": True}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
# ── Attachments ────────────────────────────────────────────────────────────
|
|
88
|
-
|
|
89
|
-
class AttachmentOut(BaseModel):
|
|
90
|
-
id: UUID
|
|
91
|
-
trip_item_id: UUID | None
|
|
92
|
-
inbox_id: UUID | None
|
|
93
|
-
file_name: str
|
|
94
|
-
mime_type: str
|
|
95
|
-
size_bytes: int | None
|
|
96
|
-
created_at: datetime
|
|
97
|
-
|
|
98
|
-
model_config = {"from_attributes": True}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
# ── Chaos Inbox ────────────────────────────────────────────────────────────
|
|
102
|
-
|
|
103
|
-
class InboxItemOut(BaseModel):
|
|
104
|
-
id: UUID
|
|
105
|
-
user_id: str
|
|
106
|
-
raw_content: str | None
|
|
107
|
-
source: str | None
|
|
108
|
-
status: str
|
|
109
|
-
parsed_data: dict[str, Any] | None
|
|
110
|
-
created_at: datetime
|
|
111
|
-
updated_at: datetime
|
|
112
|
-
|
|
113
|
-
model_config = {"from_attributes": True}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
class InboxAssign(BaseModel):
|
|
117
|
-
trip_id: UUID
|
|
118
|
-
type: ItemType = "other"
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
# ── Parsing ────────────────────────────────────────────────────────────────
|
|
122
|
-
|
|
123
|
-
class ParsedTravelData(BaseModel):
|
|
124
|
-
type: ItemType = "other"
|
|
125
|
-
title: str = "Unnamed Item"
|
|
126
|
-
booking_ref: str | None = None
|
|
127
|
-
provider: str | None = None
|
|
128
|
-
event_at: str | None = None
|
|
129
|
-
event_end_at: str | None = None
|
|
130
|
-
origin: str | None = None
|
|
131
|
-
destination: str | None = None
|
|
132
|
-
passengers: list[str] | None = None
|
|
133
|
-
confirmation_number: str | None = None
|
|
134
|
-
price: str | None = None
|
|
135
|
-
raw_summary: str | None = None
|
|
136
|
-
confidence: float = 0.0
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
class ParseResponse(BaseModel):
|
|
140
|
-
parsed: ParsedTravelData
|
|
141
|
-
raw_text: str | None = None
|
|
142
|
-
model_used: str
|
|
File without changes
|
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Admin endpoints — Basic Auth protected, no JWT required.
|
|
3
|
-
Mount at /admin (no /api/v1 prefix).
|
|
4
|
-
"""
|
|
5
|
-
import os
|
|
6
|
-
import secrets
|
|
7
|
-
from typing import Annotated
|
|
8
|
-
from fastapi import APIRouter, Depends, HTTPException
|
|
9
|
-
from fastapi.responses import HTMLResponse
|
|
10
|
-
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
|
11
|
-
from sqlalchemy.ext.asyncio import AsyncSession
|
|
12
|
-
from sqlalchemy import text
|
|
13
|
-
from app.db.database import get_db
|
|
14
|
-
from app.config import get_settings
|
|
15
|
-
|
|
16
|
-
router = APIRouter(prefix="/admin", tags=["admin"])
|
|
17
|
-
security = HTTPBasic()
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def _check_auth(credentials: Annotated[HTTPBasicCredentials, Depends(security)]) -> None:
|
|
21
|
-
s = get_settings()
|
|
22
|
-
ok_user = secrets.compare_digest(credentials.username.encode(), s.admin_user.encode())
|
|
23
|
-
ok_pass = secrets.compare_digest(credentials.password.encode(), s.admin_password.encode())
|
|
24
|
-
if not (ok_user and ok_pass):
|
|
25
|
-
raise HTTPException(status_code=401, headers={"WWW-Authenticate": "Basic"})
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
@router.get("", response_class=HTMLResponse, include_in_schema=False,
|
|
29
|
-
dependencies=[Depends(_check_auth)])
|
|
30
|
-
async def admin_dashboard():
|
|
31
|
-
html = """<!DOCTYPE html>
|
|
32
|
-
<html lang="de">
|
|
33
|
-
<head>
|
|
34
|
-
<meta charset="UTF-8">
|
|
35
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
36
|
-
<title>TCO Admin</title>
|
|
37
|
-
<style>
|
|
38
|
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
39
|
-
body { font-family: system-ui, sans-serif; background: #0f0f1a; color: #ccc; }
|
|
40
|
-
#login { display: flex; flex-direction: column; gap: 12px; max-width: 320px; margin: 15vh auto; padding: 32px; background: #1a1a2e; border-radius: 16px; }
|
|
41
|
-
#login h1 { color: #fff; font-size: 20px; }
|
|
42
|
-
input { padding: 10px 14px; border-radius: 8px; border: 1px solid #2a2a4a; background: #0f0f1a; color: #fff; font-size: 14px; }
|
|
43
|
-
button { padding: 10px; background: #4f46e5; color: #fff; border: none; border-radius: 8px; cursor: pointer; font-weight: 700; }
|
|
44
|
-
button:hover { background: #4338ca; }
|
|
45
|
-
#dash { display: none; padding: 24px; max-width: 1100px; margin: 0 auto; }
|
|
46
|
-
h2 { color: #fff; font-size: 22px; margin-bottom: 20px; }
|
|
47
|
-
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 16px; margin-bottom: 32px; }
|
|
48
|
-
.card { background: #1a1a2e; border-radius: 12px; padding: 20px; text-align: center; }
|
|
49
|
-
.card .n { font-size: 36px; font-weight: 800; color: #4f46e5; }
|
|
50
|
-
.card .l { font-size: 12px; color: #6666aa; margin-top: 4px; }
|
|
51
|
-
.section { background: #1a1a2e; border-radius: 12px; padding: 20px; margin-bottom: 24px; }
|
|
52
|
-
.section h3 { color: #fff; margin-bottom: 14px; font-size: 15px; }
|
|
53
|
-
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
54
|
-
th { text-align: left; color: #6666aa; padding: 6px 8px; border-bottom: 1px solid #2a2a4a; font-weight: 600; }
|
|
55
|
-
td { padding: 6px 8px; border-bottom: 1px solid #1a1a2e; color: #ccc; max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
56
|
-
.badge { display: inline-block; padding: 2px 8px; border-radius: 6px; font-size: 11px; font-weight: 700; }
|
|
57
|
-
.badge-pro { background: #4f46e522; color: #818cf8; }
|
|
58
|
-
.badge-free { background: #ffffff11; color: #6666aa; }
|
|
59
|
-
.top-bar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 28px; }
|
|
60
|
-
.top-bar h2 { margin: 0; }
|
|
61
|
-
#logout { background: transparent; color: #6666aa; font-size: 13px; padding: 6px 12px; border: 1px solid #2a2a4a; }
|
|
62
|
-
#err { color: #ff8888; font-size: 13px; }
|
|
63
|
-
#plan-form { display: flex; gap: 8px; margin-top: 6px; flex-wrap: wrap; }
|
|
64
|
-
#plan-form input { flex: 1; min-width: 120px; }
|
|
65
|
-
#plan-form select { padding: 10px; background: #0f0f1a; border: 1px solid #2a2a4a; color: #fff; border-radius: 8px; }
|
|
66
|
-
</style>
|
|
67
|
-
</head>
|
|
68
|
-
<body>
|
|
69
|
-
<div id="login">
|
|
70
|
-
<h1>✈️ TCO Admin</h1>
|
|
71
|
-
<input id="u" placeholder="Username" autocomplete="username">
|
|
72
|
-
<input id="p" type="password" placeholder="Password" autocomplete="current-password">
|
|
73
|
-
<button onclick="doLogin()">Login</button>
|
|
74
|
-
<span id="err"></span>
|
|
75
|
-
</div>
|
|
76
|
-
<div id="dash">
|
|
77
|
-
<div class="top-bar"><h2>✈️ TCO Admin</h2><button id="logout" onclick="doLogout()">Logout</button></div>
|
|
78
|
-
<div class="cards" id="cards"></div>
|
|
79
|
-
<div class="section">
|
|
80
|
-
<h3>Plan-Upgrade</h3>
|
|
81
|
-
<div id="plan-form">
|
|
82
|
-
<input id="pu-uid" placeholder="User ID (Keycloak sub)">
|
|
83
|
-
<select id="pu-plan"><option value="pro">pro</option><option value="free">free</option></select>
|
|
84
|
-
<input id="pu-days" placeholder="Tage (leer = unbegrenzt)" type="number" style="max-width:180px">
|
|
85
|
-
<button onclick="upgradePlan()">Setzen</button>
|
|
86
|
-
</div>
|
|
87
|
-
</div>
|
|
88
|
-
<div class="section"><h3>Top Events</h3><table id="t-events"><thead><tr><th>Event</th><th>Count</th><th>Zuletzt</th></tr></thead><tbody></tbody></table></div>
|
|
89
|
-
<div class="section"><h3>Benutzer</h3><table id="t-users"><thead><tr><th>ID</th><th>Plan</th><th>Events</th><th>Seit</th></tr></thead><tbody></tbody></table></div>
|
|
90
|
-
<div class="section"><h3>Waitlist</h3><table id="t-wl"><thead><tr><th>Email</th><th>Source</th><th>Datum</th></tr></thead><tbody></tbody></table></div>
|
|
91
|
-
</div>
|
|
92
|
-
<script>
|
|
93
|
-
let _creds = '';
|
|
94
|
-
function b64(u,p){ return btoa(u+':'+p); }
|
|
95
|
-
function fmt(s){ return s ? new Date(s).toLocaleString('de') : '-'; }
|
|
96
|
-
async function api(path, opts={}){
|
|
97
|
-
const r = await fetch(path, { headers: { Authorization: 'Basic '+_creds }, ...opts });
|
|
98
|
-
if (!r.ok) throw new Error(r.status);
|
|
99
|
-
return r.json();
|
|
100
|
-
}
|
|
101
|
-
async function doLogin(){
|
|
102
|
-
_creds = b64(document.getElementById('u').value, document.getElementById('p').value);
|
|
103
|
-
try {
|
|
104
|
-
const h = await api('/admin/health');
|
|
105
|
-
document.getElementById('login').style.display='none';
|
|
106
|
-
document.getElementById('dash').style.display='block';
|
|
107
|
-
load(h);
|
|
108
|
-
} catch { document.getElementById('err').textContent='Falsche Zugangsdaten'; _creds=''; }
|
|
109
|
-
}
|
|
110
|
-
function doLogout(){ _creds=''; document.getElementById('dash').style.display='none'; document.getElementById('login').style.display='flex'; }
|
|
111
|
-
async function load(health){
|
|
112
|
-
const c = document.getElementById('cards');
|
|
113
|
-
c.innerHTML = [
|
|
114
|
-
[health.events_total,'Events gesamt'],
|
|
115
|
-
[health.waitlist_total,'Waitlist'],
|
|
116
|
-
[health.users_total,'Benutzer'],
|
|
117
|
-
[health.pro_users,'Pro-User'],
|
|
118
|
-
].map(([n,l])=>`<div class="card"><div class="n">${n??0}</div><div class="l">${l}</div></div>`).join('');
|
|
119
|
-
const [ev, users, wl] = await Promise.all([api('/admin/events/summary'), api('/admin/users'), api('/admin/waitlist')]);
|
|
120
|
-
const tb = (id, rows) => { document.querySelector('#'+id+' tbody').innerHTML = rows; };
|
|
121
|
-
tb('t-events', ev.events.map(e=>`<tr><td>${e.event_name}</td><td>${e.count}</td><td>${fmt(e.last_seen)}</td></tr>`).join(''));
|
|
122
|
-
tb('t-users', users.users.map(u=>`<tr><td title="${u.id}">${u.id.slice(0,12)}…</td><td><span class="badge badge-${u.plan}">${u.plan}</span></td><td>${u.event_count}</td><td>${fmt(u.created_at)}</td></tr>`).join(''));
|
|
123
|
-
tb('t-wl', wl.signups.map(s=>`<tr><td>${s.email}</td><td>${s.source||'-'}</td><td>${fmt(s.created_at)}</td></tr>`).join(''));
|
|
124
|
-
}
|
|
125
|
-
async function upgradePlan(){
|
|
126
|
-
const uid = document.getElementById('pu-uid').value.trim();
|
|
127
|
-
const plan = document.getElementById('pu-plan').value;
|
|
128
|
-
const days = document.getElementById('pu-days').value;
|
|
129
|
-
if (!uid) { alert('User ID fehlt'); return; }
|
|
130
|
-
const q = days ? `?plan=${plan}&expires_days=${days}` : `?plan=${plan}`;
|
|
131
|
-
try { await api('/admin/users/'+uid+'/plan'+q, {method:'PATCH'}); alert('Plan gesetzt'); }
|
|
132
|
-
catch { alert('Fehler'); }
|
|
133
|
-
}
|
|
134
|
-
document.getElementById('p').addEventListener('keydown', e => { if(e.key==='Enter') doLogin(); });
|
|
135
|
-
</script>
|
|
136
|
-
</body>
|
|
137
|
-
</html>"""
|
|
138
|
-
return HTMLResponse(html)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
@router.get("/events/summary", dependencies=[Depends(_check_auth)])
|
|
142
|
-
async def events_summary(db: Annotated[AsyncSession, Depends(get_db)]):
|
|
143
|
-
rows = await db.execute(text("""
|
|
144
|
-
SELECT event_name, COUNT(*) as count,
|
|
145
|
-
MAX(created_at) as last_seen
|
|
146
|
-
FROM events
|
|
147
|
-
GROUP BY event_name
|
|
148
|
-
ORDER BY count DESC
|
|
149
|
-
LIMIT 50
|
|
150
|
-
"""))
|
|
151
|
-
return {"events": [dict(r._mapping) for r in rows.fetchall()]}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
@router.get("/events/recent", dependencies=[Depends(_check_auth)])
|
|
155
|
-
async def events_recent(db: Annotated[AsyncSession, Depends(get_db)], limit: int = 50):
|
|
156
|
-
rows = await db.execute(text("""
|
|
157
|
-
SELECT id, user_id, event_name, properties, platform, app_version, created_at
|
|
158
|
-
FROM events ORDER BY created_at DESC LIMIT :limit
|
|
159
|
-
"""), {"limit": min(limit, 200)})
|
|
160
|
-
return {"events": [dict(r._mapping) for r in rows.fetchall()]}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
@router.get("/waitlist", dependencies=[Depends(_check_auth)])
|
|
164
|
-
async def waitlist(db: Annotated[AsyncSession, Depends(get_db)]):
|
|
165
|
-
rows = await db.execute(text(
|
|
166
|
-
"SELECT id, email, source, created_at FROM waitlist ORDER BY created_at DESC"
|
|
167
|
-
))
|
|
168
|
-
return {"signups": [dict(r._mapping) for r in rows.fetchall()]}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
@router.get("/health", dependencies=[Depends(_check_auth)])
|
|
172
|
-
async def admin_health(db: Annotated[AsyncSession, Depends(get_db)]):
|
|
173
|
-
event_count = await db.execute(text("SELECT COUNT(*) FROM events"))
|
|
174
|
-
waitlist_count = await db.execute(text("SELECT COUNT(*) FROM waitlist"))
|
|
175
|
-
user_count = await db.execute(text("SELECT COUNT(*) FROM users"))
|
|
176
|
-
from datetime import datetime, timezone
|
|
177
|
-
pro_count = await db.execute(
|
|
178
|
-
text("SELECT COUNT(*) FROM users WHERE plan != 'free' AND (plan_expires_at IS NULL OR plan_expires_at > :now)"),
|
|
179
|
-
{"now": datetime.now(timezone.utc)},
|
|
180
|
-
)
|
|
181
|
-
return {
|
|
182
|
-
"events_total": event_count.scalar(),
|
|
183
|
-
"waitlist_total": waitlist_count.scalar(),
|
|
184
|
-
"users_total": user_count.scalar(),
|
|
185
|
-
"pro_users": pro_count.scalar(),
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
@router.get("/users", dependencies=[Depends(_check_auth)])
|
|
190
|
-
async def list_users(db: Annotated[AsyncSession, Depends(get_db)], limit: int = 100):
|
|
191
|
-
rows = await db.execute(text("""
|
|
192
|
-
SELECT u.id, u.email, u.plan, u.plan_expires_at, u.created_at,
|
|
193
|
-
COUNT(e.id) AS event_count
|
|
194
|
-
FROM users u
|
|
195
|
-
LEFT JOIN events e ON e.user_id = u.id
|
|
196
|
-
GROUP BY u.id, u.email, u.plan, u.plan_expires_at, u.created_at
|
|
197
|
-
ORDER BY u.created_at DESC
|
|
198
|
-
LIMIT :limit
|
|
199
|
-
"""), {"limit": min(limit, 500)})
|
|
200
|
-
return {"users": [dict(r._mapping) for r in rows.fetchall()]}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
@router.patch("/users/{uid}/plan", dependencies=[Depends(_check_auth)])
|
|
204
|
-
async def set_user_plan(
|
|
205
|
-
uid: str,
|
|
206
|
-
plan: str,
|
|
207
|
-
db: Annotated[AsyncSession, Depends(get_db)],
|
|
208
|
-
expires_days: int | None = None,
|
|
209
|
-
):
|
|
210
|
-
if plan not in ("free", "pro"):
|
|
211
|
-
raise HTTPException(status_code=400, detail="plan must be 'free' or 'pro'")
|
|
212
|
-
from datetime import datetime, timedelta, timezone
|
|
213
|
-
now = datetime.now(timezone.utc)
|
|
214
|
-
expires_at = (now + timedelta(days=expires_days)) if expires_days else None
|
|
215
|
-
await db.execute(
|
|
216
|
-
text("""
|
|
217
|
-
INSERT INTO users (id, plan, plan_expires_at, updated_at)
|
|
218
|
-
VALUES (:uid, :plan, :expires_at, :now)
|
|
219
|
-
ON CONFLICT (id) DO UPDATE
|
|
220
|
-
SET plan = EXCLUDED.plan,
|
|
221
|
-
plan_expires_at = EXCLUDED.plan_expires_at,
|
|
222
|
-
updated_at = EXCLUDED.updated_at
|
|
223
|
-
"""),
|
|
224
|
-
{"uid": uid, "plan": plan, "expires_at": expires_at, "now": now},
|
|
225
|
-
)
|
|
226
|
-
await db.commit()
|
|
227
|
-
return {"ok": True, "uid": uid, "plan": plan}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
@router.post("/drip/run", dependencies=[Depends(_check_auth)])
|
|
231
|
-
async def run_drip_sequence(
|
|
232
|
-
db: Annotated[AsyncSession, Depends(get_db)],
|
|
233
|
-
dry_run: bool = False,
|
|
234
|
-
):
|
|
235
|
-
from app.services.drip import run_drip
|
|
236
|
-
result = await run_drip(db, dry_run=dry_run)
|
|
237
|
-
return result
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
"""Cache stats endpoint — exposes Cachly Redis metrics to the frontend and MCP tools."""
|
|
2
|
-
from fastapi import APIRouter, Depends
|
|
3
|
-
from typing import Annotated
|
|
4
|
-
from app.auth.keycloak import user_id
|
|
5
|
-
from app.services.cache import cache_stats
|
|
6
|
-
|
|
7
|
-
router = APIRouter(prefix="/cache", tags=["cache"])
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@router.get("/stats")
|
|
11
|
-
async def get_cache_stats(_uid: Annotated[str, Depends(user_id)]):
|
|
12
|
-
"""Return Cachly Redis cache statistics for the current deployment."""
|
|
13
|
-
return await cache_stats()
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
from typing import Annotated
|
|
2
|
-
from fastapi import APIRouter, Depends, Request
|
|
3
|
-
from pydantic import BaseModel
|
|
4
|
-
from sqlalchemy.ext.asyncio import AsyncSession
|
|
5
|
-
from app.auth.keycloak import user_id
|
|
6
|
-
from app.db.database import get_db
|
|
7
|
-
from app.services import telemetry
|
|
8
|
-
from app.limiter import limiter
|
|
9
|
-
|
|
10
|
-
router = APIRouter(prefix="/events", tags=["events"])
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class EventBody(BaseModel):
|
|
14
|
-
event_name: str
|
|
15
|
-
properties: dict | None = None
|
|
16
|
-
platform: str | None = None
|
|
17
|
-
app_version: str | None = None
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@limiter.limit("30/minute")
|
|
21
|
-
@router.post("")
|
|
22
|
-
async def log_event(
|
|
23
|
-
request: Request,
|
|
24
|
-
body: EventBody,
|
|
25
|
-
uid: Annotated[str, Depends(user_id)],
|
|
26
|
-
db: Annotated[AsyncSession, Depends(get_db)],
|
|
27
|
-
):
|
|
28
|
-
await telemetry.track(db, uid, body.event_name, body.properties, body.platform, body.app_version)
|
|
29
|
-
return {"ok": True}
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Export trip as PDF itinerary.
|
|
3
|
-
GET /api/v1/trips/{trip_id}/export/pdf
|
|
4
|
-
"""
|
|
5
|
-
from typing import Annotated
|
|
6
|
-
from uuid import UUID
|
|
7
|
-
from fastapi import APIRouter, Depends, HTTPException
|
|
8
|
-
from fastapi.responses import StreamingResponse
|
|
9
|
-
from sqlalchemy.ext.asyncio import AsyncSession
|
|
10
|
-
from sqlalchemy import text
|
|
11
|
-
from io import BytesIO
|
|
12
|
-
from app.auth.keycloak import user_id
|
|
13
|
-
from app.db.database import get_db
|
|
14
|
-
|
|
15
|
-
router = APIRouter(tags=["export"])
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@router.get("/trips/{trip_id}/export/pdf")
|
|
19
|
-
async def export_trip_pdf(
|
|
20
|
-
trip_id: UUID,
|
|
21
|
-
uid: Annotated[str, Depends(user_id)],
|
|
22
|
-
db: Annotated[AsyncSession, Depends(get_db)],
|
|
23
|
-
):
|
|
24
|
-
trip_row = await db.execute(
|
|
25
|
-
text("SELECT * FROM trips WHERE id = :id AND user_id = :uid"),
|
|
26
|
-
{"id": str(trip_id), "uid": uid},
|
|
27
|
-
)
|
|
28
|
-
trip = trip_row.fetchone()
|
|
29
|
-
if not trip:
|
|
30
|
-
raise HTTPException(status_code=404, detail="Trip not found")
|
|
31
|
-
trip = dict(trip._mapping)
|
|
32
|
-
|
|
33
|
-
items_row = await db.execute(
|
|
34
|
-
text("SELECT * FROM trip_items WHERE trip_id = :id ORDER BY event_at ASC NULLS LAST"),
|
|
35
|
-
{"id": str(trip_id)},
|
|
36
|
-
)
|
|
37
|
-
items = [dict(r._mapping) for r in items_row.fetchall()]
|
|
38
|
-
|
|
39
|
-
from app.services import telemetry
|
|
40
|
-
await telemetry.track(db, uid, "pdf_export", {"trip_id": str(trip_id), "item_count": len(items)})
|
|
41
|
-
|
|
42
|
-
pdf_bytes = _build_pdf(trip, items)
|
|
43
|
-
filename = f"trip-{trip['name'].replace(' ', '_')[:40]}.pdf"
|
|
44
|
-
return StreamingResponse(
|
|
45
|
-
BytesIO(pdf_bytes),
|
|
46
|
-
media_type="application/pdf",
|
|
47
|
-
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def _build_pdf(trip: dict, items: list[dict]) -> bytes:
|
|
52
|
-
from reportlab.lib.pagesizes import A4
|
|
53
|
-
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
|
54
|
-
from reportlab.lib.units import cm
|
|
55
|
-
from reportlab.lib import colors
|
|
56
|
-
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable
|
|
57
|
-
from reportlab.lib.enums import TA_LEFT
|
|
58
|
-
|
|
59
|
-
buf = BytesIO()
|
|
60
|
-
doc = SimpleDocTemplate(buf, pagesize=A4, leftMargin=2*cm, rightMargin=2*cm, topMargin=2*cm, bottomMargin=2*cm)
|
|
61
|
-
styles = getSampleStyleSheet()
|
|
62
|
-
|
|
63
|
-
PURPLE = colors.HexColor("#4f46e5")
|
|
64
|
-
GRAY = colors.HexColor("#666688")
|
|
65
|
-
|
|
66
|
-
title_style = ParagraphStyle("title", parent=styles["Heading1"], textColor=PURPLE, fontSize=22, spaceAfter=4)
|
|
67
|
-
sub_style = ParagraphStyle("sub", parent=styles["Normal"], textColor=GRAY, fontSize=10, spaceAfter=16)
|
|
68
|
-
h3_style = ParagraphStyle("h3", parent=styles["Heading3"], textColor=PURPLE, fontSize=12, spaceBefore=12, spaceAfter=4)
|
|
69
|
-
body_style = ParagraphStyle("body", parent=styles["Normal"], fontSize=9, leading=13)
|
|
70
|
-
label_style = ParagraphStyle("label", parent=styles["Normal"], fontSize=8, textColor=GRAY)
|
|
71
|
-
|
|
72
|
-
story = []
|
|
73
|
-
story.append(Paragraph(f"✈ {trip['name']}", title_style))
|
|
74
|
-
|
|
75
|
-
date_range = ""
|
|
76
|
-
if trip.get("start_date") and trip.get("end_date"):
|
|
77
|
-
date_range = f"{trip['start_date']} – {trip['end_date']}"
|
|
78
|
-
elif trip.get("start_date"):
|
|
79
|
-
date_range = str(trip["start_date"])
|
|
80
|
-
if date_range:
|
|
81
|
-
story.append(Paragraph(date_range, sub_style))
|
|
82
|
-
if trip.get("description"):
|
|
83
|
-
story.append(Paragraph(trip["description"], body_style))
|
|
84
|
-
story.append(Spacer(1, 0.4*cm))
|
|
85
|
-
story.append(HRFlowable(width="100%", color=PURPLE, thickness=1))
|
|
86
|
-
story.append(Spacer(1, 0.3*cm))
|
|
87
|
-
|
|
88
|
-
if not items:
|
|
89
|
-
story.append(Paragraph("Keine Einträge vorhanden.", body_style))
|
|
90
|
-
else:
|
|
91
|
-
TYPE_EMOJI = {"flight": "✈", "train": "🚆", "bus": "🚌", "hotel": "🏨",
|
|
92
|
-
"rental_car": "🚗", "activity": "🎯", "transfer": "🚕",
|
|
93
|
-
"document": "📄", "other": "📌"}
|
|
94
|
-
for item in items:
|
|
95
|
-
emoji = TYPE_EMOJI.get(item.get("type", "other"), "📌")
|
|
96
|
-
story.append(Paragraph(f"{emoji} {item.get('title', '–')}", h3_style))
|
|
97
|
-
|
|
98
|
-
meta = []
|
|
99
|
-
if item.get("event_at"):
|
|
100
|
-
meta.append(["Datum/Zeit", str(item["event_at"])[:16]])
|
|
101
|
-
if item.get("provider"):
|
|
102
|
-
meta.append(["Anbieter", item["provider"]])
|
|
103
|
-
if item.get("booking_ref"):
|
|
104
|
-
meta.append(["Buchungs-Nr.", item["booking_ref"]])
|
|
105
|
-
|
|
106
|
-
if meta:
|
|
107
|
-
tbl = Table(meta, colWidths=[3.5*cm, 12*cm])
|
|
108
|
-
tbl.setStyle(TableStyle([
|
|
109
|
-
("TEXTCOLOR", (0,0), (0,-1), GRAY),
|
|
110
|
-
("FONTSIZE", (0,0), (-1,-1), 8),
|
|
111
|
-
("BOTTOMPADDING", (0,0), (-1,-1), 2),
|
|
112
|
-
("TOPPADDING", (0,0), (-1,-1), 2),
|
|
113
|
-
]))
|
|
114
|
-
story.append(tbl)
|
|
115
|
-
story.append(Spacer(1, 0.2*cm))
|
|
116
|
-
|
|
117
|
-
story.append(Spacer(1, 1*cm))
|
|
118
|
-
story.append(Paragraph("Erstellt mit Travel Chaos Organizer · tco.app", label_style))
|
|
119
|
-
|
|
120
|
-
doc.build(story)
|
|
121
|
-
buf.seek(0)
|
|
122
|
-
return buf.read()
|