6 Commits
Author SHA1 Message Date
valknar 94ffa9e930 chore: bump version to 0.4.7
CI / Static checks (push) Successful in 35s
CI / Build and push image (push) Successful in 1m32s
2026-08-19 08:46:02 +02:00
valknarandClaude Sonnet 5 491c0a9c7c fix: show live connection indicator on mobile
CI / Static checks (push) Successful in 1m8s
CI / Build and push image (push) Skipped
The header's live/connecting status was hidden below the 860px
breakpoint; keep it visible on mobile too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 08:45:38 +02:00
valknarandClaude Sonnet 5 1d51c5c887 chore: bump version to 0.4.6
CI / Static checks (push) Successful in 38s
CI / Build and push image (push) Successful in 1m30s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpxhtQY3CExQdMs4j7MmJe
2026-08-18 16:58:56 +02:00
valknarandClaude Sonnet 5 2c84a4c9c2 fix: remove project name badges from the Coolify widget
Coolify was the only service reporting the generic detail field
(project name chips); drop it end to end - collector, type, and the
widget's chip row - rather than leave the plumbing for zero producers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpxhtQY3CExQdMs4j7MmJe
2026-08-18 16:58:47 +02:00
valknarandClaude Sonnet 5 6ab3054d9a chore: bump version to 0.4.5
CI / Static checks (push) Successful in 36s
CI / Build and push image (push) Successful in 1m34s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpxhtQY3CExQdMs4j7MmJe
2026-08-18 10:26:58 +02:00
valknarandClaude Sonnet 5 7549658fbb fix: bookmark widget sizing, generic globe icon, code-server brand icon
- multi-link bookmark cards now span 4/2 (matching the system widget)
  instead of 3/1, so they're not cramped and line up into an even
  3-up row alongside it
- the link list scrolls internally instead of stretching the card,
  so widgets in the same row stay equal height regardless of link count
- BrandIcon gains a small "generic icon" escape hatch (currently just
  `globe`, via phosphor) for widgets that don't have a real brand mark
- added a `coder` brand icon (Coder/code-server) for the Code Server link,
  since simple-icons has no official VS Code mark to use

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpxhtQY3CExQdMs4j7MmJe
2026-08-18 10:26:45 +02:00
10 changed files with 21 additions and 30 deletions
-3
View File
@@ -169,7 +169,4 @@ body {
grid-column: 1 / -1 !important;
grid-row: auto !important;
}
.pn-live-label {
display: none !important;
}
}
+13
View File
@@ -1,7 +1,20 @@
import { GlobeSimple } from "@phosphor-icons/react/ssr";
import { BRAND_ICON_PATHS } from "@/lib/brand-icons";
// Generic (non-brand) icons that don't belong in BRAND_ICON_PATHS - for
// widgets that link to something with no single logo of its own.
const GENERIC_ICONS = {
globe: GlobeSimple,
};
export function BrandIcon({ slug, size = 18 }: { slug?: string; size?: number }) {
if (!slug) return null;
const GenericIcon = GENERIC_ICONS[slug as keyof typeof GENERIC_ICONS];
if (GenericIcon) {
return <GenericIcon size={size} className="shrink-0 text-fg-muted" aria-hidden />;
}
const path = BRAND_ICON_PATHS[slug];
if (!path) return null;
+2 -2
View File
@@ -8,13 +8,13 @@ type BookmarkWidget = Extract<Widget, { type: "bookmark" }>;
export function BookmarkWidget({ widget }: { widget: BookmarkWidget }) {
if (widget.links && widget.links.length > 0) {
return (
<div className={PN_CARD_CLASS} style={{ gridColumn: "span 3" }}>
<div className={PN_CARD_CLASS} style={{ gridColumn: "span 4", gridRow: "span 2" }}>
<div className="flex items-center gap-2">
<BrandIcon slug={widget.icon} />
<span className="min-w-0 flex-1 truncate text-[15px] font-medium text-fg">{widget.name}</span>
</div>
{widget.description && <span className="text-xs text-fg-muted">{widget.description}</span>}
<div className="-mx-1 flex flex-col divide-y divide-border">
<div className="-mx-1 flex min-h-0 flex-1 flex-col divide-y divide-border overflow-y-auto">
{widget.links.map((link) => (
<a
key={link.href}
-9
View File
@@ -60,15 +60,6 @@ export function ServiceWidget({ widget, widgetId }: { widget: ServiceWidget; wid
))}
</div>
)}
{data.detail && data.detail.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{data.detail.map((entry) => (
<StatusTag key={entry} tone="neutral">
{entry}
</StatusTag>
))}
</div>
)}
{data.statError && <StatusTag tone="degraded">{data.statError}</StatusTag>}
{widget.showStats && data.docker.cpuPercent !== null && memPercent !== null && (
<div className="mt-0.5 flex gap-4">
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -9,7 +9,7 @@ import { collectUmami } from "./services/umami";
import { collectHeadscale } from "./services/headscale";
import { collectTraefikStat } from "./services/traefik";
function collectServiceStat(widget: ServiceWidget): Promise<{ stats: ServiceStat[]; detail?: string[] }> {
function collectServiceStat(widget: ServiceWidget): Promise<{ stats: ServiceStat[] }> {
switch (widget.service) {
case "gitea":
return collectGitea(widget);
+2 -13
View File
@@ -2,12 +2,6 @@ import type { CoolifyServiceWidget } from "@/lib/config/schema";
import type { ServiceStat } from "@/lib/types/service-result";
import { serviceBaseUrl } from "./base-url";
const DETAIL_LIMIT = 5;
interface CoolifyProject {
name: string;
}
async function fetchCoolify<T>(base: string, path: string, apiToken: string): Promise<T> {
const response = await fetch(`${base}${path}`, {
headers: { Authorization: `Bearer ${apiToken}` },
@@ -19,22 +13,17 @@ async function fetchCoolify<T>(base: string, path: string, apiToken: string): Pr
return (await response.json()) as T;
}
export async function collectCoolify(
widget: CoolifyServiceWidget
): Promise<{ stats: ServiceStat[]; detail?: string[] }> {
export async function collectCoolify(widget: CoolifyServiceWidget): Promise<{ stats: ServiceStat[] }> {
const base = serviceBaseUrl(widget);
const [projects, resources] = await Promise.all([
fetchCoolify<CoolifyProject[]>(base, "/api/v1/projects", widget.apiToken),
fetchCoolify<unknown[]>(base, "/api/v1/projects", widget.apiToken),
fetchCoolify<unknown[]>(base, "/api/v1/resources", widget.apiToken),
]);
const names = projects.slice(0, DETAIL_LIMIT).map((p) => p.name);
if (projects.length > DETAIL_LIMIT) names.push(`+${projects.length - DETAIL_LIMIT} more`);
return {
stats: [
{ label: "Projects", value: String(projects.length) },
{ label: "Resources", value: String(resources.length) },
],
detail: names.length > 0 ? names : undefined,
};
}
-1
View File
@@ -8,6 +8,5 @@ export interface ServiceStat {
export interface ServiceWidgetResult {
docker: DockerContainerResult;
stats: ServiceStat[];
detail?: string[];
statError?: string;
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "pulsenode",
"version": "0.4.4",
"version": "0.4.7",
"private": true,
"scripts": {
"dev": "tsx watch server.ts",
+1
View File
@@ -26,6 +26,7 @@ ICONS = {
"postgresql": "postgresql",
"redis": "redis",
"headscale": "tailscale",
"coder": "coder",
}
ROOT = Path(__file__).resolve().parent.parent