diff --git a/src/app/p/[clientSlug]/[projectSlug]/dashboard/page.tsx b/src/app/p/[clientSlug]/[projectSlug]/dashboard/page.tsx
index 48509f1..2c493df 100644
--- a/src/app/p/[clientSlug]/[projectSlug]/dashboard/page.tsx
+++ b/src/app/p/[clientSlug]/[projectSlug]/dashboard/page.tsx
@@ -12,6 +12,8 @@ import { Methodology } from "@/components/dashboard/methodology";
import { RecentActivity } from "@/components/dashboard/recent-activity";
import { RecomputeButton } from "@/components/dashboard/recompute-button";
import { RefreshCommitsButton } from "@/components/dashboard/refresh-commits-button";
+import { aiConfigured } from "@/lib/ai/settings";
+import { LogWorkPanel } from "@/components/dashboard/log-work-panel";
import { getSession } from "@/lib/auth/session";
import { getServerPb } from "@/lib/pb/server";
import { loadBoard } from "@/lib/data/board";
@@ -88,6 +90,16 @@ export default async function DashboardPage({
)}
+ {session.role === "owner" && (
+
+ )}
+
{!metrics || metrics.totalCommits === 0 ? (
diff --git a/src/components/dashboard/log-work-panel.tsx b/src/components/dashboard/log-work-panel.tsx
new file mode 100644
index 0000000..828d843
--- /dev/null
+++ b/src/components/dashboard/log-work-panel.tsx
@@ -0,0 +1,191 @@
+"use client";
+
+import { useState, useTransition } from "react";
+import { Plus, Sparkles } from "lucide-react";
+
+import { logWorkAction, polishNoteAction } from "@/app/actions/log-work";
+import type { CardRecord } from "@/lib/pb/types";
+
+/**
+ * The owner's way to put off-repo work onto the dashboard.
+ *
+ * Follows the same inline-expand pattern as NewProjectForm and OwnerAdd
+ * elsewhere in this app, rather than introducing a modal/dialog component
+ * that does not otherwise exist in the codebase.
+ */
+export function LogWorkPanel({
+ projectId,
+ clientSlug,
+ projectSlug,
+ cards,
+ assistantAvailable,
+}: {
+ projectId: string;
+ clientSlug: string;
+ projectSlug: string;
+ cards: CardRecord[];
+ assistantAvailable: boolean;
+}) {
+ const [open, setOpen] = useState(false);
+ const [date, setDate] = useState(() => new Date().toISOString().slice(0, 10));
+ const [hours, setHours] = useState("");
+ const [message, setMessage] = useState("");
+ const [cardId, setCardId] = useState("");
+ const [pending, startTransition] = useTransition();
+ const [polishing, setPolishing] = useState(false);
+ const [error, setError] = useState(null);
+
+ if (!open) {
+ return (
+
+ );
+ }
+
+ const field =
+ "w-full rounded-lg border border-border-subtle bg-panel px-3 py-2 text-sm text-text-primary outline-none focus:border-brand";
+
+ function submit(event: React.FormEvent) {
+ event.preventDefault();
+ const parsedHours = Number(hours);
+ if (!message.trim() || !Number.isFinite(parsedHours) || parsedHours <= 0) return;
+ setError(null);
+
+ startTransition(async () => {
+ const result = await logWorkAction({
+ projectId,
+ clientSlug,
+ projectSlug,
+ date,
+ hours: parsedHours,
+ message,
+ cardId: cardId || undefined,
+ });
+
+ if (!result.ok) {
+ setError(result.message ?? "Could not log that work");
+ return;
+ }
+
+ setOpen(false);
+ setHours("");
+ setMessage("");
+ setCardId("");
+ });
+ }
+
+ async function polish() {
+ if (!message.trim()) return;
+ setPolishing(true);
+ setError(null);
+ try {
+ const result = await polishNoteAction({ text: message });
+ if (result.ok && result.text) setMessage(result.text);
+ else setError(result.message ?? "Could not polish that note");
+ } finally {
+ setPolishing(false);
+ }
+ }
+
+ return (
+
+ );
+}