Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
3dfd04b
[cueweb] Add Services (Facility Service Defaults) page (CueCommander …
ramonfigueiredo Jun 12, 2026
311cfb1
[cueweb] Add Stuck Frames page (CueCommander parity)
ramonfigueiredo Jun 12, 2026
e5502de
[cueweb] Stuck Frames: full StuckFramePlugin parity (CueCommander)
ramonfigueiredo Jun 12, 2026
791ddde
[cueweb] Monitor Hosts: Full CueCommander parity
ramonfigueiredo Jun 12, 2026
6015a08
[cueweb] Add Monitor Cue page (CueCommander parity)
ramonfigueiredo Jun 12, 2026
912cf57
[cueweb] Job/Layer/Frame context-menu parity + frame log viewer enhan…
ramonfigueiredo Jun 13, 2026
fc32257
Merge branch 'cueweb/job-layer-frame-actions-and-log-viewer' into cue…
ramonfigueiredo Jun 13, 2026
42c8846
[cueweb] Monitor Cue: CueGUI parity for menu, columns, booking bar, s…
ramonfigueiredo Jun 13, 2026
72bee10
Merge branch 'master' into cueweb-monitor-hosts-full
ramonfigueiredo Jun 13, 2026
3cedab5
Merge branch 'master' into cueweb/job-layer-frame-actions-and-log-viewer
ramonfigueiredo Jun 13, 2026
46edae4
Merge branch 'master' into cueweb-monitor-cue
ramonfigueiredo Jun 13, 2026
15ce44e
[cueweb] Address Monitor Hosts review: route cleanup, races, a11y, fi…
ramonfigueiredo Jun 13, 2026
53a59a2
[cueweb] Monitor Hosts: ownership, comments parity, tag refinements, …
ramonfigueiredo Jun 13, 2026
4ac4247
Merge branch 'master' into cueweb-facility-service-defaults
ramonfigueiredo Jun 13, 2026
a15f4bd
Merge branch 'master' into cueweb-stuck-frames
ramonfigueiredo Jun 13, 2026
8245516
[cueweb] Services: surface load/save/delete failures instead of maski…
ramonfigueiredo Jun 15, 2026
e57ab7f
[cueweb] Stuck Frames: bound per-job fan-out, paginate frames, surfac…
ramonfigueiredo Jun 15, 2026
5870b61
[cueweb] Stuck Frames: address review feedback
ramonfigueiredo Jun 15, 2026
74f55e2
[cueweb] Monitor Hosts: gate comment save on action success
ramonfigueiredo Jun 15, 2026
f7de7fb
[cueweb] Stuck Frames: address review feedback
ramonfigueiredo Jun 15, 2026
8260178
[cueweb] Monitor Cue: address review feedback
ramonfigueiredo Jun 15, 2026
dd59f8a
[cueweb] frame/preview: canonicalize with realpath before the root check
ramonfigueiredo Jun 15, 2026
0fc75ae
[cueweb] Stuck Frames: address review feedback
ramonfigueiredo Jun 15, 2026
2cf2f6f
[cueweb] Layer/frame actions & log viewer: address review feedback
ramonfigueiredo Jun 15, 2026
7d4a516
Merge branch 'cueweb/job-layer-frame-actions-and-log-viewer' of https…
ramonfigueiredo Jun 15, 2026
9d72247
Merge branch 'cueweb-monitor-cue' into cueweb/job-layer-frame-actions…
ramonfigueiredo Jun 15, 2026
e13790e
[cueweb] Stuck Frames: address review feedback
ramonfigueiredo Jun 16, 2026
956df07
[cueweb/docs] Document Facility Service Defaults (Services) page
ramonfigueiredo Jun 16, 2026
11d20e0
[cueweb] Monitor Cue: Add show right-click menu (Properties, Create G…
ramonfigueiredo Jun 17, 2026
8457de4
Merge branch 'cueweb-facility-service-defaults' into cueweb-monitor-cue
ramonfigueiredo Jun 17, 2026
cf7b48c
[cueweb] Monitor Cue: group folder tree + Service Properties (show ov…
ramonfigueiredo Jun 17, 2026
69f2737
[cueweb] Monitor Cue: harden API routes and dialogs per review feedback
ramonfigueiredo Jun 17, 2026
6c98a2e
[cueweb] Frame log viewer: version metadata, download, and error jumper
ramonfigueiredo Jun 18, 2026
9dfcd27
[cueweb] Attributes panel: frame selection + copy key/value buttons
ramonfigueiredo Jun 18, 2026
567e905
Merge branch 'cueweb-monitor-hosts-full' into cueweb/job-layer-frame-…
ramonfigueiredo Jun 18, 2026
0e211f2
Merge branch 'cueweb-monitor-cue' into cueweb/job-layer-frame-actions…
ramonfigueiredo Jun 18, 2026
bc964e0
Merge branch 'cueweb-stuck-frames' into cueweb/job-layer-frame-action…
ramonfigueiredo Jun 18, 2026
bae451b
[cueweb] Wire Attributes panel to all entity views (CueGUI parity)
ramonfigueiredo Jun 18, 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
6 changes: 6 additions & 0 deletions cueweb/.env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
NEXT_PUBLIC_OPENCUE_ENDPOINT=http://your-rest-gateway-url.com

# Optional allow-list for the frame preview route (/api/frame/preview), as a
# colon-separated list of absolute path prefixes. When set, the server only
# serves preview images located under one of these roots. When unset, preview
# reads are not restricted to a root (set this to harden the deployment).
# CUEWEB_PREVIEW_ROOTS=/mnt/render:/shows

# Sentry values
SENTRY_ENVIRONMENT='development'
SENTRY_DSN = sentrydsn
Expand Down
23 changes: 23 additions & 0 deletions cueweb/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,29 @@ ENV NEXT_PUBLIC_OPENCUE_ENDPOINT=${NEXT_PUBLIC_OPENCUE_ENDPOINT}
ARG NEXT_PUBLIC_LOG_EDITOR_URL=
ENV NEXT_PUBLIC_LOG_EDITOR_URL=${NEXT_PUBLIC_LOG_EDITOR_URL}

# CueProgBar launcher (Monitor Jobs "Show Progress Bar"). CueProgBar is a
# desktop tool (python -m cuegui.cueguiplugin.cueprogbar <job>); a browser
# can only reach it via a registered URL scheme. {job} is replaced with the
# job name. Leave empty to fall back to the in-table Progress column.
ARG NEXT_PUBLIC_CUEPROGBAR_URL=
ENV NEXT_PUBLIC_CUEPROGBAR_URL=${NEXT_PUBLIC_CUEPROGBAR_URL}

# The CueProgBar command shown in the "Show Progress Bar" dialog. {job} is
# replaced with the job name. Sites override this with their own launcher,
# e.g. "spawn launch cueprogbar {job}".
ARG NEXT_PUBLIC_CUEPROGBAR_COMMAND=python -m cuegui.cueguiplugin.cueprogbar {job}
ENV NEXT_PUBLIC_CUEPROGBAR_COMMAND=${NEXT_PUBLIC_CUEPROGBAR_COMMAND}

# "Preview All" (Frames menu) opens rendered output in an external image
# viewer. PREVIEW_COMMAND is the command shown/copied; PREVIEW_URL is an
# optional registered URL scheme the Launch button hands off to a local
# handler. Placeholders {paths} (output paths), {job}, {layer}, {frame} are
# substituted. Override per site, e.g. a viewer scheme "openrv://{paths}".
ARG NEXT_PUBLIC_PREVIEW_COMMAND=rv {paths}
ENV NEXT_PUBLIC_PREVIEW_COMMAND=${NEXT_PUBLIC_PREVIEW_COMMAND}
ARG NEXT_PUBLIC_PREVIEW_URL=
ENV NEXT_PUBLIC_PREVIEW_URL=${NEXT_PUBLIC_PREVIEW_URL}

# Authentication providers - use ARG to allow override at build time
# Set to empty string to disable authentication (sandbox mode)
# Set to comma-separated list for production (e.g., "google,okta,github,ldap")
Expand Down
2 changes: 1 addition & 1 deletion cueweb/app/api/frame/action/createdependonframe/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { NextRequest, NextResponse } from "next/server";
// and Layer on Simulation Frame dependency wizard flows. The latter
// loops this call once per source frame in the source layer.
export async function POST(request: NextRequest) {
const endpoint = "/frame.FrameInterface/CreateDependencyOnFrame";
const endpoint = "/job.FrameInterface/CreateDependencyOnFrame";
const method = request.method;
if (method !== 'POST') {
return NextResponse.json({ error: 'Invalid method. Only POST is allowed.' }, { status: 405 });
Expand Down
2 changes: 1 addition & 1 deletion cueweb/app/api/frame/action/createdependonjob/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { NextRequest, NextResponse } from "next/server";
// FrameInterface.CreateDependencyOnJob. Used by the Frame On Job
// dependency wizard flow.
export async function POST(request: NextRequest) {
const endpoint = "/frame.FrameInterface/CreateDependencyOnJob";
const endpoint = "/job.FrameInterface/CreateDependencyOnJob";
const method = request.method;
if (method !== 'POST') {
return NextResponse.json({ error: 'Invalid method. Only POST is allowed.' }, { status: 405 });
Expand Down
2 changes: 1 addition & 1 deletion cueweb/app/api/frame/action/createdependonlayer/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { NextRequest, NextResponse } from "next/server";
// FrameInterface.CreateDependencyOnLayer. Used by the Frame On Layer
// dependency wizard flow.
export async function POST(request: NextRequest) {
const endpoint = "/frame.FrameInterface/CreateDependencyOnLayer";
const endpoint = "/job.FrameInterface/CreateDependencyOnLayer";
const method = request.method;
if (method !== 'POST') {
return NextResponse.json({ error: 'Invalid method. Only POST is allowed.' }, { status: 405 });
Expand Down
42 changes: 42 additions & 0 deletions cueweb/app/api/frame/action/dropdepends/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright Contributors to the OpenCue Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { handleRoute } from '@/app/utils/api_utils';
import { NextRequest, NextResponse } from "next/server";

// Drop all dependencies on a frame (CueGUI FrameActions.dropDepends).
// RPC: /job.FrameInterface/DropDepends. Request: { frame, target } where
// target is a depend.DependTarget name (default "ANY_TARGET" = drop all).
export async function POST(request: NextRequest) {
const endpoint = "/job.FrameInterface/DropDepends";
if (request.method !== 'POST') {
return NextResponse.json({ error: 'Invalid method. Only POST is allowed.' }, { status: 405 });
}
let jsonBody: any;
try {
jsonBody = await request.json();
} catch {
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
}
if (!jsonBody?.frame) {
return NextResponse.json({ error: 'Invalid request body (need {frame})' }, { status: 400 });
}
if (!jsonBody.target) jsonBody.target = "ANY_TARGET";
const response = await handleRoute(request.method, endpoint, JSON.stringify(jsonBody), true);
const responseData = await response.json();
if (!response.ok) return NextResponse.json({ error: responseData.error }, { status: response.status });
return NextResponse.json({ data: responseData.data }, { status: response.status });
}
41 changes: 41 additions & 0 deletions cueweb/app/api/frame/action/getdepends/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright Contributors to the OpenCue Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { handleRoute } from '@/app/utils/api_utils';
import { NextRequest, NextResponse } from "next/server";

// Return the dependencies a frame depends on (CueGUI DependDialog ->
// getWhatThisDependsOn). RPC: /job.FrameInterface/GetWhatThisDependsOn.
// Request: { frame }. Response wraps a depend.DependSeq.
export async function POST(request: NextRequest) {
const endpoint = "/job.FrameInterface/GetWhatThisDependsOn";
const method = request.method;
if (method !== 'POST') {
return NextResponse.json({ error: 'Invalid method. Only POST is allowed.' }, { status: 405 });
}

const body = JSON.stringify(await request.json());
const jsonBody = JSON.parse(body);
if (!jsonBody || typeof jsonBody !== 'object' || !jsonBody.frame) {
return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });
}

const response = await handleRoute(method, endpoint, body, true);
const responseData = await response.json();

if (!response.ok) return NextResponse.json({ error: responseData.error }, { status: response.status });
return NextResponse.json({ data: responseData.data }, { status: response.status });
}
41 changes: 41 additions & 0 deletions cueweb/app/api/frame/action/markaswaiting/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright Contributors to the OpenCue Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { handleRoute } from '@/app/utils/api_utils';
import { NextRequest, NextResponse } from "next/server";

// Mark a frame as waiting, ignoring its dependencies once (CueGUI
// FrameActions.markAsWaiting). RPC: /job.FrameInterface/MarkAsWaiting.
// Request: { frame }.
export async function POST(request: NextRequest) {
const endpoint = "/job.FrameInterface/MarkAsWaiting";
const method = request.method;
if (method !== 'POST') {
return NextResponse.json({ error: 'Invalid method. Only POST is allowed.' }, { status: 405 });
}

const body = JSON.stringify(await request.json());
const jsonBody = JSON.parse(body);
if (!jsonBody || typeof jsonBody !== 'object' || !jsonBody.frame) {
return NextResponse.json({ error: 'Invalid request body' }, { status: 400 });
}

const response = await handleRoute(method, endpoint, body, true);
const responseData = await response.json();

if (!response.ok) return NextResponse.json({ error: responseData.error }, { status: response.status });
return NextResponse.json({ data: responseData.data }, { status: response.status });
}
107 changes: 107 additions & 0 deletions cueweb/app/api/frame/preview/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Copyright Contributors to the OpenCue Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { promises as fs } from "fs";
import { NextRequest, NextResponse } from "next/server";
import path from "path";
import { fileExtension, isWebRenderableImage } from "@/app/utils/preview_utils";

// Serves a rendered frame image from the server filesystem for the frame
// preview side panel. GET /api/frame/preview?path=<absolute file path>.
//
// A browser can't read filesystem paths, so this route streams the bytes (the
// CueWeb container must have the render output mounted/readable). EXR-like
// formats return 415 so the panel shows its "not supported in browser"
// fallback. Path traversal is blocked and, when CUEWEB_PREVIEW_ROOTS is set
// (colon-separated absolute prefixes), reads are restricted to those roots.

const MIME: Record<string, string> = {
png: "image/png",
jpg: "image/jpeg",
jpeg: "image/jpeg",
gif: "image/gif",
webp: "image/webp",
bmp: "image/bmp",
avif: "image/avif",
svg: "image/svg+xml",
};

function allowedRoots(): string[] {
return (process.env.CUEWEB_PREVIEW_ROOTS ?? "")
.split(":")
.map((r) => r.trim())
.filter(Boolean);
}

export async function GET(request: NextRequest) {
const target = request.nextUrl.searchParams.get("path") ?? "";

if (!target || target.includes("\0")) {
return NextResponse.json({ error: "Missing or invalid path" }, { status: 400 });
}
if (!path.isAbsolute(target)) {
return NextResponse.json({ error: "Path must be absolute" }, { status: 400 });
}
// Resolve to a canonical absolute path so traversal segments are collapsed
// before any boundary check (a literal ".." substring test is both leaky and
// prone to false positives on names like "foo..bar").
const normalized = path.resolve(target);
Comment thread
ramonfigueiredo marked this conversation as resolved.

// When preview roots are configured, the resolved path must sit inside one of
// them. Roots are an optional per-site allow-list; when unset, reads are not
// restricted to a root (render output paths are site-specific).
const roots = allowedRoots().map((r) => path.resolve(r));
if (roots.length > 0) {
const inAllowedRoot = roots.some((root) => {
const rel = path.relative(root, normalized);
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
});
if (!inAllowedRoot) {
return NextResponse.json({ error: "Path is outside the allowed preview roots" }, { status: 403 });
}
}

const ext = fileExtension(normalized);
if (!isWebRenderableImage(normalized)) {
// EXR / TIFF / DPX etc. - the browser can't render these inline.
return NextResponse.json(
{ error: "unsupported", ext, message: "Preview not supported in browser for this format" },
{ status: 415 },
);
}

try {
const data = await fs.readFile(normalized);
return new NextResponse(new Uint8Array(data), {
status: 200,
headers: {
"Content-Type": MIME[ext] ?? "application/octet-stream",
"Cache-Control": "private, max-age=60",
},
});
} catch (error: any) {
// Log the resolved path server-side for diagnostics, but don't echo the
// server filesystem layout back to the client.
console.error(`Frame preview read failed for ${normalized}:`, error?.code ?? error);
if (error?.code === "ENOENT") {
return NextResponse.json({ error: "File not found" }, { status: 404 });
}
if (error?.code === "EACCES") {
return NextResponse.json({ error: "Permission denied" }, { status: 403 });
}
return NextResponse.json({ error: "Could not read image" }, { status: 500 });
}
}
52 changes: 52 additions & 0 deletions cueweb/app/api/job/action/addrenderpart/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright Contributors to the OpenCue Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { handleRoute } from '@/app/utils/api_utils';
import { NextRequest, NextResponse } from "next/server";

// Add a render partition to a job (CueGUI "Use Local Cores" - book a host's
// resources to the job). Request: { job, host, threads, max_cores,
// max_memory, max_gpu_memory, max_gpus, username }.
// RPC: /job.JobInterface/AddRenderPartition.
export async function POST(request: NextRequest) {
const endpoint = "/job.JobInterface/AddRenderPartition";
if (request.method !== 'POST') {
return NextResponse.json({ error: 'Invalid method. Only POST is allowed.' }, { status: 405 });
}
let jsonBody: any;
try {
jsonBody = await request.json();
} catch {
return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 });
}
if (!jsonBody?.job || typeof jsonBody.host !== 'string' || !jsonBody.host) {
return NextResponse.json({ error: 'Invalid request body: job and host required' }, { status: 400 });
}
const response = await handleRoute(request.method, endpoint, JSON.stringify(jsonBody), true);
const responseData = await response.json();
if (!response.ok) {
// Cuebot only allows a local render partition on a NIMBY-locked host (the
// workstation must be reserved for local use first). Surface that as a
// clear message instead of the raw SpcueRuntimeException stack trace.
const raw = String(responseData?.error ?? "");
const friendly = /not NIMBY locked/i.test(raw)
? `Use Local Cores requires host "${jsonBody.host}" to be NIMBY-locked first ` +
`(this reserves the workstation for local use). NIMBY-lock the host, then try again.`
: responseData.error;
return NextResponse.json({ error: friendly }, { status: response.status });
}
return NextResponse.json({ data: responseData.data }, { status: response.status });
}
42 changes: 42 additions & 0 deletions cueweb/app/api/job/action/markdoneframes/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright Contributors to the OpenCue Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { handleRoute } from '@/app/utils/api_utils';
import { NextRequest, NextResponse } from "next/server";

// Mark a job's frames done, selected by a FrameSearchCriteria (CueGUI
// FrameActions.markdone / eatandmarkdone -> job.markdoneFrames(name=...)).
// RPC: /job.JobInterface/MarkDoneFrames. Request: { job, req } where req is a
// FrameSearchCriteria (e.g. { frames: ["<frame name>"] }).
export async function POST(request: NextRequest) {
const endpoint = "/job.JobInterface/MarkDoneFrames";
if (request.method !== 'POST') {
return NextResponse.json({ error: 'Invalid method. Only POST is allowed.' }, { status: 405 });
}
let jsonBody: any;
try {
jsonBody = await request.json();
} catch {
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
}
if (!jsonBody?.job || !jsonBody.req || typeof jsonBody.req !== 'object') {
return NextResponse.json({ error: 'Invalid request body (need {job, req})' }, { status: 400 });
}
const response = await handleRoute(request.method, endpoint, JSON.stringify(jsonBody), true);
const responseData = await response.json();
if (!response.ok) return NextResponse.json({ error: responseData.error }, { status: response.status });
return NextResponse.json({ data: responseData.data }, { status: response.status });
}
Loading
Loading