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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ dist/

# editor settings
.vscode/
.zed/

# testing
/coverage
Expand Down
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ RUN bun run --cwd apps/backend build
FROM oven/bun:1-alpine
WORKDIR /app

RUN apk add --no-cache valkey

COPY --from=pocketbase /usr/local/bin/pocketbase /usr/local/bin/pocketbase
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
ENV PB_BINARY_PATH=/usr/local/bin/pocketbase
ENV NODE_PATH=/app/apps/backend:/app/packages

Expand All @@ -73,4 +76,5 @@ COPY --from=build /app/packages /app/packages
RUN bun install --production --frozen-lockfile

EXPOSE 3000 8090
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
CMD ["bun", "run", "--cwd", "apps/backend", "start"]
127 changes: 127 additions & 0 deletions apps/backend/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,126 @@ paths:
$ref: "#/components/responses/JsonOk"
"400":
$ref: "#/components/responses/JsonBadRequest"
/monitoring/ssh-hosts:
get:
tags:
- monitoring
summary: List SSH hosts
responses:
"200":
$ref: "#/components/responses/JsonOk"
post:
tags:
- monitoring
summary: Create SSH host
requestBody:
$ref: "#/components/requestBodies/JsonBody"
responses:
"200":
$ref: "#/components/responses/JsonOk"
/monitoring/ssh-hosts/{id}:
put:
tags:
- monitoring
summary: Update SSH host
parameters:
- $ref: "#/components/parameters/Id"
requestBody:
$ref: "#/components/requestBodies/JsonBody"
responses:
"200":
$ref: "#/components/responses/JsonOk"
delete:
tags:
- monitoring
summary: Delete SSH host
parameters:
- $ref: "#/components/parameters/Id"
responses:
"200":
$ref: "#/components/responses/JsonOk"
/monitoring/hosts:
get:
tags:
- monitoring
summary: List system-agent hosts
responses:
"200":
$ref: "#/components/responses/JsonOk"
post:
tags:
- monitoring
summary: Create system-agent host
requestBody:
$ref: "#/components/requestBodies/JsonBody"
responses:
"200":
$ref: "#/components/responses/JsonOk"
/monitoring/hosts/{id}:
get:
tags:
- monitoring
summary: Get system-agent host
parameters:
- $ref: "#/components/parameters/Id"
responses:
"200":
$ref: "#/components/responses/JsonOk"
put:
tags:
- monitoring
summary: Update system-agent host
parameters:
- $ref: "#/components/parameters/Id"
requestBody:
$ref: "#/components/requestBodies/JsonBody"
responses:
"200":
$ref: "#/components/responses/JsonOk"
delete:
tags:
- monitoring
summary: Delete system-agent host
parameters:
- $ref: "#/components/parameters/Id"
responses:
"200":
$ref: "#/components/responses/JsonOk"
/monitoring/hosts/{id}/stats:
get:
tags:
- monitoring
summary: Get current system-agent host stats
parameters:
- $ref: "#/components/parameters/Id"
responses:
"200":
$ref: "#/components/responses/JsonOk"
/monitoring/hosts/{id}/history:
get:
tags:
- monitoring
summary: Get system-agent host metric history
parameters:
- $ref: "#/components/parameters/Id"
- name: timestamp
in: query
required: false
schema:
type: string
responses:
"200":
$ref: "#/components/responses/JsonOk"
/monitoring/hosts/{id}/refresh:
post:
tags:
- monitoring
summary: Refresh a system-agent host
parameters:
- $ref: "#/components/parameters/Id"
responses:
"200":
$ref: "#/components/responses/JsonOk"
"401":
$ref: "#/components/responses/JsonUnauthorized"
/news:
Expand Down Expand Up @@ -1181,6 +1301,13 @@ paths:
"200":
$ref: "#/components/responses/JsonOk"
components:
parameters:
Id:
name: id
in: path
required: true
schema:
type: string
schemas:
GenericObject:
type: object
Expand Down
47 changes: 16 additions & 31 deletions apps/backend/src/jobs/news/feed-builder.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import {
createNewsFeedItemsCache,
getAllNewsFeeds,
getNewsFeedById,
getNewsSubscriptionById,
getNewsFeedItemsCacheByUrl,
updateNewsFeedRecord,
updateNewsFeedItemsCache,
updateNewsSubscription,
} from "../../lib/data/superuser";
import { applyNewsTopics, normalizeMaxFeedItems } from "../../lib/data/news";
import { writeFeedItemsCache } from "../../lib/cache/feed-items";
import { createLogger } from "../../lib/logger";
import { getFeedItems } from "./helper";

Expand Down Expand Up @@ -117,6 +115,7 @@ export async function newsFeedBuilder(feedId?: string): Promise<{
fallbackThumbnailUrl: subscriptionRecord?.fallbackThumbnailUrl,
}) as FeedItem[];
if (subscriptionRecord?.id) {
await writeFeedItemsCache(subscriptionRecord.id, feedItems, [subscriptionRecord.id]);
await updateNewsSubscription(subscriptionRecord.id, { fetchErrors: "" });
}
return {
Expand Down Expand Up @@ -156,33 +155,6 @@ export async function newsFeedBuilder(feedId?: string): Promise<{
subscription_id: subscriptionId,
subscription_name: subscriptionName,
})));
try {
const existing = await getNewsFeedItemsCacheByUrl(res.feedUrl!);

const existingItem = existing.items.length > 0 ? existing.items[0] : undefined;

if (existingItem) {
await updateNewsFeedItemsCache(existingItem.id, {
url: res.feedUrl,
json: JSON.stringify(items),
});
} else {
await createNewsFeedItemsCache({
url: res.feedUrl,
json: JSON.stringify(items),
});
}

cachedCount++;
} catch (err: any) {
feedResult.errors++;
feedResult.details.push({
feedId: newsFeed.id,
action: 'cache_upsert_error',
feedUrl: res.feedUrl,
error: err?.message || String(err),
});
}
} else if (res.action === 'feed_fetch_error') {
feedFetchErrors++;
feedResult.errors++;
Expand All @@ -192,12 +164,13 @@ export async function newsFeedBuilder(feedId?: string): Promise<{
}
}

let cachedItems: Array<FeedItem & { subscription_id: string; subscription_name: string }> = feedItems;
if (newsFeed.userId) {
try {
const sortedItems = feedItems.sort((left, right) => new Date(right.pubDate).getTime() - new Date(left.pubDate).getTime());
const groupedItems = await applyNewsTopics(String(newsFeed.userId), sortedItems, topicSubscriptions);
cachedItems = groupedItems.slice(0, maxFeedItems) as typeof feedItems;
await updateNewsFeedRecord(newsFeed.id, {
feedCache: groupedItems.slice(0, maxFeedItems),
maxFeedItems,
});
feedResult.updated++;
Expand All @@ -211,6 +184,18 @@ export async function newsFeedBuilder(feedId?: string): Promise<{
}
}

try {
await writeFeedItemsCache(newsFeed.id, cachedItems, [newsFeed.id]);
cachedCount++;
} catch (err: any) {
feedResult.errors++;
feedResult.details.push({
feedId: newsFeed.id,
action: 'redis_cache_write_error',
error: err?.message || String(err),
});
}

feedResult.updated += cachedCount;
feedResult.details.push({
feedId: newsFeed.id,
Expand Down
28 changes: 28 additions & 0 deletions apps/backend/src/lib/cache/feed-items.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { RedisClient } from "bun";

const redisUrl = Bun.env.REDIS_URL || Bun.env.VALKEY_URL || "redis://127.0.0.1:6379";
const client = new RedisClient(redisUrl);

export async function readFeedItemsCache(feedId: string): Promise<unknown[] | null> {
const raw = await client.hget(`feedItems:${feedId}`, "json");
if (!raw) return null;

try {
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed : null;
} catch {
return null;
}
}

export async function writeFeedItemsCache(
feedId: string,
items: unknown[],
feedIds: string[] = [feedId],
) {
await client.hmset(`feedItems:${feedId}`, [
"json", JSON.stringify(items),
"date", new Date().toISOString(),
"feedIds", JSON.stringify(feedIds),
]);
}
Loading
Loading