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
4 changes: 3 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ Dashboard sessions are created by:
Discord-backed sessions carry the linked CRM contact id from the local `people`
cache when available. Steering Committee+ sessions can use broader CRM people
lookup and onboarding views. Admin+ sessions can access jobs, reruns, sync
actions, and audit views.
actions, and audit views. The Discord `Workflows Engineer` role is a
Steering Committee peer for write access, with admin read access to jobs/audit
and dry-run responses for admin-only rerun/sync writes.

Sensitive dashboard permissions require SSO validation in production. Local,
dev, development, and test environments allow trusted dev role context for
Expand Down
3 changes: 3 additions & 0 deletions ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ current precedence rules.
- `Optional`: `DISCORD_API_TIMEOUT_SECONDS` (default: `8.0`)
- `Optional`: `DISCORD_LINK_TTL_SECONDS` (default: `600`)
- `Optional`: `DISCORD_BOT_TOKEN` (needed only for fallback Discord API checks; DB role check remains primary)
- `Workflows Engineer` is not an admin role. It receives Steering Committee
write permissions plus jobs/audit read and dry-run access for admin-only
rerun/sync dashboard writes.

## Worker Consumer

Expand Down
75 changes: 59 additions & 16 deletions apps/admin_dashboard/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -719,7 +719,7 @@ function App() {
const initialProjectDetailId = detailIdFromPath("projects")
const [user, setUser] = useState<User | null>(null)
const [view, setViewState] = useState<View>(viewFromPath())
const [toast, setToast] = useState<{ message: string; tone?: "ok" | "error" }>({
const [toast, setToast] = useState<{ message: string; tone?: "ok" | "warning" | "error" }>({
message: "",
})
const [permissions, setPermissions] = useState<string[]>([])
Expand Down Expand Up @@ -781,6 +781,14 @@ function App() {
return permissions.includes(permission)
}

function canDryRun(permission: string) {
return permissions.includes(`${permission}:dry_run`)
}

function canUse(permission: string) {
return can(permission) || canDryRun(permission)
}

function canView(nextView: View) {
return can(routePermissions[nextView])
}
Expand All @@ -789,7 +797,7 @@ function App() {
return (Object.keys(routes) as View[]).find((candidate) => canView(candidate)) || "people"
}

function showToast(message: string, tone?: "ok" | "error") {
function showToast(message: string, tone?: "ok" | "warning" | "error") {
setToast({ message, tone })
}

Expand Down Expand Up @@ -958,10 +966,21 @@ function App() {
setBusy("syncProjects", true)
showToast("Queueing project sync")
try {
const payload = await requestJson<{ job_id: string }>("/dashboard/api/sync/projects", {
const payload = await requestJson<{
job_id?: string
dry_run?: boolean
would_enqueue?: { job_type?: string }
}>("/dashboard/api/sync/projects", {
method: "POST",
})
showToast(`Queued project sync ${payload.job_id}`, "ok")
if (payload.dry_run) {
showToast(
`Dry run only: would queue ${payload.would_enqueue?.job_type || "project sync"}`,
"warning",
)
} else {
showToast(`Queued project sync ${payload.job_id}`, "ok")
}
} catch (error) {
showError(error, "Unable to queue project sync")
} finally {
Expand Down Expand Up @@ -1482,12 +1501,20 @@ function App() {
setBusy(`rerun:${jobId}`, true)
showToast(`Rerunning ${jobId}`)
try {
const payload = await requestJson<{ job_id: string }>(
`/dashboard/api/jobs/${encodeURIComponent(jobId)}/rerun`,
{ method: "POST" },
)
showToast(`Queued rerun ${payload.job_id}`, "ok")
await loadJobs()
const payload = await requestJson<{
job_id?: string
dry_run?: boolean
would_enqueue?: { job_type?: string }
}>(`/dashboard/api/jobs/${encodeURIComponent(jobId)}/rerun`, { method: "POST" })
if (payload.dry_run) {
showToast(
`Dry run only: would rerun ${payload.would_enqueue?.job_type || jobId}`,
"warning",
)
} else {
showToast(`Queued rerun ${payload.job_id}`, "ok")
await loadJobs()
}
} catch (error) {
showError(error, "Unable to rerun job")
} finally {
Expand All @@ -1499,10 +1526,21 @@ function App() {
setBusy("syncPeople", true)
showToast("Queueing people sync")
try {
const payload = await requestJson<{ job_id: string }>("/dashboard/api/sync/people", {
const payload = await requestJson<{
job_id?: string
dry_run?: boolean
would_enqueue?: { job_type?: string }
}>("/dashboard/api/sync/people", {
method: "POST",
})
showToast(`Queued people sync ${payload.job_id}`, "ok")
if (payload.dry_run) {
showToast(
`Dry run only: would queue ${payload.would_enqueue?.job_type || "people sync"}`,
"warning",
)
} else {
showToast(`Queued people sync ${payload.job_id}`, "ok")
}
} catch (error) {
showError(error, "Unable to queue people sync")
} finally {
Expand Down Expand Up @@ -1911,7 +1949,7 @@ function App() {
crmBaseUrl={crmBaseUrl}
people={sortedPeople}
sort={sort.people}
canSync={can("people:sync")}
canSync={canUse("people:sync")}
loading={loading}
peopleQuery={peopleQuery}
peopleMember={peopleMember}
Expand Down Expand Up @@ -1979,7 +2017,7 @@ function App() {
loading={loading}
query={projectQuery}
status={projectStatus}
canSync={can("projects:sync")}
canSync={canUse("projects:sync")}
Comment thread
michaelmwu marked this conversation as resolved.
canWrite={can("projects:write")}
Comment thread
michaelmwu marked this conversation as resolved.
crmContactUrl={crmContactUrl}
setQuery={setProjectQuery}
Expand Down Expand Up @@ -2053,7 +2091,7 @@ function App() {
status={status}
jobType={jobType}
jobCounts={jobCounts}
canWrite={can("jobs:write")}
canWrite={canUse("jobs:write")}
setMinutes={setMinutes}
setStatus={setStatus}
setJobType={setJobType}
Expand Down Expand Up @@ -2259,7 +2297,11 @@ function HistoricalPersonChoiceModal({
)
}

function DashboardToast({ toast }: { toast: { message: string; tone?: "ok" | "error" } }) {
function DashboardToast({
toast,
}: {
toast: { message: string; tone?: "ok" | "warning" | "error" }
}) {
if (!toast.message) return null
return (
<div
Expand All @@ -2268,6 +2310,7 @@ function DashboardToast({ toast }: { toast: { message: string; tone?: "ok" | "er
className={cn(
"fixed bottom-5 right-5 z-50 max-w-sm rounded-md border bg-background px-4 py-3 text-sm font-semibold shadow-lg",
toast.tone === "ok" && "border-emerald-500/40 text-emerald-300",
toast.tone === "warning" && "border-amber-500/40 text-amber-200",
toast.tone === "error" && "border-red-500/40 text-red-300",
)}
>
Expand Down
6 changes: 3 additions & 3 deletions apps/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ curl -X GET "http://localhost:8090/jobs/<job_id>" \
## Backend API Endpoints

- `GET /health`: Redis/Postgres/worker health check.
- `GET /dashboard`: Session-authenticated operations dashboard. OIDC admins and Discord Steering Committee+ users get the full dashboard; active Members may use the gig-only view for gigs they originally posted.
- `GET /dashboard`: Session-authenticated operations dashboard. OIDC admins and Discord Steering Committee+ users get the full dashboard; active Members may use the gig-only view for gigs they originally posted. Discord users with the `Workflows Engineer` role get Steering Committee write permissions plus admin read/dry-run access.
- `GET /dashboard/api/me`: Dashboard session identity, including linked CRM contact id when available.
- `GET /dashboard/api/jobs`: Session-authenticated recent jobs list for the dashboard.
- `GET /dashboard/api/jobs/{job_id}`: Session-authenticated dashboard job detail with sensitive payload keys redacted.
Expand Down Expand Up @@ -71,15 +71,15 @@ curl -X GET "http://localhost:8090/jobs/<job_id>" \

Discord deep-link identity policy:

- Discord deep links are available to active CRM-linked Discord users. Members receive gig-only permissions for their own posted gigs; Steering Committee+ users receive broader dashboard permissions.
- Discord deep links are available to active CRM-linked Discord users. Members receive gig-only permissions for their own posted gigs; Steering Committee+ users receive broader dashboard permissions. `Workflows Engineer` users receive Steering Committee write permissions, admin read permissions for jobs/audit, and dry-run access for admin-only dashboard writes such as job reruns and people/project syncs.
- `DISCORD_ADMIN_ROLES` controls which Discord roles can receive admin dashboard permissions (`Admin,Owner` recommended).
- `OIDC_ADMIN_GROUPS` controls normal OIDC dashboard admin membership (`authentik Admins` recommended).
- `AUTH_SESSION_TTL_SECONDS` controls dashboard session lifetime after login (`86400`, one day, by default).
- `DASHBOARD_PUBLIC_BASE_URL` should be set to the public dashboard origin in production, for example `https://workflows.508.dev`, so Discord-created links use the browser-accessible host.
- `DISCORD_LINK_REQUIRE_OIDC_IDENTITY_CHECKS=true` (default): Discord deep links also require OIDC email identity checks against the linked CRM/Discord dashboard user.
- `DISCORD_LINK_REQUIRE_OIDC_IDENTITY_CHECKS=false`: Discord deep links create a Discord-backed session directly after re-validating active CRM membership + Discord Steering Committee+ role, without forcing an OIDC roundtrip.
- In local/dev/test only, the trusted Discord bot role context can create and consume a dashboard link when the local `people` cache has no matching CRM-linked row. Production still requires the normal CRM/people identity.
- Jobs, reruns, people sync, and audit are sensitive admin permissions and require an SSO-validated dashboard session even when the user entered through a Discord link. Local/dev/test environments allow these permissions for development.
- Jobs, reruns, people sync, project sync, and audit are sensitive admin permissions and require an SSO-validated dashboard session even when the user entered through a Discord link. Local/dev/test environments allow these permissions for development. `Workflows Engineer` is the exception for Discord-backed sessions: it can read jobs/audit and receive dry-run responses for rerun/sync writes without receiving real admin write permissions.

### Known handler wiring expectation

Expand Down
Loading