Merge branch 'main' into dependabot/npm_and_yarn/production-e9efbc881e

This commit is contained in:
Ullrich Schäfer 2026-06-10 01:28:40 +02:00 committed by GitHub
commit 8eeb775f54
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
122 changed files with 4617 additions and 190 deletions

View file

@ -144,9 +144,12 @@ export type Audience = "public" | "followers-only";
export const activities = journalSchema.table("activities", {
id: text("id").primaryKey(),
ownerId: text("owner_id")
.notNull()
.references(() => users.id),
// Local author. NULL for activities ingested from a remote trails
// actor's outbox, where `remoteActorIri` identifies the author —
// exactly one of the two is set (check constraint below; same
// pattern as follows.follower_id / follower_actor_iri). Resolves
// the owner_id open question in social-federation design.md.
ownerId: text("owner_id").references(() => users.id),
routeId: text("route_id").references(() => routes.id),
name: text("name").notNull(),
description: text("description").default(""),
@ -164,16 +167,25 @@ export const activities = journalSchema.table("activities", {
// Federation provenance (spec: social-federation). NULL for local
// activities. For rows ingested from a remote trails actor's outbox:
// `remoteOriginIri` is the activity's IRI on the origin instance
// (unique → replay-safe `ON CONFLICT DO NOTHING` ingestion), and
// `remoteActorIri` keys into `remote_actors`.
// (unique → replay-safe `ON CONFLICT DO NOTHING` ingestion),
// `remoteActorIri` keys into `remote_actors`, and
// `remotePublishedAt` carries the origin's publish time (feed sort
// uses COALESCE(remote_published_at, created_at)).
remoteOriginIri: text("remote_origin_iri").unique(),
remoteActorIri: text("remote_actor_iri"),
remotePublishedAt: timestamp("remote_published_at", { withTimezone: true }),
audience: text("audience").$type<Audience>().notNull().default("public"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
}, (t) => ({
// Hot paths: listActivities (sort by started_at) and listPublicActivitiesForOwner.
ownerStartedIdx: index("activities_owner_started_idx").on(t.ownerId, t.startedAt.desc()),
ownerCreatedIdx: index("activities_owner_created_idx").on(t.ownerId, t.createdAt.desc()),
// Feed join for remote rows (spec §8).
remoteActorIdx: index("activities_remote_actor_idx").on(t.remoteActorIri),
hasAuthorCheck: check(
"activities_has_author_check",
sql`(${t.ownerId} IS NOT NULL) <> (${t.remoteActorIri} IS NOT NULL)`,
),
}));
// --- OAuth2 PKCE (mobile app auth) ---
@ -328,6 +340,11 @@ export const importBatches = journalSchema.table("import_batches", {
importedCount: integer("imported_count").notNull().default(0),
duplicateCount: integer("duplicate_count").notNull().default(0),
errorMessage: text("error_message"),
// Ranged backfill requests (Garmin): the activity time window this
// batch asked the provider to re-deliver. NULL for pick-list style
// imports (komoot bulk) that aren't range-shaped.
rangeStart: timestamp("range_start", { withTimezone: true }),
rangeEnd: timestamp("range_end", { withTimezone: true }),
startedAt: timestamp("started_at", { withTimezone: true }).notNull().defaultNow(),
completedAt: timestamp("completed_at", { withTimezone: true }),
});

View file

@ -192,7 +192,7 @@ export default {
},
federation: {
title: "Föderiert by design",
body: "Deine Journal-Instanz spricht via ActivityPub mit anderen trails.cool-Servern. Folge Freundinnen und Freunden überall.",
body: "Teil des Fediverse via ActivityPub: Mastodon-Nutzer:innen können dir folgen und deine öffentlichen Aktivitäten sehen, und du folgst trails-Nutzer:innen auf jeder Instanz.",
},
ownership: {
title: "Deine Daten, immer",
@ -321,6 +321,19 @@ export default {
count_other: "Folgt {{count}}",
empty: "Folgt noch niemandem.",
},
outgoing: {
heading: "Folgen über Instanzen hinweg",
subtitle: "Folge Leuten auf anderen trails.cool-Instanzen. Anfragen bleiben ausstehend, bis ihre Instanz sie annimmt.",
handleLabel: "Handle der Person",
followButton: "Folgen",
pendingBadge: "Ausstehend",
acceptedBadge: "Folge ich",
cancelRequest: "Anfrage zurückziehen",
unfollow: "Entfolgen",
empty: "Noch keine Follows auf anderen Instanzen. Gib oben ein Handle ein, um zu starten.",
notTrails: "Ausgehende Föderation ist derzeit nur zwischen trails-Instanzen möglich — diese Instanz nutzt kein trails.cool.",
disabled: "Föderation ist auf dieser Instanz nicht aktiviert.",
},
prevPage: "Zurück",
nextPage: "Weiter",
pageOfTotal: "Seite {{page}} von {{totalPages}}",
@ -328,6 +341,7 @@ export default {
title: "Feed",
heading: "Folge ich",
empty: "Du folgst noch niemandem. Stöbere Profile durch und klicke auf Folgen, um deinen Feed aufzubauen.",
followRemote: "Folge jemandem auf einer anderen trails-Instanz →",
seePublic: "Oder durchstöbere den öffentlichen Feed dieser Instanz →",
toggle: {
followed: "Folge ich",
@ -464,6 +478,29 @@ export default {
},
},
sync: {
garmin: {
title: "Von Garmin importieren",
subtitle:
"Garmin liefert Aktivitäten automatisch an trails.cool, sobald sie entstehen. Für ältere Aktivitäten fordere unten einen Zeitraum an — Garmin liefert sie asynchron nach.",
notConnected: "Dein Garmin-Konto ist noch nicht verbunden.",
goConnect: "Garmin in den Einstellungen verbinden",
needsRelink: "Deine Garmin-Verbindung muss neu verknüpft werden, bevor du Importe anfordern kannst.",
from: "Von",
to: "Bis",
request: "Import anfordern",
requesting: "Wird angefordert…",
requested: "Angefordert! Aktivitäten erscheinen, sobald Garmin sie liefert.",
asyncNote:
"Garmin verarbeitet Verlaufs-Anfragen auf ihrer Seite — große Zeiträume können dauern. Überlappende Zeiträume erneut anzufordern ist unproblematisch; nichts wird doppelt importiert.",
importedSince_one: "{{count}} Aktivität seit dieser Anfrage importiert",
importedSince_other: "{{count}} Aktivitäten seit dieser Anfrage importiert",
errors: {
not_connected: "Dein Garmin-Konto ist nicht verbunden.",
needs_relink: "Deine Garmin-Verbindung muss zuerst neu verknüpft werden.",
invalid_range: "Wähle einen gültigen Zeitraum in der Vergangenheit (Start vor Ende).",
request_failed: "Garmin hat die Anfrage abgelehnt — versuche es in ein paar Minuten erneut.",
},
},
import: "Importieren",
importFrom: "Import von {{provider}}",
imported: "Importiert",

View file

@ -192,7 +192,7 @@ export default {
},
federation: {
title: "Federated by design",
body: "Your Journal instance talks to other trails.cool servers via ActivityPub. Follow friends anywhere.",
body: "Part of the fediverse via ActivityPub: Mastodon users can follow you and see your public activities, and you can follow trails users on any instance.",
},
ownership: {
title: "Your data, always",
@ -321,6 +321,19 @@ export default {
count_other: "Following {{count}}",
empty: "Not following anyone yet.",
},
outgoing: {
heading: "Following across instances",
subtitle: "Follow people on other trails.cool instances. Requests stay pending until their instance accepts.",
handleLabel: "Their handle",
followButton: "Follow",
pendingBadge: "Pending",
acceptedBadge: "Following",
cancelRequest: "Cancel request",
unfollow: "Unfollow",
empty: "No follows on other instances yet. Enter a handle above to start.",
notTrails: "Outbound federation is currently trails-to-trails only — that instance doesn't run trails.cool.",
disabled: "Federation is not enabled on this instance.",
},
prevPage: "Previous",
nextPage: "Next",
pageOfTotal: "Page {{page}} of {{totalPages}}",
@ -328,6 +341,7 @@ export default {
title: "Feed",
heading: "Following",
empty: "You're not following anyone yet. Browse profiles and tap Follow to start building your feed.",
followRemote: "Follow someone on another trails instance →",
seePublic: "Or browse the instance public feed →",
toggle: {
followed: "Following",
@ -464,6 +478,29 @@ export default {
},
},
sync: {
garmin: {
title: "Import from Garmin",
subtitle:
"Garmin delivers activities to trails.cool as they happen. To pull in your history, request a date range below — Garmin sends those activities over asynchronously.",
notConnected: "Your Garmin account isn't connected yet.",
goConnect: "Connect Garmin in settings",
needsRelink: "Your Garmin connection needs to be re-linked before requesting imports.",
from: "From",
to: "To",
request: "Request import",
requesting: "Requesting…",
requested: "Requested! Activities will appear as Garmin delivers them.",
asyncNote:
"Garmin processes history requests on their side — large ranges can take a while to arrive. Re-requesting an overlapping range is safe; nothing gets imported twice.",
importedSince_one: "{{count}} activity imported since this request",
importedSince_other: "{{count}} activities imported since this request",
errors: {
not_connected: "Your Garmin account isn't connected.",
needs_relink: "Your Garmin connection needs to be re-linked first.",
invalid_range: "Pick a valid date range in the past (start before end).",
request_failed: "Garmin rejected the request — try again in a few minutes.",
},
},
import: "Import",
importFrom: "Import from {{provider}}",
imported: "Imported",