WIP: Komoot import integration
Add Komoot API client, import logic, crypto helpers, integration routes, DB schema changes, and i18n strings for the Komoot import feature. Import processing runs as a durable pg-boss background job with retries instead of blocking the HTTP request. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
32c5fbde8f
commit
5fd60ba07d
24 changed files with 982 additions and 51 deletions
|
|
@ -70,6 +70,7 @@ export const routes = journalSchema.table("routes", {
|
|||
elevationLoss: real("elevation_loss"),
|
||||
dayBreaks: jsonb("day_breaks").$type<number[]>(),
|
||||
tags: jsonb("tags").$type<string[]>(),
|
||||
source: text("source"),
|
||||
plannerState: bytea("planner_state"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
|
|
@ -160,9 +161,27 @@ export const syncConnections = journalSchema.table("sync_connections", {
|
|||
refreshToken: text("refresh_token").notNull(),
|
||||
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
|
||||
providerUserId: text("provider_user_id"),
|
||||
encryptedCredentials: text("encrypted_credentials"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const importBatches = journalSchema.table("import_batches", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
connectionId: text("connection_id")
|
||||
.notNull()
|
||||
.references(() => syncConnections.id, { onDelete: "cascade" }),
|
||||
status: text("status").notNull().default("running"),
|
||||
totalFound: integer("total_found").default(0),
|
||||
importedCount: integer("imported_count").default(0),
|
||||
duplicateCount: integer("duplicate_count").default(0),
|
||||
errorMessage: text("error_message"),
|
||||
startedAt: timestamp("started_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
completedAt: timestamp("completed_at", { withTimezone: true }),
|
||||
});
|
||||
|
||||
export const syncImports = journalSchema.table("sync_imports", {
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id")
|
||||
|
|
|
|||
|
|
@ -241,9 +241,34 @@ export default {
|
|||
previous: "Zurück",
|
||||
next: "Weiter",
|
||||
},
|
||||
integrations: {
|
||||
title: "Integrationen",
|
||||
subtitle: "Externe Dienste verbinden, um deine Daten zu importieren.",
|
||||
import: "Touren importieren",
|
||||
importing: "Importiere...",
|
||||
disconnect: "Trennen",
|
||||
importRunning: "Import läuft...",
|
||||
importComplete: "Import abgeschlossen!",
|
||||
importFailed: "Import fehlgeschlagen.",
|
||||
totalFound: "Touren gefunden: {{count}}",
|
||||
importedCount: "Importiert: {{count}}",
|
||||
duplicateCount: "Übersprungen (bereits importiert): {{count}}",
|
||||
komoot: {
|
||||
description: "Importiere deine aufgezeichneten Touren von Komoot.",
|
||||
email: "Komoot E-Mail",
|
||||
password: "Komoot Passwort",
|
||||
connect: "Komoot verbinden",
|
||||
connectedAs: "Verbunden als {{username}}",
|
||||
since: "Seit {{date}}",
|
||||
whyPasswordTitle: "Warum brauchen wir dein Passwort?",
|
||||
whyPasswordBody: "Komoot bietet keine öffentliche API oder OAuth-Anmeldung an. Deine Zugangsdaten werden benötigt, um auf deine Tourdaten zuzugreifen.",
|
||||
howStored: "Dein Passwort wird verschlüsselt (AES-256-GCM) auf dem Server gespeichert und nur beim Importieren entschlüsselt. Beim Trennen werden deine Zugangsdaten dauerhaft gelöscht.",
|
||||
},
|
||||
},
|
||||
nav: {
|
||||
routes: "Routen",
|
||||
activities: "Aktivitäten",
|
||||
integrations: "Integrationen",
|
||||
login: "Anmelden",
|
||||
register: "Registrieren",
|
||||
profile: "Profil",
|
||||
|
|
@ -272,6 +297,8 @@ export default {
|
|||
passkeyNotSupported: "Dein Browser unterstützt keine Passkeys. Bitte verwende einen anderen Browser zur Registrierung.",
|
||||
passkeyNotSupportedRegister: "Dein Browser unterstützt keine Passkeys. Du kannst dich stattdessen per Magic Link registrieren und später einen Passkey hinzufügen.",
|
||||
registerWithMagicLink: "Per Magic Link registrieren",
|
||||
useMagicLinkInstead: "Stattdessen einen Magic Link verwenden",
|
||||
usePasskeyInstead: "Stattdessen einen Passkey verwenden",
|
||||
},
|
||||
},
|
||||
mobile: {
|
||||
|
|
|
|||
|
|
@ -241,9 +241,34 @@ export default {
|
|||
previous: "Previous",
|
||||
next: "Next",
|
||||
},
|
||||
integrations: {
|
||||
title: "Integrations",
|
||||
subtitle: "Connect external services to import your data.",
|
||||
import: "Import Tours",
|
||||
importing: "Importing...",
|
||||
disconnect: "Disconnect",
|
||||
importRunning: "Import in progress...",
|
||||
importComplete: "Import complete!",
|
||||
importFailed: "Import failed.",
|
||||
totalFound: "Tours found: {{count}}",
|
||||
importedCount: "Imported: {{count}}",
|
||||
duplicateCount: "Skipped (already imported): {{count}}",
|
||||
komoot: {
|
||||
description: "Import your recorded tours from Komoot.",
|
||||
email: "Komoot Email",
|
||||
password: "Komoot Password",
|
||||
connect: "Connect Komoot",
|
||||
connectedAs: "Connected as {{username}}",
|
||||
since: "Since {{date}}",
|
||||
whyPasswordTitle: "Why do we need your password?",
|
||||
whyPasswordBody: "Komoot does not offer a public API or OAuth login. Your credentials are required to access your tour data.",
|
||||
howStored: "Your password is encrypted (AES-256-GCM) on the server and only decrypted when importing tours. Disconnecting permanently deletes your stored credentials.",
|
||||
},
|
||||
},
|
||||
nav: {
|
||||
routes: "Routes",
|
||||
activities: "Activities",
|
||||
integrations: "Integrations",
|
||||
login: "Sign In",
|
||||
register: "Register",
|
||||
profile: "Profile",
|
||||
|
|
@ -272,6 +297,8 @@ export default {
|
|||
passkeyNotSupported: "Your browser does not support passkeys. Please use a different browser to register.",
|
||||
passkeyNotSupportedRegister: "Your browser doesn't support passkeys. You can register with a magic link instead and add a passkey later from a supported browser.",
|
||||
registerWithMagicLink: "Register with Magic Link",
|
||||
useMagicLinkInstead: "Use a magic link instead",
|
||||
usePasskeyInstead: "Use a passkey instead",
|
||||
},
|
||||
},
|
||||
mobile: {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { JobDefinition } from "./types.ts";
|
|||
|
||||
export async function startWorker(
|
||||
boss: PgBoss,
|
||||
jobs: JobDefinition[],
|
||||
jobs: JobDefinition<any>[],
|
||||
): Promise<void> {
|
||||
await boss.start();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue