@cachly-dev/mcp-server 0.10.65 → 0.10.66

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.
Files changed (269) hide show
  1. package/dist/src/handlers/brain.js +1 -1
  2. package/dist/src/index.js +1 -1
  3. package/package.json +7 -1
  4. package/server.json +2 -2
  5. package/.env.test.example +0 -23
  6. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -50
  7. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  8. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
  9. package/.github/workflows/eas-build-android.yml +0 -58
  10. package/.github/workflows/eas-update.yml +0 -55
  11. package/.github/workflows/publish.yml +0 -72
  12. package/BENCH.md +0 -113
  13. package/CHANGELOG.md +0 -653
  14. package/Dockerfile +0 -16
  15. package/LAUNCH_TEXTS.md +0 -182
  16. package/PROGRESS.md +0 -141
  17. package/STRATEGY.md +0 -228
  18. package/VISION_10X.md +0 -133
  19. package/apps/travel-chaos-organizer/.env.example +0 -49
  20. package/apps/travel-chaos-organizer/DEPLOY.md +0 -328
  21. package/apps/travel-chaos-organizer/Makefile +0 -57
  22. package/apps/travel-chaos-organizer/README.md +0 -159
  23. package/apps/travel-chaos-organizer/backend/.env.example +0 -10
  24. package/apps/travel-chaos-organizer/backend/Dockerfile +0 -25
  25. package/apps/travel-chaos-organizer/backend/alembic/env.py +0 -56
  26. package/apps/travel-chaos-organizer/backend/alembic/script.py.mako +0 -24
  27. package/apps/travel-chaos-organizer/backend/alembic/versions/.gitkeep +0 -0
  28. package/apps/travel-chaos-organizer/backend/alembic/versions/0001_stripe_customer_id.py +0 -24
  29. package/apps/travel-chaos-organizer/backend/alembic/versions/0002_telegram_chat_id.py +0 -24
  30. package/apps/travel-chaos-organizer/backend/alembic.ini +0 -39
  31. package/apps/travel-chaos-organizer/backend/app/__init__.py +0 -0
  32. package/apps/travel-chaos-organizer/backend/app/auth/__init__.py +0 -0
  33. package/apps/travel-chaos-organizer/backend/app/auth/keycloak.py +0 -81
  34. package/apps/travel-chaos-organizer/backend/app/config.py +0 -66
  35. package/apps/travel-chaos-organizer/backend/app/db/__init__.py +0 -0
  36. package/apps/travel-chaos-organizer/backend/app/db/database.py +0 -20
  37. package/apps/travel-chaos-organizer/backend/app/db/migrations/001_init.sql +0 -71
  38. package/apps/travel-chaos-organizer/backend/app/db/migrations/002_telemetry_freemium.sql +0 -31
  39. package/apps/travel-chaos-organizer/backend/app/db/migrations/003_telegram_chat_id.sql +0 -4
  40. package/apps/travel-chaos-organizer/backend/app/db/migrations/004_stripe_and_email.sql +0 -5
  41. package/apps/travel-chaos-organizer/backend/app/limiter.py +0 -4
  42. package/apps/travel-chaos-organizer/backend/app/logging_config.py +0 -25
  43. package/apps/travel-chaos-organizer/backend/app/main.py +0 -77
  44. package/apps/travel-chaos-organizer/backend/app/models/__init__.py +0 -0
  45. package/apps/travel-chaos-organizer/backend/app/models/schemas.py +0 -142
  46. package/apps/travel-chaos-organizer/backend/app/routers/__init__.py +0 -0
  47. package/apps/travel-chaos-organizer/backend/app/routers/admin.py +0 -237
  48. package/apps/travel-chaos-organizer/backend/app/routers/cache.py +0 -13
  49. package/apps/travel-chaos-organizer/backend/app/routers/events.py +0 -29
  50. package/apps/travel-chaos-organizer/backend/app/routers/export.py +0 -122
  51. package/apps/travel-chaos-organizer/backend/app/routers/inbox.py +0 -122
  52. package/apps/travel-chaos-organizer/backend/app/routers/items.py +0 -130
  53. package/apps/travel-chaos-organizer/backend/app/routers/mail.py +0 -108
  54. package/apps/travel-chaos-organizer/backend/app/routers/parse.py +0 -303
  55. package/apps/travel-chaos-organizer/backend/app/routers/payments.py +0 -179
  56. package/apps/travel-chaos-organizer/backend/app/routers/trips.py +0 -134
  57. package/apps/travel-chaos-organizer/backend/app/routers/users.py +0 -169
  58. package/apps/travel-chaos-organizer/backend/app/routers/waitlist.py +0 -37
  59. package/apps/travel-chaos-organizer/backend/app/services/__init__.py +0 -0
  60. package/apps/travel-chaos-organizer/backend/app/services/cache.py +0 -131
  61. package/apps/travel-chaos-organizer/backend/app/services/drip.py +0 -128
  62. package/apps/travel-chaos-organizer/backend/app/services/email.py +0 -61
  63. package/apps/travel-chaos-organizer/backend/app/services/errors.py +0 -44
  64. package/apps/travel-chaos-organizer/backend/app/services/notifier.py +0 -10
  65. package/apps/travel-chaos-organizer/backend/app/services/ollama.py +0 -101
  66. package/apps/travel-chaos-organizer/backend/app/services/parser.py +0 -22
  67. package/apps/travel-chaos-organizer/backend/app/services/quota.py +0 -85
  68. package/apps/travel-chaos-organizer/backend/app/services/sentry.py +0 -11
  69. package/apps/travel-chaos-organizer/backend/app/services/telemetry.py +0 -35
  70. package/apps/travel-chaos-organizer/backend/pytest.ini +0 -4
  71. package/apps/travel-chaos-organizer/backend/requirements-dev.txt +0 -6
  72. package/apps/travel-chaos-organizer/backend/requirements.txt +0 -25
  73. package/apps/travel-chaos-organizer/backend/tests/__init__.py +0 -0
  74. package/apps/travel-chaos-organizer/backend/tests/conftest.py +0 -223
  75. package/apps/travel-chaos-organizer/backend/tests/test_admin.py +0 -99
  76. package/apps/travel-chaos-organizer/backend/tests/test_cache.py +0 -142
  77. package/apps/travel-chaos-organizer/backend/tests/test_drip.py +0 -32
  78. package/apps/travel-chaos-organizer/backend/tests/test_e2e_chain.py +0 -121
  79. package/apps/travel-chaos-organizer/backend/tests/test_export.py +0 -48
  80. package/apps/travel-chaos-organizer/backend/tests/test_inbox.py +0 -107
  81. package/apps/travel-chaos-organizer/backend/tests/test_items.py +0 -138
  82. package/apps/travel-chaos-organizer/backend/tests/test_mail.py +0 -113
  83. package/apps/travel-chaos-organizer/backend/tests/test_ollama.py +0 -145
  84. package/apps/travel-chaos-organizer/backend/tests/test_parse_url.py +0 -98
  85. package/apps/travel-chaos-organizer/backend/tests/test_parser.py +0 -72
  86. package/apps/travel-chaos-organizer/backend/tests/test_payments.py +0 -163
  87. package/apps/travel-chaos-organizer/backend/tests/test_quota.py +0 -46
  88. package/apps/travel-chaos-organizer/backend/tests/test_search.py +0 -67
  89. package/apps/travel-chaos-organizer/backend/tests/test_trips.py +0 -123
  90. package/apps/travel-chaos-organizer/backend/tests/test_users.py +0 -52
  91. package/apps/travel-chaos-organizer/backups/.gitkeep +0 -0
  92. package/apps/travel-chaos-organizer/caddy/Caddyfile +0 -27
  93. package/apps/travel-chaos-organizer/docker-compose.node1.yml +0 -31
  94. package/apps/travel-chaos-organizer/docker-compose.override.yml +0 -14
  95. package/apps/travel-chaos-organizer/docker-compose.yml +0 -128
  96. package/apps/travel-chaos-organizer/frontend/.env.example +0 -13
  97. package/apps/travel-chaos-organizer/frontend/app/(app)/_layout.tsx +0 -80
  98. package/apps/travel-chaos-organizer/frontend/app/(app)/inbox.tsx +0 -166
  99. package/apps/travel-chaos-organizer/frontend/app/(app)/search.tsx +0 -130
  100. package/apps/travel-chaos-organizer/frontend/app/(app)/settings.tsx +0 -381
  101. package/apps/travel-chaos-organizer/frontend/app/(app)/trips/[id].tsx +0 -346
  102. package/apps/travel-chaos-organizer/frontend/app/(app)/trips.tsx +0 -363
  103. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/cancel.tsx +0 -34
  104. package/apps/travel-chaos-organizer/frontend/app/(app)/upgrade/success.tsx +0 -35
  105. package/apps/travel-chaos-organizer/frontend/app/(auth)/login.tsx +0 -91
  106. package/apps/travel-chaos-organizer/frontend/app/_layout.tsx +0 -71
  107. package/apps/travel-chaos-organizer/frontend/app/auth.tsx +0 -29
  108. package/apps/travel-chaos-organizer/frontend/app/index.tsx +0 -16
  109. package/apps/travel-chaos-organizer/frontend/app/onboarding.tsx +0 -119
  110. package/apps/travel-chaos-organizer/frontend/app.config.ts +0 -54
  111. package/apps/travel-chaos-organizer/frontend/assets/adaptive-icon.svg +0 -66
  112. package/apps/travel-chaos-organizer/frontend/assets/icon.svg +0 -79
  113. package/apps/travel-chaos-organizer/frontend/assets/splash.svg +0 -15
  114. package/apps/travel-chaos-organizer/frontend/components/BottomSheet.tsx +0 -102
  115. package/apps/travel-chaos-organizer/frontend/components/ConfettiCannon.tsx +0 -107
  116. package/apps/travel-chaos-organizer/frontend/components/ConfirmDialog.tsx +0 -109
  117. package/apps/travel-chaos-organizer/frontend/components/ErrorBoundary.tsx +0 -45
  118. package/apps/travel-chaos-organizer/frontend/components/FileUploadButton.tsx +0 -19
  119. package/apps/travel-chaos-organizer/frontend/components/ImportSheet.tsx +0 -155
  120. package/apps/travel-chaos-organizer/frontend/components/OfflineBadge.tsx +0 -55
  121. package/apps/travel-chaos-organizer/frontend/components/QRCodeViewer.tsx +0 -80
  122. package/apps/travel-chaos-organizer/frontend/components/ScoreRing.tsx +0 -39
  123. package/apps/travel-chaos-organizer/frontend/components/Screen.tsx +0 -39
  124. package/apps/travel-chaos-organizer/frontend/components/Skeleton.tsx +0 -70
  125. package/apps/travel-chaos-organizer/frontend/components/SwipeableInboxItem.tsx +0 -121
  126. package/apps/travel-chaos-organizer/frontend/components/SwipeableTimelineItem.tsx +0 -70
  127. package/apps/travel-chaos-organizer/frontend/components/TimelineItem.tsx +0 -73
  128. package/apps/travel-chaos-organizer/frontend/components/TimelineItemDetail.tsx +0 -130
  129. package/apps/travel-chaos-organizer/frontend/components/Toast.tsx +0 -60
  130. package/apps/travel-chaos-organizer/frontend/components/ToastContext.tsx +0 -42
  131. package/apps/travel-chaos-organizer/frontend/components/TripCard.tsx +0 -146
  132. package/apps/travel-chaos-organizer/frontend/components/TripQRModal.tsx +0 -70
  133. package/apps/travel-chaos-organizer/frontend/components/UpgradeBanner.tsx +0 -44
  134. package/apps/travel-chaos-organizer/frontend/eas.json +0 -22
  135. package/apps/travel-chaos-organizer/frontend/hooks/useInbox.ts +0 -24
  136. package/apps/travel-chaos-organizer/frontend/hooks/useNetworkSync.ts +0 -60
  137. package/apps/travel-chaos-organizer/frontend/hooks/useTrips.ts +0 -112
  138. package/apps/travel-chaos-organizer/frontend/lib/api.ts +0 -223
  139. package/apps/travel-chaos-organizer/frontend/lib/auth.ts +0 -93
  140. package/apps/travel-chaos-organizer/frontend/lib/db.ts +0 -86
  141. package/apps/travel-chaos-organizer/frontend/lib/fileCache.ts +0 -54
  142. package/apps/travel-chaos-organizer/frontend/lib/haptics.ts +0 -13
  143. package/apps/travel-chaos-organizer/frontend/lib/notifications.ts +0 -112
  144. package/apps/travel-chaos-organizer/frontend/lib/offlineQueue.ts +0 -145
  145. package/apps/travel-chaos-organizer/frontend/lib/quota.tsx +0 -43
  146. package/apps/travel-chaos-organizer/frontend/lib/sentry.ts +0 -28
  147. package/apps/travel-chaos-organizer/frontend/lib/shareIntent.ts +0 -44
  148. package/apps/travel-chaos-organizer/frontend/package-lock.json +0 -13221
  149. package/apps/travel-chaos-organizer/frontend/package.json +0 -44
  150. package/apps/travel-chaos-organizer/frontend/tsconfig.json +0 -23
  151. package/apps/travel-chaos-organizer/keycloak/realm-export.json +0 -59
  152. package/apps/travel-chaos-organizer/landing/index.html +0 -773
  153. package/apps/travel-chaos-organizer/scripts/backup.sh +0 -14
  154. package/apps/travel-chaos-organizer/scripts/configure-env.sh +0 -82
  155. package/apps/travel-chaos-organizer/scripts/deploy.sh +0 -54
  156. package/apps/travel-chaos-organizer/scripts/health-check.sh +0 -36
  157. package/apps/travel-chaos-organizer/scripts/migrate.sh +0 -7
  158. package/apps/travel-chaos-organizer/scripts/server-setup.sh +0 -85
  159. package/apps/travel-chaos-organizer/scripts/smoke-test.sh +0 -29
  160. package/apps/travel-chaos-organizer/scripts/verify-tracking.sh +0 -71
  161. package/apps/travel-chaos-organizer/telegram-bot/.env.example +0 -3
  162. package/apps/travel-chaos-organizer/telegram-bot/Dockerfile +0 -6
  163. package/apps/travel-chaos-organizer/telegram-bot/bot.py +0 -504
  164. package/apps/travel-chaos-organizer/telegram-bot/requirements.txt +0 -3
  165. package/dist/src/__tests__/auth.test.d.ts +0 -1
  166. package/dist/src/__tests__/auth.test.js +0 -107
  167. package/dist/src/__tests__/auth.test.js.map +0 -1
  168. package/dist/src/__tests__/brain-flow.test.d.ts +0 -12
  169. package/dist/src/__tests__/brain-flow.test.js +0 -1241
  170. package/dist/src/__tests__/brain-flow.test.js.map +0 -1
  171. package/dist/src/__tests__/confidence.test.d.ts +0 -13
  172. package/dist/src/__tests__/confidence.test.js +0 -233
  173. package/dist/src/__tests__/confidence.test.js.map +0 -1
  174. package/dist/src/__tests__/e2e/api-auth.test.d.ts +0 -7
  175. package/dist/src/__tests__/e2e/api-auth.test.js +0 -162
  176. package/dist/src/__tests__/e2e/api-auth.test.js.map +0 -1
  177. package/dist/src/__tests__/e2e/api-health.test.d.ts +0 -7
  178. package/dist/src/__tests__/e2e/api-health.test.js +0 -95
  179. package/dist/src/__tests__/e2e/api-health.test.js.map +0 -1
  180. package/dist/src/__tests__/e2e/cli.test.d.ts +0 -10
  181. package/dist/src/__tests__/e2e/cli.test.js +0 -204
  182. package/dist/src/__tests__/e2e/cli.test.js.map +0 -1
  183. package/dist/src/__tests__/e2e/config.d.ts +0 -35
  184. package/dist/src/__tests__/e2e/config.js +0 -53
  185. package/dist/src/__tests__/e2e/config.js.map +0 -1
  186. package/dist/src/__tests__/e2e/funnel.test.d.ts +0 -13
  187. package/dist/src/__tests__/e2e/funnel.test.js +0 -90
  188. package/dist/src/__tests__/e2e/funnel.test.js.map +0 -1
  189. package/dist/src/__tests__/e2e/mcp-tools.test.d.ts +0 -7
  190. package/dist/src/__tests__/e2e/mcp-tools.test.js +0 -129
  191. package/dist/src/__tests__/e2e/mcp-tools.test.js.map +0 -1
  192. package/dist/src/__tests__/e2e/provisioning.test.d.ts +0 -15
  193. package/dist/src/__tests__/e2e/provisioning.test.js +0 -123
  194. package/dist/src/__tests__/e2e/provisioning.test.js.map +0 -1
  195. package/dist/src/__tests__/handlers-unit.test.d.ts +0 -12
  196. package/dist/src/__tests__/handlers-unit.test.js +0 -505
  197. package/dist/src/__tests__/handlers-unit.test.js.map +0 -1
  198. package/dist/src/__tests__/namespace.test.d.ts +0 -1
  199. package/dist/src/__tests__/namespace.test.js +0 -114
  200. package/dist/src/__tests__/namespace.test.js.map +0 -1
  201. package/dist/src/__tests__/rerank.test.d.ts +0 -1
  202. package/dist/src/__tests__/rerank.test.js +0 -125
  203. package/dist/src/__tests__/rerank.test.js.map +0 -1
  204. package/dist/src/__tests__/utils.test.d.ts +0 -1
  205. package/dist/src/__tests__/utils.test.js +0 -83
  206. package/dist/src/__tests__/utils.test.js.map +0 -1
  207. package/dist/src/bench/cachly-bench.d.ts +0 -39
  208. package/dist/src/bench/cachly-bench.js +0 -223
  209. package/dist/src/bench/cachly-bench.js.map +0 -1
  210. package/dist/src/bench/fixtures.d.ts +0 -37
  211. package/dist/src/bench/fixtures.js +0 -225
  212. package/dist/src/bench/fixtures.js.map +0 -1
  213. package/dist/src/search.test.d.ts +0 -1
  214. package/dist/src/search.test.js +0 -713
  215. package/dist/src/search.test.js.map +0 -1
  216. package/eslint.config.js +0 -16
  217. package/glama.json +0 -36
  218. package/llms.txt +0 -155
  219. package/packages/telegram-notify/SPLIT_PLAN.md +0 -106
  220. package/packages/telegram-notify/client.ts +0 -88
  221. package/packages/telegram-notify/emojis.json +0 -22
  222. package/packages/telegram-notify/pyproject.toml +0 -17
  223. package/packages/telegram-notify/telegram_notify/__init__.py +0 -3
  224. package/packages/telegram-notify/telegram_notify/client.py +0 -87
  225. package/packages/telegram-notify/telegram_notify/emojis.json +0 -22
  226. package/scripts/e2e-run.sh +0 -160
  227. package/scripts/generate-icons.js +0 -28
  228. package/smithery.yaml +0 -45
  229. package/src/__tests__/auth.test.ts +0 -147
  230. package/src/__tests__/brain-flow.test.ts +0 -1472
  231. package/src/__tests__/confidence.test.ts +0 -285
  232. package/src/__tests__/e2e/api-auth.test.ts +0 -182
  233. package/src/__tests__/e2e/api-health.test.ts +0 -103
  234. package/src/__tests__/e2e/cli.test.ts +0 -228
  235. package/src/__tests__/e2e/config.ts +0 -53
  236. package/src/__tests__/e2e/funnel.test.ts +0 -99
  237. package/src/__tests__/e2e/mcp-tools.test.ts +0 -191
  238. package/src/__tests__/e2e/provisioning.test.ts +0 -128
  239. package/src/__tests__/handlers-unit.test.ts +0 -677
  240. package/src/__tests__/namespace.test.ts +0 -126
  241. package/src/__tests__/rerank.test.ts +0 -141
  242. package/src/__tests__/utils.test.ts +0 -81
  243. package/src/auth.ts +0 -46
  244. package/src/bench/cachly-bench.ts +0 -263
  245. package/src/bench/fixtures.ts +0 -256
  246. package/src/ckg.ts +0 -103
  247. package/src/confidence.ts +0 -38
  248. package/src/embeddings.ts +0 -217
  249. package/src/handlers/advanced.ts +0 -647
  250. package/src/handlers/brain.ts +0 -2622
  251. package/src/handlers/cache.ts +0 -633
  252. package/src/handlers/context.ts +0 -222
  253. package/src/handlers/fedbrain.ts +0 -1232
  254. package/src/handlers/instances.ts +0 -180
  255. package/src/handlers/roadmap.ts +0 -201
  256. package/src/handlers/syndicate.ts +0 -460
  257. package/src/handlers/tco.ts +0 -345
  258. package/src/handlers/team.ts +0 -1249
  259. package/src/index.ts +0 -2815
  260. package/src/namespace.ts +0 -128
  261. package/src/notifier.ts +0 -5
  262. package/src/rerank.ts +0 -148
  263. package/src/search.test.ts +0 -848
  264. package/src/search.ts +0 -1781
  265. package/src/tools.ts +0 -2056
  266. package/src/utils.ts +0 -90
  267. package/test-features.cjs +0 -23
  268. package/tsconfig.json +0 -16
  269. 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
- }
@@ -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
@@ -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()