Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
cdc3a63
Generated with Hive: Fix View Source link rendering for web page node…
pitoi Apr 22, 2026
a08d240
Merge remote-tracking branch 'origin/main' into bugfix/cmoa69jeh0017j…
pitoi Apr 22, 2026
b952696
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi Apr 23, 2026
edd2777
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi Apr 23, 2026
b6dcece
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi Apr 23, 2026
29a9fee
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi Apr 23, 2026
4c99f3d
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi Apr 25, 2026
0cbeb6c
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi Apr 27, 2026
5e7da12
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi Apr 27, 2026
69b5dbb
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi Apr 27, 2026
148d871
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi Apr 27, 2026
9d38a0f
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi Apr 27, 2026
e8f45b1
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi Apr 27, 2026
6553cc8
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi Apr 28, 2026
9c8a6f2
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi Apr 28, 2026
677ab9e
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi Apr 28, 2026
e9f09d9
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi Apr 29, 2026
9eec12d
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi Apr 29, 2026
f1c114c
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi Apr 29, 2026
ceb6066
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi Apr 30, 2026
ca9d4b9
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi Apr 30, 2026
5f5e507
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi Apr 30, 2026
9158f0a
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi May 1, 2026
fed704b
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi May 1, 2026
8e6531c
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi May 2, 2026
fb36f2b
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi May 3, 2026
a70dd9c
Resolve merge conflicts with main: merge hasWebPageLink with hasTwitt…
pitoi May 4, 2026
18435c0
Resolve merge conflict: rename WebPage mock node to n12 to avoid coll…
pitoi May 4, 2026
c219dc2
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi May 4, 2026
5a1d885
Resolve merge conflict in radar-settings.tsx: keep isAdmin guard, ado…
pitoi May 4, 2026
926f72d
Resolve merge conflict in radar-settings.tsx: keep isAdmin guard, ado…
pitoi May 5, 2026
361f3a6
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi May 5, 2026
1b0a098
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi May 5, 2026
219d6a0
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi May 5, 2026
2726863
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi May 5, 2026
8392ff7
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi May 6, 2026
318ec5f
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi May 6, 2026
69743e5
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi May 6, 2026
db509c8
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi May 6, 2026
6917cea
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi May 6, 2026
0df9578
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi May 6, 2026
619cade
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi May 7, 2026
a05649f
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi May 7, 2026
f209d12
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi May 7, 2026
f395195
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi May 8, 2026
79e529b
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi May 11, 2026
b5868c2
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi May 11, 2026
b71fd0a
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi May 11, 2026
db46ffe
Merge branch 'main' into bugfix/cmoa69jeh0017jj04lzawepu4-fix-view-so…
pitoi May 11, 2026
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
19 changes: 16 additions & 3 deletions src/app/ontology/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useRouter } from "next/navigation"
import { OntologyGraph } from "./ontology-graph"
import { TypeEditor } from "./type-editor"
import { Plus, ArrowLeft, Box, Grid2x2 } from "lucide-react"
import { useUserStore } from "@/stores/user-store"

