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