feat(sync): notify on enable, change defaults to daily and designer profiles

This commit is contained in:
Tautvydas Derzinskas committed 2026-10-12 04:08:24 +02:00
1 parent acfa844ebe
commit 9334438a52
11 files changed
+60 -15

No files matched your search

+1 -1
View File
@@ -285,7 +285,7 @@ out as the backend's user, so `PUID`/`PGID` need write access to the folder. The
## Synced Collections
Link a MakerWorld, Printables or Thingiverse collection to Thingport once, with one tick in Thingport Grab, and every
model added to it later is imported into your library on its own. Thingport checks it every hour, 6 hours or 24 hours,
model added to it later is imported into your library on its own. Thingport checks it once a day, or every 6 hours or every hour,
imports new models one at a time and sends a notification saying what came in. Sync only ever adds: nothing is deleted
on either side. See the **[synced collections guide](docs/guides/synced-collections.md)**.
@@ -0,0 +1,2 @@
-- New syncs are checked daily. Existing syncs keep their interval.
ALTER TABLE "CollectionSync" ALTER COLUMN "intervalHours" SET DEFAULT 24;
+2 -1
View File
@@ -208,9 +208,10 @@ model CollectionSync {
externalId String
sourceUrl String
// MakerWorld only: which print profiles a new model brings ("url" is the model's default one).
// New MakerWorld syncs start on "designer" (see createSync).
profileScope String @default("url")
// How often the provider's collection is checked: 1, 6 or 24.
intervalHours Int @default(1)
intervalHours Int @default(24)
// Every model id the provider's collection has listed, so a sync only imports what's new since.
knownIds String[] @default([])
// Checks in a row that found the provider's collection gone. One bad answer doesn't unsync it.
@@ -4,7 +4,7 @@
import { COLLECTION_SYNC_ENABLED } from "../config";
/** The intervals a collection can pick, in hours. The shortest is also the default. */
/** The intervals a collection can pick, in hours. New syncs get 24 (see CollectionSync.intervalHours). */
export const SYNC_INTERVAL_HOURS = [1, 6, 24] as const;
export type SyncIntervalHours = (typeof SYNC_INTERVAL_HOURS)[number];
@@ -10,6 +10,7 @@ import { parseMakerworldCollectionUrl } from "./makerworldCollections";
import { parsePrintablesCollectionUrl } from "./printablesApi";
import { parseThingiverseCollectionUrl, parseThingiverseLikesUrl } from "./thingiverseApi";
import { createLog } from "./auditLog";
import { createNotification } from "./notificationService";
import type { SyncIntervalHours } from "./collectionSyncSchedule";
import {
LIKES_ID_PREFIX,
@@ -206,9 +207,38 @@ export async function enableSync(userId: string, input: EnableSyncInput): Promis
targetId: created.collectionId,
details: { name: created.collection.name, provider: source.provider, url: source.url },
});
// The sync exists by now, so a failed notification mustn't fail the request: a retry finds the
// sync and wouldn't send it either.
await notifySyncEnabled(created).catch((err) => console.error("[sync] enable notification failed", err));
return toSyncLinkOut(created);
}
const INTERVAL_PHRASES: Record<number, string> = { 1: "every hour", 6: "every 6 hours", 24: "once a day" };
const PROFILE_SCOPE_PHRASES: Record<string, string> = {
url: "its default print profile",
designer: "all of the designer's print profiles",
all: "all of its print profiles, community ones included",
};
/** Confirms in the app what was switched on from Thingport Grab, with the defaults it got. */
async function notifySyncEnabled(sync: CollectionSync & { collection: Collection }): Promise<void> {
const name = sync.collection.name;
const interval = INTERVAL_PHRASES[sync.intervalHours] ?? `every ${sync.intervalHours} hours`;
const profiles =
sync.provider === "makerworld" && PROFILE_SCOPE_PHRASES[sync.profileScope]
? `, each with ${PROFILE_SCOPE_PHRASES[sync.profileScope]}`
: "";
await createNotification(sync.userId, {
type: "sync",
title: `"${name}" is now synced`,
body:
`New models in ${sourceDescription(sync)} are imported here ${interval}${profiles}. ` +
"Change this in the collection's sync configuration.",
externalUrl: sync.sourceUrl,
internalPath: `/models/collections/${sync.collectionId}`,
});
}
// How many "Things (MakerWorld 2)"-style names to try before giving up.
const MAX_NAME_SUFFIX = 20;
@@ -222,6 +252,9 @@ function* collectionNamesFor(title: string, provider: string): Generator<string>
for (let n = 2; n <= MAX_NAME_SUFFIX; n++) yield `${title} (${label} ${n})`;
}
// A synced collection follows what the designer publishes, so new models bring all of their profiles.
const NEW_MAKERWORLD_SYNC_PROFILE_SCOPE: SyncProfileScope = "designer";
/** Links `collection` to `source`. `takenBy` names the provider when the collection already follows
* another source; `link` is the existing one when another request just made this same link. */
async function createSync(
@@ -241,6 +274,7 @@ async function createSync(
externalId: source.externalId,
sourceUrl: source.url,
knownIds,
...(source.provider === "makerworld" ? { profileScope: NEW_MAKERWORLD_SYNC_PROFILE_SCOPE } : {}),
},
include: { collection: true },
});
+9 -4
View File
@@ -120,7 +120,7 @@ async function importDirectly(designId: string) {
}
async function latestNotification() {
return prisma.notification.findFirstOrThrow({ where: { userId }, orderBy: { createdAt: "desc" } });
return prisma.notification.findFirstOrThrow({ where: { userId }, orderBy: [{ createdAt: "desc" }, { id: "desc" }] });
}
describe("synced collections", () => {
@@ -164,7 +164,12 @@ describe("synced collections", () => {
const list = await request(app).get("/api/collections").set(auth());
const listed = list.body.find((c: { id: string }) => c.id === res.body.collection_id);
expect(listed.sync).toMatchObject({ provider: "makerworld", profile_scope: "url" });
expect(listed.sync).toMatchObject({ provider: "makerworld", profile_scope: "designer", interval_hours: 24 });
const notification = await latestNotification();
expect(notification.title).toBe(`"Phoenix flock" is now synced`);
expect(notification.body).toContain("once a day, each with all of the designer's print profiles");
expect(notification.internalPath).toBe(`/models/collections/${res.body.collection_id}`);
});
it("brings back a deleted collection of already-imported models, in the provider's order", async () => {
@@ -556,13 +561,13 @@ describe("synced collections", () => {
expect(res.status).toBe(400);
});
it("checks every collection on its own interval, 1 hour unless it picks 6 or 24", async () => {
it("checks every collection on its own interval, 24 hours unless it picks 1 or 6", async () => {
const enabled = await enable({
url: `https://www.thingiverse.com/collections/${newId()}`,
title: `Thingiverse ${newId()}`,
known_ids: [],
});
expect(enabled.body.interval_hours).toBe(1);
expect(enabled.body.interval_hours).toBe(24);
const patch = (body: Record<string, unknown>) =>
request(app).patch(`/api/collection/${enabled.body.collection_id}/sync`).set(auth()).send(body);
+6 -3
View File
@@ -43,6 +43,8 @@ out: sync only picks up models added **after** you turn it on.
Already imported the collection before? Open its page again, open the panel and tick the box. The panel says
everything is already in your library, and **Save** turns sync on.
Thingport sends a notification once sync is on, naming the collection it follows and its settings.
## Find it in Thingport
A synced collection's tile on the **Collections** page shows the provider's logo with a green tick.
@@ -57,12 +59,13 @@ linked:
- **Overview** shows the chain from the provider's collection to yours, when it was last checked and when the next
check is due, and a link to the collection on the provider's site. **Sync now** checks it right away. **Stop
syncing** unlinks it.
- **Settings** sets how often it's checked: every hour, 6 hours or 24 hours. For MakerWorld it also sets which print
profiles each new model brings: the default one, the designer's, or all of them, community ones included.
- **Settings** sets how often it's checked: every hour, 6 hours or 24 hours (the default). For MakerWorld it also sets
which print profiles each new model brings: the default one, the designer's (the default), or all of them, community
ones included.
## How it works
- **On a schedule.** Each synced collection is checked on its own interval, every hour unless you change it. A check
- **On a schedule.** Each synced collection is checked on its own interval, once a day unless you change it. A check
compares the provider's collection with the models Thingport has already seen there.
- **New models only.** A model that's already in your library is just added to the collection. The rest are queued and
imported one at a time, a minute apart, so the provider isn't flooded with requests. You get a notification when
+2 -2
View File
@@ -186,7 +186,7 @@ On a MakerWorld, Printables or Thingiverse collection page, the panel's **Keep t
sync** box (off by default) links the collection to its Thingport counterpart. A Thingiverse user's
Likes page works the same way (**Keep these likes in sync**), filing into "Thingiverse likes
(@username)" and picking up models they like later. Thingport then checks
it every hour (or every 6 or 24 hours, picked per collection) and imports models added to it later, pausing a minute between models, and sends a
it once a day (or every hour or 6 hours, picked per collection) and imports models added to it later, pausing a minute between models, and sends a
notification saying what came in. Models already on the page when you turn sync on, including any
you leave unticked, aren't imported by the sync.
@@ -209,7 +209,7 @@ extension can turn sync on, since it has just seen which models the collection h
rest wait, paused, in the import queue until it's started again from Administration > Import queue.
- If the provider's collection is deleted, the Thingport collection is unsynced (its models stay)
and you get a notification. Deleting the Thingport collection removes the sync with it.
- **Sync now** checks the collection right away instead of waiting for the hour, and a notification
- **Sync now** checks the collection right away instead of waiting for the next check, and a notification
says how it went, "up to date" included. It's available again 5 minutes after the last check.
- To turn sync off, untick the box on the collection page, or use **Stop syncing** in the
collection's sync configuration.
+1 -1
View File
@@ -35,7 +35,7 @@ export function syncToggleHtml(state: SyncLookup | null): string {
const added = isLikes() ? "Models liked later" : `Models added to it on ${provider} later`;
const hint = state.sync
? `Synced with "${escapeHtml(state.sync.collection_name)}" in Thingport. Untick to stop.`
: `${added} are imported into Thingport automatically, every hour unless you change it there. Models you leave out now stay out.`;
: `${added} are imported into Thingport automatically, on a schedule you can change there. Models you leave out now stay out.`;
return `
<label class="tg-toggle">
<input id="tg-sync" type="checkbox" ${state.sync ? "checked" : ""} />
+1 -1
View File
@@ -34,7 +34,7 @@ collection before? Tick the box and click **Save**.
## What happens next
Thingport checks the collection every hour (or every 6 or 24 hours, if you'd rather) and imports what's new, one model
Thingport checks the collection once a day (or every 6 hours or every hour, if you'd rather) and imports what's new, one model
at a time, a minute apart, so the provider's limits aren't hit. A notification tells you what came in.
Synced collections show the provider's logo on the Collections page. Open one and click the logo to see its sync
+1 -1
View File
@@ -45,7 +45,7 @@ const SECTIONS = [
points: [
"Keep saving models to collections on the sites you browse, even from your phone. They turn up in your library on their own.",
"Follow someone else's public collection, or a Thingiverse user's likes, and get each model they add.",
"Checked every hour, 6 hours or 24 hours, with a notification saying what came in, and Sync now for when you can't wait.",
"Checked once a day, or every 6 hours or every hour, with a notification saying what came in, and Sync now for when you can't wait.",
"New models are imported one at a time, a minute apart, so the provider's limits aren't hit.",
"Sync only adds: removing a model on the provider never deletes it from Thingport, and a model you deleted isn't brought back.",
"Turn it on with one tick in Thingport Grab, on the collection's own page.",