const OntologyGraph3D = dynamic(
() => import("./ontology-graph-3d").then((m) => ({ default: m.OntologyGraph3D })),
Expand Down Expand Up @@ -47,11 +48,20 @@ export interface SchemaEdge {

export default function OntologyPage() {
const router = useRouter()
const isAdmin = useUserStore((s) => s.isAdmin)
const isAuthenticated = useUserStore((s) => s.isAuthenticated)
const store = useSchemaStore()
const [selectedId, setSelectedId] = useState<string | null>(null)
const [view3D, setView3D] = useState(false)
const [schemaError, setSchemaError] = useState<string | null>(null)

useEffect(() => {
if (isAuthenticated && !isAdmin) {
router.replace("/")
return
}
}, [isAdmin, isAuthenticated, router])

useEffect(() => {
if (isMocksEnabled()) {
store.setSchemas(SMALL_SCHEMAS)
Expand All @@ -66,17 +76,19 @@ export default function OntologyPage() {

const handleUpdateSchema = useCallback(
async (updated: SchemaNode) => {
if (!isAdmin) return
try {
await store.updateSchema(updated)
setSchemaError(null)
} catch (err) {
setSchemaError(err instanceof Error ? err.message : "Failed to save schema")
}
},
[store]
[isAdmin, store]
)

const handleAddType = useCallback(async () => {
if (!isAdmin) return
// Find next available name
const existing = new Set(store.schemas.map((s) => s.type))
let n = 1
Expand All @@ -98,14 +110,15 @@ export default function OntologyPage() {
setSchemaError(err instanceof Error ? err.message : "Failed to save schema")
}
setSelectedId(id)
}, [store])
}, [isAdmin, store])

const handleDeleteSchema = useCallback(
(refId: string) => {
if (!isAdmin) return
store.removeSchema(refId)
if (selectedId === refId) setSelectedId(null)
},
[selectedId, store]
[isAdmin, selectedId, store]
)

return (
Expand Down
25 changes: 23 additions & 2 deletions src/components/layout/node-preview-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ function isUrl(value: string): boolean {
}
}

function isMediaUrl(value: string): boolean {
return /\.(mp4|webm|mov|mp3|ogg|wav|m4a)(\?.*)?$/i.test(value)
}

function formatDuration(seconds: number): string {
const h = Math.floor(seconds / 3600)
const m = Math.floor((seconds % 3600) / 60)
Expand Down Expand Up @@ -200,7 +204,8 @@ function MediaCard({ node, props }: { node: GraphNode; props: Record<string, unk
const isThisNodeSelected = usePlayerStore(
(s) => s.playingNode?.ref_id === node.ref_id
)
const mediaUrl = (props.media_url ?? props.link) as string | undefined
const rawLink = typeof props.link === "string" ? props.link : undefined
const mediaUrl = (props.media_url ?? (rawLink && isMediaUrl(rawLink) ? rawLink : undefined)) as string | undefined
const duration = typeof props.duration === "number" ? props.duration : undefined
const show = (props.show_title ?? props.show) as string | undefined
const channel = props.channel as string | undefined
Expand Down Expand Up @@ -583,7 +588,12 @@ export function NodePreviewPanel({ node, onBack, schemas }: NodePreviewPanelProp
// bio wins over twitter_handle → Person, not TwitterAccount.
// media_url wins over source_link → MediaCard, not ArticleCard.
const hasTweet = !!fp && "tweet_id" in fp && "text" in fp
const hasMedia = !!fp && ("media_url" in fp || "link" in fp)
const linkValue = typeof fp?.link === "string" ? fp.link : undefined
const hasMedia = !!fp && (
"media_url" in fp ||
(linkValue !== undefined && isMediaUrl(linkValue))
)
const hasWebPageLink = !!linkValue && !isMediaUrl(linkValue)
const hasTranscript = !!fp && typeof fp.transcript === "string"
const hasSummary = hasMedia && !!fp && typeof fp.summary === "string" && fp.summary.length > 0
const hasPerson = !!fp && "bio" in fp && !hasTweet
Expand Down Expand Up @@ -769,6 +779,17 @@ export function NodePreviewPanel({ node, onBack, schemas }: NodePreviewPanelProp
{hasMedia && fullNode && <MediaCard node={fullNode} props={fp} />}
{hasSummary && <SummaryBlock text={fp.summary as string} />}
{hasArticle && <ArticleCard props={fp} />}
{hasWebPageLink && (
<a
href={linkValue}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 text-xs text-primary hover:underline"
>
<ExternalLink className="h-3 w-3" />
View Source
</a>
)}
{hasTranscript && <TranscriptBlock text={fp.transcript as string} />}

{/* Fallback: remaining properties not covered by widgets */}
Expand Down
3 changes: 2 additions & 1 deletion src/components/layout/sources-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ function SourceRow({
const [deleting, setDeleting] = useState(false)

const handleDelete = useCallback(async () => {
if (!canEdit) return
setDeleting(true)
try {
await api.delete(`/radar/${source.ref_id}`)
Expand All @@ -53,7 +54,7 @@ function SourceRow({
} finally {
setDeleting(false)
}
}, [source.ref_id, onDelete])
}, [canEdit, source.ref_id, onDelete])

const displayName = extractNameFromSource(source.source, source.source_type as never)

Expand Down
12 changes: 9 additions & 3 deletions src/components/modals/radar-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from "@/lib/graph-api"
import { isMocksEnabled, MOCK_CRON_CONFIGS } from "@/lib/mock-data"
import { CADENCE_PRESETS, snapToPreset } from "@/lib/cadence-presets"
import { useUserStore } from "@/stores/user-store"

const SOURCE_TYPE_LABELS: Record<RadarSourceType, string> = {
twitter_handle: "Twitter handles",
Expand All @@ -23,6 +24,7 @@ const SOURCE_TYPE_LABELS: Record<RadarSourceType, string> = {
}

export function RadarSettings({ open }: { open: boolean }) {
const isAdmin = useUserStore((s) => s.isAdmin)
const [configs, setConfigs] = useState<CronConfig[] | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
Expand Down Expand Up @@ -54,6 +56,7 @@ export function RadarSettings({ open }: { open: boolean }) {
sourceType: RadarSourceType,
fields: Partial<Pick<CronConfig, "enabled" | "cadence" | "workflow_id">>
) => {
if (!isAdmin) return
// Optimistic update so the row reacts immediately on toggle/edit.
setConfigs((prev) =>
prev
Expand All @@ -76,7 +79,7 @@ export function RadarSettings({ open }: { open: boolean }) {
load()
}
},
[load]
[isAdmin, load]
)

if (loading && !configs) {
Expand All @@ -101,7 +104,7 @@ export function RadarSettings({ open }: { open: boolean }) {
off to pause without losing the cadence.
</p>
{configs?.map((cfg) => (
<RadarRow key={cfg.source_type} config={cfg} onUpdate={handleUpdate} />
<RadarRow key={cfg.source_type} config={cfg} onUpdate={handleUpdate} isAdmin={isAdmin} />
))}
</div>
)
Expand All @@ -110,12 +113,14 @@ export function RadarSettings({ open }: { open: boolean }) {
function RadarRow({
config,
onUpdate,
isAdmin,
}: {
config: CronConfig
onUpdate: (
sourceType: RadarSourceType,
fields: Partial<Pick<CronConfig, "enabled" | "cadence" | "workflow_id">>
) => Promise<void>
isAdmin: boolean
}) {
const [cadence, setCadence] = useState(snapToPreset(config.cadence))
const [running, setRunning] = useState(false)
Expand All @@ -126,6 +131,7 @@ function RadarRow({
}, [config.cadence])

const handleRunNow = useCallback(async () => {
if (!isAdmin) return
if (isMocksEnabled()) {
setRunMessage("Mock: dispatched")
return
Expand All @@ -140,7 +146,7 @@ function RadarRow({
} finally {
setRunning(false)
}
}, [config.source_type])
}, [isAdmin, config.source_type])

return (
<div className="rounded-lg border border-border/50 bg-muted/30 p-3 space-y-2">
Expand Down
69 changes: 69 additions & 0 deletions src/lib/__tests__/node-preview-panel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -768,3 +768,72 @@ describe("NodePreviewPanel – paid_properties lock placeholders", () => {
expect(screen.queryByText("🔒")).toBeNull()
})
})

describe("NodePreviewPanel – View Source / web page link", () => {
beforeEach(() => {
vi.clearAllMocks()
userStoreOverrides = {}
})

it("renders 'View Source' link for a web page node with a plain link URL", async () => {
const node: GraphNode = {
ref_id: "n9",
node_type: "WebPage",
properties: { name: "Sphinx Chat Website", description: "Decentralised messaging on Lightning." },
}
const fullNode: GraphNode = {
...node,
properties: { ...node.properties, link: "https://sphinx.chat" },
}
mockApiGet.mockResolvedValue({ nodes: [fullNode], edges: [] })

render(<NodePreviewPanel node={node} onBack={vi.fn()} schemas={[]} />)

await waitFor(() => {
expect(screen.getByText("View Source")).toBeInTheDocument()
})
const link = screen.getByRole("link", { name: /view source/i })
expect(link).toHaveAttribute("href", "https://sphinx.chat")
expect(link).toHaveAttribute("target", "_blank")
})

it("does not render 'View Source' for an audio node with a media link URL", async () => {
const node: GraphNode = {
ref_id: "a1",
node_type: "Episode",
properties: { name: "Audio Episode" },
}
const fullNode: GraphNode = {
...node,
properties: { ...node.properties, link: "https://example.com/audio.mp3" },
}
mockApiGet.mockResolvedValue({ nodes: [fullNode], edges: [] })

render(<NodePreviewPanel node={node} onBack={vi.fn()} schemas={[]} />)

await waitFor(() => {
expect(screen.getByText("Play Audio")).toBeInTheDocument()
})
expect(screen.queryByText("View Source")).toBeNull()
})

it("renders player and no View Source for a node with media_url", async () => {
const node: GraphNode = {
ref_id: "m1",
node_type: "Episode",
properties: { name: "Media Node" },
}
const fullNode: GraphNode = {
...node,
properties: { ...node.properties, media_url: "https://example.com/episode.mp3" },
}
mockApiGet.mockResolvedValue({ nodes: [fullNode], edges: [] })

render(<NodePreviewPanel node={node} onBack={vi.fn()} schemas={[]} />)

await waitFor(() => {
expect(screen.getByText("Play Audio")).toBeInTheDocument()
})
expect(screen.queryByText("View Source")).toBeNull()
})
})
19 changes: 19 additions & 0 deletions src/lib/mock-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ export const MOCK_NODES: GraphNode[] = [
source_role: "guest",
},
},
{
ref_id: "n12",
node_type: "WebPage",
properties: { name: "Sphinx Chat Website", description: "Decentralised messaging on Lightning." },
},
]

// Full node data returned after unlock (simulates GET /v2/nodes/:ref_id?expand=edges)
Expand Down Expand Up @@ -282,6 +287,20 @@ export const MOCK_FULL_NODES: Record<string, GraphData> = {
],
edges: [],
},
n12: {
nodes: [
{
ref_id: "n12",
node_type: "WebPage",
properties: {
name: "Sphinx Chat Website",
description: "Decentralised messaging on Lightning.",
link: "https://sphinx.chat",
},
},
],
edges: [],
},
}

export const MOCK_EDGES: GraphEdge[] = [
Expand Down
Loading