Skip to content

Commit 02f8606

Browse files
Merge pull request #261 from andreasmolnardev/dev-testenv
Changes from testenv
2 parents 7f7b7c7 + 5d7d386 commit 02f8606

11 files changed

Lines changed: 175 additions & 35 deletions

File tree

apps/backend/src/jobs/news/feed-builder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export async function newsFeedBuilder(feedId?: string): Promise<{
6464
}> {
6565
const result = { processed: 0, skipped: 0, updated: 0, errors: 0, details: [] as any[] };
6666

67-
const maxItemsPerFeed = 20;
67+
const maxItemsPerFeed = 50;
6868

6969
logger.info("Running news feed builder");
7070

apps/backend/src/lib/data/news.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,9 @@ import type {
1616
NewsSavedArticleList,
1717
NewsSavedArticlesResponse,
1818
NewsSubscribeInput,
19-
NewsSubscription,
2019
NewsSubscriptionsResponse,
2120
NewsUpdateInput,
22-
} from "@dashwise/types/sdk-types";
21+
} from "@dashwise/types/sdk";
2322
import {
2423
deleteNewsSubscription,
2524
getAllNewsFeeds,
@@ -42,6 +41,23 @@ type NewsTopicDraft = {
4241
articles: NewsFeedItem[];
4342
};
4443

44+
type NewsSubscription = {
45+
id?: string;
46+
userId?: string;
47+
url?: string;
48+
feedUrl?: string;
49+
name?: string;
50+
newFeedTitles?: string[];
51+
linkReplaceRule?: Record<string, string>;
52+
fallbackThumbnailUrl?: string;
53+
thumbnailOverwriteUrl?: string;
54+
similarityGroupingWordsBlacklist?: string;
55+
enableTopicGrouping?: boolean;
56+
json?: unknown;
57+
title?: string;
58+
icon?: string;
59+
};
60+
4561
function escapeFilter(value: string) {
4662
return value.replace(/"/g, '\\"');
4763
}
@@ -368,7 +384,7 @@ function normalizeSubscription(entry: Record<string, unknown> | null): NewsSubsc
368384
if (!url) return null;
369385

370386
return {
371-
id: entry.id ? String(entry.id) : undefined,
387+
id: String(entry.id || ""),
372388
userId: entry.userId ? String(entry.userId) : undefined,
373389
url,
374390
feedUrl: url,
@@ -754,7 +770,11 @@ async function buildFeedFromSubscriptions(
754770
return feed.sort((left, right) => itemTime(right) - itemTime(left));
755771
}
756772

757-
export async function getNewsFeed(userId: string, feedId?: string | null): Promise<NewsFeedItem[]> {
773+
export async function getNewsFeed(
774+
userId: string,
775+
feedId?: string | null,
776+
options?: { limit?: number },
777+
): Promise<{ items: NewsFeedItem[]; total: number; limit: number }> {
758778
const feeds = await getUserFeeds(userId);
759779
const subscriptionIds = await getUserSubscriptionIdsFromFeeds(feeds);
760780
const excludedIds = await getUserExcludedSubscriptionIdsFromFeeds(feeds);
@@ -769,7 +789,16 @@ export async function getNewsFeed(userId: string, feedId?: string | null): Promi
769789
: subscriptions.filter((subscription) => !excludedIds.has(String(subscription.id || "")));
770790

771791
const feed = await buildFeedFromSubscriptions(scopedSubscriptions, feedId, feeds);
772-
return applyNewsTopics(userId, feed, scopedSubscriptions);
792+
const limit = Number.isFinite(Number(options?.limit)) && Number(options?.limit) > 0
793+
? Math.floor(Number(options?.limit))
794+
: 50;
795+
const items = await applyNewsTopics(userId, feed, scopedSubscriptions);
796+
797+
return {
798+
items: items.slice(0, limit),
799+
total: items.length,
800+
limit,
801+
};
773802
}
774803

775804
export async function getNewsSubscriptions(userId: string): Promise<NewsSubscriptionsResponse> {

apps/backend/src/routes/news.route.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Hono } from "hono";
33
import type { Context } from "hono";
44

55
import { createNewsFeedRecordForUser, deleteNewsSavedArticle, deleteNewsSavedArticleList, getNewsFeed, getNewsFeedRecord, getNewsFeeds, getNewsSavedArticles, getNewsSubscriptions, saveNewsArticle, subscribeNewsFeed, unsubscribeNewsFeed, updateNewsFeed, updateNewsFeedRecordForUser, getNewsFeedMetadata, updateNewsSubscription, updateNewsSavedArticleReadState } from "../lib/data/news";
6-
import type { NewsFeedItem, NewsFeedMetadata, NewsFeedRecordCreateInput, NewsFeedRecordUpdateInput, NewsSubscribeInput, NewsUpdateInput } from "../lib/data/news";
6+
import type { NewsFeedItem, NewsFeedMetadata, NewsFeedRecordCreateInput, NewsFeedRecordUpdateInput, NewsSubscribeInput, NewsUpdateInput } from "@dashwise/types/sdk";
77

88
import { readAuthToken, readJsonBody, requireAuth, withJson } from "./shared";
99
import { createLogger } from "../lib/logger";
@@ -204,11 +204,17 @@ newsRoute
204204
}))
205205
.get("/api/v1/news/feeds/:id", withJson(async (c) => {
206206
const { userId } = await requireAuth({ token: readAuthToken(c) });
207-
return getNewsFeed(userId, c.req.param("id"));
207+
const limit = Number(c.req.query("limit") ?? "");
208+
return getNewsFeed(userId, c.req.param("id"), {
209+
limit: Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : undefined,
210+
});
208211
}))
209212
.get("/api/v1/news/feed", withJson(async (c) => {
210213
const { userId } = await requireAuth({ token: readAuthToken(c) });
211-
return getNewsFeed(userId, c.req.query("feedId") ?? "all");
214+
const limit = Number(c.req.query("limit") ?? "");
215+
return getNewsFeed(userId, c.req.query("feedId") ?? "all", {
216+
limit: Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : undefined,
217+
});
212218
}))
213219
.get("/api/v1/news/feed-records/:id", withJson(async (c) => {
214220
const { userId } = await requireAuth({ token: readAuthToken(c) });

apps/web/src/components/news/NewsDashboard.tsx

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export default function NewsDashboardComponent() {
8787
const [saveListSelection, setSaveListSelection] = useState("readLater");
8888
const [newSaveListName, setNewSaveListName] = useState("");
8989
const [saveError, setSaveError] = useState<string | null>(null);
90+
const [feedTotal, setFeedTotal] = useState(0);
9091

9192
const itemsPerPage = 15;
9293
const { token, withAuth } = useAuth();
@@ -114,21 +115,27 @@ export default function NewsDashboardComponent() {
114115
}
115116
};
116117

117-
const loadFeed = async () => {
118+
const loadFeed = async (page = 1) => {
118119
if (!token) return;
119120

120121
try {
122+
setFeed(null);
123+
setFeedTotal(0);
124+
121125
if (activeSavedList) {
122126
const data = await withAuth((auth) => getNewsSavedArticlesAction(auth, activeSavedList));
123127
setSavedArticlesData(data);
124128
setFeed(data.articles.map((article) => article.json));
129+
setFeedTotal(data.articles.length);
125130
return;
126131
}
127132

133+
const limit = Math.max(50, page * itemsPerPage);
128134
const data = await withAuth((auth) =>
129-
getNewsFeedAction(auth, activeFeedId)
135+
getNewsFeedAction(auth, activeFeedId, limit)
130136
);
131-
setFeed(Array.isArray(data) ? data : []);
137+
setFeed(Array.isArray(data.items) ? data.items : []);
138+
setFeedTotal(Number(data.total || 0));
132139
} catch (err) {
133140
console.error("Failed to load news:", err);
134141
}
@@ -141,7 +148,7 @@ export default function NewsDashboardComponent() {
141148

142149
useEffect(() => {
143150
setCurrentPage(1);
144-
loadFeed();
151+
loadFeed(1);
145152
}, [token, activeFeedId]);
146153

147154
useEffect(() => {
@@ -338,7 +345,7 @@ export default function NewsDashboardComponent() {
338345
try {
339346
await withAuth((auth) => refreshNewsFeedAction(auth, targetFeedIds));
340347
setRefreshStatus("Fetching latest articles…");
341-
await loadFeed();
348+
await loadFeed(currentPage);
342349
} catch (err) {
343350
console.error("Refresh failed:", err);
344351
} finally {
@@ -555,12 +562,18 @@ export default function NewsDashboardComponent() {
555562
)
556563
: [];
557564

558-
const totalPages = Math.ceil(allArticles.length / itemsPerPage);
565+
const totalPages = Math.ceil(feedTotal / itemsPerPage);
559566
const paginatedArticles = allArticles.slice(
560567
(currentPage - 1) * itemsPerPage,
561568
currentPage * itemsPerPage,
562569
);
563570

571+
const handlePageChange = (page: number) => {
572+
if (page === currentPage) return;
573+
setCurrentPage(page);
574+
void loadFeed(page);
575+
};
576+
564577
return (
565578
<div className="grid grid-rows-[auto_auto_1fr_auto] min-h-0 h-dvh p-0 overflow-hidden text-(--surface-foreground) bg-(--surface)">
566579
{/* HEADER */}
@@ -649,9 +662,7 @@ export default function NewsDashboardComponent() {
649662
onClick={(e) => {
650663
e.preventDefault();
651664
if (currentPage > 1) {
652-
setCurrentPage(
653-
currentPage - 1,
654-
);
665+
handlePageChange(currentPage - 1);
655666
}
656667
}}
657668
className={currentPage === 1
@@ -677,9 +688,7 @@ export default function NewsDashboardComponent() {
677688
href="#"
678689
onClick={(e) => {
679690
e.preventDefault();
680-
setCurrentPage(
681-
page,
682-
);
691+
handlePageChange(page);
683692
}}
684693
isActive={currentPage ===
685694
page}
@@ -710,9 +719,7 @@ export default function NewsDashboardComponent() {
710719
if (
711720
currentPage < totalPages
712721
) {
713-
setCurrentPage(
714-
currentPage + 1,
715-
);
722+
handlePageChange(currentPage + 1);
716723
}
717724
}}
718725
className={currentPage ===

apps/web/src/components/settings/pages/EditGlanceablesView.tsx

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
SelectTrigger,
1111
SelectValue,
1212
} from "@/components/ui/select";
13-
import { Separator } from "@/components/ui/separator";
13+
import { Checkbox } from "@/components/ui/checkbox";
1414
import { Input } from "@/components/ui/input";
1515
import { GlanceableSide } from "./utils";
1616
import { useLocalization } from "@/context/LocalizationContext";
@@ -56,7 +56,7 @@ export function EditGlanceablesView({
5656
fonts,
5757
}: EditGlanceablesViewProps) {
5858
const localization = useLocalization();
59-
const { withAuth } = useAuth();
59+
const { withAuth, user } = useAuth();
6060
const editorTitle =
6161
selectedClockPart === "clock"
6262
? "Edit Glanceable Clock"
@@ -125,7 +125,13 @@ export function EditGlanceablesView({
125125
<div className="flex min-h-10 items-center justify-center rounded-full px-2 py-0.5 frosted">
126126
<GlanceableComponent
127127
type={selectedClockType}
128-
params={clockGlanceables[selectedClockType] ?? {}}
128+
params={selectedClockType === "greeting"
129+
? {
130+
...(clockGlanceables[selectedClockType] ?? {}),
131+
username: clockGlanceables[selectedClockType]?.username ??
132+
user?.username,
133+
}
134+
: clockGlanceables[selectedClockType] ?? {}}
129135
formatters={{
130136
formatTemperature: localization.formatTemperature,
131137
formatTime: localization.formatTime,
@@ -254,6 +260,28 @@ export function EditGlanceablesView({
254260
</div>
255261
)}
256262

263+
{selectedClockType === "greeting" && (
264+
<div className="space-y-2">
265+
<label className="flex items-center gap-2 text-sm text-white/75">
266+
<Checkbox
267+
checked={Boolean(
268+
clockGlanceables[selectedClockType]?.showUsername,
269+
)}
270+
onCheckedChange={(checked) => {
271+
setClockGlanceables((prev) => ({
272+
...prev,
273+
[selectedClockType]: {
274+
...(prev[selectedClockType] ?? {}),
275+
showUsername: Boolean(checked),
276+
},
277+
}));
278+
}}
279+
/>
280+
Show username
281+
</label>
282+
</div>
283+
)}
284+
257285
{integrationInfo?.environmentDefinitions &&
258286
Object.entries(integrationInfo.environmentDefinitions).map(([key, def]) => (
259287
<div key={key} className="space-y-2">

apps/web/src/components/widgets/ClockWidget.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type ClockWidgetProps = {
1717
outlineColor?: string;
1818
outlineWidth?: number;
1919
className?: string;
20+
isPreview?: boolean;
2021
style?: React.CSSProperties;
2122
};
2223

@@ -36,6 +37,7 @@ export default function ClockWidget({
3637
outlineColor,
3738
outlineWidth,
3839
className,
40+
isPreview,
3941
style,
4042
}: ClockWidgetProps) {
4143
const [time, setTime] = useState("");
@@ -105,7 +107,7 @@ export default function ClockWidget({
105107
return (
106108
<div
107109
className={cn(
108-
"text-6xl text-center p-4",
110+
isPreview ? "text-2xl text-center p-1 leading-none whitespace-nowrap" : "text-6xl text-center p-4 whitespace-nowrap",
109111
className
110112
)}
111113
style={finalStyle}

apps/web/src/components/widgets/Widget.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,10 +157,10 @@ function RssFeedWidgetWrapper({
157157
const selectedFeedId = String(subscriptionId || feedId || "all").trim() || "all";
158158

159159
setLoading(true);
160-
void withAuth((auth) => getNewsFeedAction(auth, selectedFeedId))
160+
void withAuth((auth) => getNewsFeedAction(auth, selectedFeedId, Math.max(maxItems, 50)))
161161
.then((feedItems) => {
162162
if (!cancelled) {
163-
setItems(Array.isArray(feedItems) ? feedItems : []);
163+
setItems(Array.isArray(feedItems?.items) ? feedItems.items : []);
164164
}
165165
})
166166
.catch((err) => {

apps/web/src/components/widgets/dashboard/GlanceableClock.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,20 @@ export default function GlanceableClockWidget({ className, params, isPreview }:
5858

5959
const getParams = (type: string) => {
6060
const override = glanceableOverrides?.[type];
61-
if (override && typeof override === "object") return override;
61+
if (override && typeof override === "object") {
62+
return type === "greeting"
63+
? { ...override, username: override.username ?? user?.username }
64+
: override;
65+
}
6266
const fallback = defaultGlanceables.find((g) => g?.type === type);
6367
if (!fallback) return undefined;
6468
const { type: _t, ...rest } = fallback;
69+
if (type === "greeting") {
70+
return {
71+
...(Object.keys(rest).length > 0 ? rest : {}),
72+
username: (rest as Record<string, any>).username ?? user?.username,
73+
};
74+
}
6575
return Object.keys(rest).length > 0 ? rest : undefined;
6676
};
6777

@@ -86,6 +96,7 @@ export default function GlanceableClockWidget({ className, params, isPreview }:
8696
outlineEnabled={clockStyle?.outlineEnabled}
8797
outlineColor={clockStyle?.outlineColor}
8898
outlineWidth={clockStyle?.outlineWidth}
99+
isPreview={isPreview}
89100
/>
90101
</div>
91102
</div>

apps/web/src/lib/apiClient.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import config from "@/lib/config";
1818
import { client } from "./api/client.gen";
1919
import * as sdk from "./api/sdk.gen";
2020

21-
const { getAppConfig, getAppInfo, postAuthLogin, postAuthChangePassword, postAuthSignup, postAuthValidateAuth, deleteAuthDeleteAccount, patchAuthUpdateUserProperty, getLinksCollections, postLinksCollections, putLinksCollectionsByCollectionId, postLinksTags, putLinksTagsByTagId, getLinksHomeGroups, postLinksHomeGroups, putLinksFoldersByFolderIdIcon, getLinksHome, getLinksFolders, postLinksFolders, getLinksItems, getLinksTags, postLinksItems, putLinksItemsByLinkId, deleteLinksItemsByLinkId, postLinksReorder, getIntegrations, postIntegrations, putIntegrationsById, deleteIntegrationsById, postIntegrationsTestEndpoint, getIntegrationsWidgetProperties, getWidgetsByIntegration, postIntegrationsConsumerData, getIntegrationsCaldavEvents, postIntegrationsProxyAction, getWidgets, getGlanceables, getGlanceablesByIntegration, getMonitoringStatus, postMonitoringStatus, getMonitors, getMonitorsById, putMonitorsById, postMonitors, deleteMonitorsById, getNewsFeedsById, getNewsFeedRecordsById, postNewsFeedRecords, getNewsSubscriptions, getNewsFeeds, getNewsFeedMetadata, postNewsFeedRefresh, postNewsFeedSubscribe, postNewsFeedUnsubscribe, postNewsFeedUpdate, postNewsFeedRecordsById, postNewsFixMissingTitles, getPageConfig, getPageConfigUserPages, putPageConfig, postPageConfigHome, postPageConfigMigrateLegacy, postPageConfigIntegrationData, getSearchItems, getSearchItemsFrequentlyUsed, postSearchItemsUsageStats, getLocations, getJobsPullIcons, postWallpapers, getNotifications, getNotificationsTopics, postNotificationsTopics, deleteNotificationsTopics, postNotificationsMarkAsRead, postNotificationsTest, getNotificationsTopicTokens, postNotificationsTopicTokens, deleteNotificationsTopicTokens, getNotificationsForwarders, postNotificationsForwarders, putNotificationsForwarders, deleteNotificationsForwarders } = sdk;
21+
const { getAppConfig, getAppInfo, postAuthLogin, postAuthChangePassword, postAuthSignup, postAuthValidateAuth, deleteAuthDeleteAccount, patchAuthUpdateUserProperty, getLinksCollections, postLinksCollections, putLinksCollectionsByCollectionId, postLinksTags, putLinksTagsByTagId, getLinksHomeGroups, postLinksHomeGroups, putLinksFoldersByFolderIdIcon, getLinksHome, getLinksFolders, postLinksFolders, getLinksItems, getLinksTags, postLinksItems, putLinksItemsByLinkId, deleteLinksItemsByLinkId, postLinksReorder, getIntegrations, postIntegrations, putIntegrationsById, deleteIntegrationsById, postIntegrationsTestEndpoint, getIntegrationsWidgetProperties, getWidgetsByIntegration, postIntegrationsConsumerData, getIntegrationsCaldavEvents, postIntegrationsProxyAction, getWidgets, getGlanceables, getGlanceablesByIntegration, getMonitoringStatus, postMonitoringStatus, getMonitors, getMonitorsById, putMonitorsById, postMonitors, deleteMonitorsById, getNewsFeedRecordsById, postNewsFeedRecords, getNewsSubscriptions, getNewsFeeds, getNewsFeedMetadata, postNewsFeedRefresh, postNewsFeedSubscribe, postNewsFeedUnsubscribe, postNewsFeedUpdate, postNewsFeedRecordsById, postNewsFixMissingTitles, getPageConfig, getPageConfigUserPages, putPageConfig, postPageConfigHome, postPageConfigMigrateLegacy, postPageConfigIntegrationData, getSearchItems, getSearchItemsFrequentlyUsed, postSearchItemsUsageStats, getLocations, getJobsPullIcons, postWallpapers, getNotifications, getNotificationsTopics, postNotificationsTopics, deleteNotificationsTopics, postNotificationsMarkAsRead, postNotificationsTest, getNotificationsTopicTokens, postNotificationsTopicTokens, deleteNotificationsTopicTokens, getNotificationsForwarders, postNotificationsForwarders, putNotificationsForwarders, deleteNotificationsForwarders } = sdk;
2222
export * from "./api/sdk.gen";
2323
export type { GenericObject, Error } from "./api/types.gen";
2424

@@ -65,6 +65,12 @@ export type MonitoringSshHostInput = {
6565
privateKey?: string;
6666
};
6767

68+
export type NewsFeedPageResponse = {
69+
items: NewsFeedItem[];
70+
total: number;
71+
limit: number;
72+
};
73+
6874
function stringifyError(error: unknown) {
6975
if (typeof error === "string") return error;
7076
if (error && typeof error === "object") {
@@ -383,8 +389,13 @@ export async function updateMonitoringSshHostAction(auth: ActionAuth, hostId: st
383389

384390
// --- News actions ---
385391

386-
export async function getNewsFeedAction(auth: ActionAuth, feedId?: string | null): Promise<NewsFeedItem[]> {
387-
return extractData(await getNewsFeedsById({ path: { id: feedId ?? "all" }, headers: authHeaders(auth) })) as Promise<NewsFeedItem[]>;
392+
export async function getNewsFeedAction(auth: ActionAuth, feedId?: string | null, limit?: number): Promise<NewsFeedPageResponse> {
393+
return extractData(await client.get({
394+
url: "/news/feeds/{id}",
395+
path: { id: feedId ?? "all" },
396+
query: limit ? { limit } : undefined,
397+
headers: authHeaders(auth),
398+
})) as Promise<NewsFeedPageResponse>;
388399
}
389400

390401
export async function getNewsFeedRecordAction(auth: ActionAuth, feedId?: string | null): Promise<NewsFeedRecord | null> {

0 commit comments

Comments
 (0)