Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ dashwise-sdk/node_modules
.DS_Store
*.pem

# local development data
**/.data/

# debug
npm-debug.log*
yarn-debug.log*
Expand Down
10 changes: 6 additions & 4 deletions apps/backend/src/jobs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,12 @@ export function registerJobsCron() {
await runDefaultIntegrationsJob("scheduled run");
});

void runNewsFeedBuilderJob("initial run");
Bun.cron(config.FEED_BUILDING_SCHEDULE, async () => {
await runNewsFeedBuilderJob("scheduled run");
});
if (!config.DEV_DISABLE_NEWS_INDEXING) {
void runNewsFeedBuilderJob("initial run");
Bun.cron(config.FEED_BUILDING_SCHEDULE, async () => {
await runNewsFeedBuilderJob("scheduled run");
});
}

Bun.cron(config.NOTIFICATION_FORWARDER_SCHEDULE, async () => {
await runNotificationForwarderJob("cron schedule");
Expand Down
7 changes: 6 additions & 1 deletion apps/backend/src/jobs/news/feed-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
updateNewsSubscription,
} from "../../lib/data/superuser";
import {
hasSubscriptionArticles,
readSubscriptionArticles,
type CachedArticle,
writeMaterializedFeed,
Expand Down Expand Up @@ -333,7 +334,11 @@ export async function newsFeedBuilder(feedId?: string, options: BuilderOptions =

const fetchResults = await Promise.all(allSubscriptions
.filter((subscription) => targetSubscriptionIds.has(String(subscription.id)))
.map((subscription) => fetchAndCacheSubscription(subscription, result)));
.map((subscription) => {
const subscriptionId = String(subscription.id || "");
if (config.DEV_DISABLE_NEWS_INDEXING && hasSubscriptionArticles(subscriptionId)) return Promise.resolve(false);
return fetchAndCacheSubscription(subscription, result);
}));
result.processed = fetchResults.length;

const affectedUsers = new Set<string>(targetUserIds);
Expand Down
89 changes: 69 additions & 20 deletions apps/backend/src/jobs/shortcuts-indexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ type ShortcutRow = {
name: string;
icon: string;
secondary: string;
action: string;
app: string;
action: string;
actions?: Record<string, string>;
app: string;
tags: string[];
sourceId?: string;
sourceUpdated?: string;
Expand All @@ -29,8 +30,10 @@ type ShortcutDefaultsRow = {
icon?: unknown;
secondary?: unknown;
secondaryInfo?: unknown;
action?: unknown;
tags?: unknown;
action?: unknown;
actions?: unknown;
secondaryActions?: unknown;
tags?: unknown;
};

export async function runShortcutsIndexing() {
Expand All @@ -55,7 +58,7 @@ export async function runShortcutsIndexing() {
name,
icon: String(link?.iconUrl || link?.folderIcon || "/icons/faGlobe.svg"),
secondary: String(link?.collection || link?.folder || "Link"),
action: url.startsWith("url:") ? url : `url:${url}`,
action: "link",
app: "",
tags: [
name,
Expand Down Expand Up @@ -153,8 +156,9 @@ function buildDefaultShortcutRows(): ShortcutRow[] {
name,
icon: String(shortcut?.icon ?? "/icons/faGlobe.svg"),
secondary: String(shortcut?.secondaryInfo ?? shortcut?.secondary ?? "Dashwise"),
action,
app: "",
action,
actions: normalizeActions(shortcut.actions ?? shortcut.secondaryActions),
app: "",
tags: [
name,
...(Array.isArray(shortcut?.tags) ? shortcut.tags.map((tag: unknown) => String(tag)) : []),
Expand Down Expand Up @@ -194,6 +198,22 @@ function normalizeObject(raw: unknown): Record<string, any> {
return {};
}

function normalizeActions(raw: unknown): Record<string, string> {
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
return Object.fromEntries(
Object.entries(raw as Record<string, unknown>)
.filter(([label, value]) => label.trim() && typeof value === "string" && value.trim())
.map(([label, value]) => [label.trim(), String(value).trim()]),
);
}

function parseActions(raw: unknown): Record<string, string> {
if (typeof raw === "string") {
try { return normalizeActions(JSON.parse(raw)); } catch { return {}; }
}
return normalizeActions(raw);
}

function normalizeKey(value: string) {
return String(value || "")
.trim()
Expand Down Expand Up @@ -312,8 +332,9 @@ async function buildIntegrationShortcutRows(
name: item.name,
icon: item.icon || integrationIcon,
secondary: item.secondaryInfo || integrationName,
action: serializeShortcutAction(item.action),
app: shortcutApp.id,
action: serializeShortcutAction(item.action),
actions: item.actions,
app: shortcutApp.id,
tags: item.tags,
sourceId: integration.id,
sourceUpdated: (integration as any).updated as string,
Expand Down Expand Up @@ -381,23 +402,28 @@ async function rebuildUserShortcuts(pb: any, userId: string, rows: ShortcutRow[]
const existingRecords = existingBySource.get(sid) || [];

// Determine if it's a link or integration
const isLink = newRows.length === 1 && newRows[0].app === "" && newRows[0].action.startsWith("url:");
const isLink = newRows.length === 1 && newRows[0].app === "" && (newRows[0].action === "link" || newRows[0].action.startsWith("url:"));

if (isLink) {
const newRow = newRows[0];
const existingRecord = existingRecords[0];
let preservedState: Record<string, unknown> = {};

if (existingRecord) {
// Keep the existing shortcut when its source has not changed.
const sourceUpdated = new Date(newRow.sourceUpdated || 0).getTime();
const itemUpdated = new Date(existingRecord.updated).getTime();

if (sourceUpdated <= itemUpdated) {
if (JSON.stringify(parseActions(existingRecord.secondaryActions)) !== JSON.stringify(newRow.actions ?? {})) {
await pb.collection("shortcuts").update(existingRecord.id, { secondaryActions: newRow.actions ?? {} });
}
// Discard (keep existing)
continue;
}

// Replace
// Replace while preserving user-owned state.
preservedState = preserveShortcutState(existingRecord, newRow.app);
await pb.collection("shortcuts").delete(existingRecord.id).catch(() => {});
}

Expand All @@ -406,11 +432,13 @@ async function rebuildUserShortcuts(pb: any, userId: string, rows: ShortcutRow[]
name: newRow.name,
icon: newRow.icon,
secondary: newRow.secondary,
action: newRow.action,
app: newRow.app || null,
action: newRow.action,
secondaryActions: newRow.actions ?? {},
app: newRow.app || null,
tags: JSON.stringify(newRow.tags ?? []),
sourceId: sid,
sourceUpdated: newRow.sourceUpdated,
...preservedState,
});
} else {
// Integration logic: "regenerate every time and check whether the output differs"
Expand All @@ -428,8 +456,9 @@ async function rebuildUserShortcuts(pb: any, userId: string, rows: ShortcutRow[]
name: r.name,
icon: r.icon,
secondary: r.secondary,
action,
app: r.app,
action,
actions: parseActions(r.secondaryActions),
app: r.app,
tags: parseTags(r.tags),
};
}).sort((a, b) => a.action.localeCompare(b.action));
Expand All @@ -438,8 +467,9 @@ async function rebuildUserShortcuts(pb: any, userId: string, rows: ShortcutRow[]
name: r.name,
icon: r.icon,
secondary: r.secondary,
action: r.action,
app: r.app,
action: r.action,
actions: r.actions,
app: r.app,
tags: r.tags,
})).sort((a, b) => a.action.localeCompare(b.action));

Expand All @@ -450,25 +480,44 @@ async function rebuildUserShortcuts(pb: any, userId: string, rows: ShortcutRow[]
continue;
}

// Replace all for this source
// Replace all for this source, retaining user-owned state for matching rows.
const remainingRecords = [...existingRecords];
for (const r of existingRecords) await pb.collection("shortcuts").delete(r.id).catch(() => {});
for (const row of newRows) {
const matchingRecordIndex = remainingRecords.findIndex((record) =>
String(record.name ?? "") === row.name || String(record.action ?? "") === row.action,
);
const matchingRecord = matchingRecordIndex >= 0 ? remainingRecords.splice(matchingRecordIndex, 1)[0] : null;
const preservedState = matchingRecord
? preserveShortcutState(matchingRecord, row.app)
: {};
await pb.collection("shortcuts").create({
user: userId,
name: row.name,
icon: row.icon,
secondary: row.secondary,
action: row.action,
app: row.app || null,
action: row.action,
secondaryActions: row.actions ?? {},
app: row.app || null,
tags: JSON.stringify(row.tags ?? []),
sourceId: sid,
sourceUpdated: row.sourceUpdated,
...preservedState,
});
}
}
}
}

function preserveShortcutState(record: Record<string, any>, generatedApp: string) {
const hasRootOverride = Boolean(generatedApp) && !record.app;
return {
isPinned: Boolean(record.isPinned),
isDisabled: Boolean(record.isDisabled),
...(hasRootOverride ? { app: null } : {}),
};
}

function serializeShortcutAction(action: unknown): string {
if (typeof action === "string") {
return action.trim();
Expand Down
Loading
Loading