Skip to content

app

The studio dashboard. See Web App for the concepts. Route modules are numerous and change often — this page documents the stable spine.

Bootstrap

app.main

Context builders

app.context

AlcContextMiddleware

AlcContextMiddleware(app)

Pure ASGI middleware (NOT BaseHTTPMiddleware — streaming responses must render inside the bound context) that:

  1. activates the alc request context for /c/{company_id}[/project/{pid}] URLs so every model-layer path lookup grounds to the URL, and
  2. 307-redirects legacy /project/..., /ide/..., /upload/... URLs to the owning company's /c/{cid}/... equivalent (method + body preserved).
Source code in app\context.py
def __init__(self, app):
    self.app = app

default_company_id

default_company_id(request: Request) -> str

The company a context-less request should ground to: this browser's remembered choice (cookie) if still valid, else the global boot default. Read-only — never writes config.

Source code in app\context.py
def default_company_id(request: Request) -> str:
    """The company a context-less request should ground to: this browser's
    remembered choice (cookie) if still valid, else the global boot default.
    Read-only — never writes config."""
    cid = request.cookies.get(ACTIVE_COMPANY_COOKIE)
    if cid and cid in CFG.companies:
        return cid
    return CFG.company_id

default_project_id

default_project_id(
    request: Request, company_id: str
) -> str | None

This browser's remembered project for company_id (cookie) if it still exists on that company, else the company's last-known project. Read-only.

Source code in app\context.py
def default_project_id(request: Request, company_id: str) -> str | None:
    """This browser's remembered project for *company_id* (cookie) if it still
    exists on that company, else the company's last-known project. Read-only."""
    company = CFG.companies.get(company_id)
    if not company:
        return None
    pid = request.cookies.get(ACTIVE_PROJECT_COOKIE)
    if pid and pid in (company.projects or {}):
        return pid
    return company.last_project_id

company_prefix

company_prefix() -> str

The canonical URL prefix for the ACTIVE company (/c/{company_id}). Request-scoped inside a request; the persisted default elsewhere. Shared by python URL builders and (via template_helpers) every Jinja template.

Source code in app\context.py
def company_prefix() -> str:
    """The canonical URL prefix for the ACTIVE company (``/c/{company_id}``).
    Request-scoped inside a request; the persisted default elsewhere. Shared by
    python URL builders and (via template_helpers) every Jinja template."""
    cid = alc_context.active_company_id() or CFG.company_id
    return f"/c/{cid}"

accessible_companies

accessible_companies() -> dict

CFG.companies filtered to what the current identity may see: org-linked companies need membership, unlinked local companies stay open.

Source code in app\context.py
def accessible_companies() -> dict:
    """CFG.companies filtered to what the current identity may see: org-linked
    companies need membership, unlinked local companies stay open."""
    ids = IDENTITY.accessible_company_ids()
    if ids is None:
        return CFG.companies
    return {cid: c for cid, c in CFG.companies.items() if cid in ids}

get_deadline_context

get_deadline_context(request: Request) -> dict

Reads render state from app.state — no circular imports needed.

Source code in app\context.py
def get_deadline_context(request: Request) -> dict:
    """Reads render state from app.state — no circular imports needed."""
    manager = request.app.state.render_manager
    return {
        "progress": manager.progress_percent,
        "pending_jobs": manager.pending_jobs,
        "pending_count": manager.pending_count,
        "rendering_count": manager.rendering_count,
        "done_today": manager.completed_today_count or 0,
        "has_errors": manager.has_errors,
    }

get_dashboard_context

get_dashboard_context(
    request: Request, project_id: str | None = None
) -> dict

Standard studio context for every page.

Pure READ: grounded by the request's URL context (AlcContextMiddleware), never syncs POM or writes config. Two tabs on different companies each get their own context.

Source code in app\context.py
def get_dashboard_context(request: Request, project_id: str | None = None) -> dict:
    """
    Standard studio context for every page.

    Pure READ: grounded by the request's URL context (AlcContextMiddleware),
    never syncs POM or writes config. Two tabs on different companies each get
    their own context.
    """
    company = alc_context.active_company()
    projects = company.projects if company else {}
    current_project_id = project_id or alc_context.active_project_id()
    current_project = projects.get(current_project_id) if current_project_id else None
    deadline = get_deadline_context(request)

    current_path = ""
    counts = {
        "scenes": 0,
        "videos": 0,
        "characters": 0,
        "props": 0,
        "dressing": 0,
        "environments": 0,
    }
    has_script = False
    if current_project is not None:
        try:
            current_path = Path(current_project.path_object.path).as_posix()
        except Exception:
            current_path = ""
        try:
            active_registry = PROJECT_REGISTRY.get_project(
                current_project_id, company_id=company.id if company else None
            )
            has_script = active_registry.has_script
            from alc.services.videos import list_videos

            counts = {
                "scenes": active_registry.scene_count,
                "videos": len(list_videos(current_project_id)),
                "characters": active_registry.character_count,
                "props": active_registry.prop_count,
                "dressing": active_registry.dressing_count,
                "environments": active_registry.environment_count,
            }
        except Exception as e:
            logger.error(f"Failed to fetch registry counts: {e}")

    identity = IDENTITY.peek()
    view_mode = resolve_view_mode(request, identity, company)

    # The persistent Unreal connection header (shell) reads this. Only loaded in
    # the Unreal lens so no other workspace pays for the disk read.
    unreal_session = None
    if view_mode == "unreal" and company and current_project_id:
        from alc.services import unreal as unreal_svc

        unreal_session = unreal_svc.load_session(
            getattr(company, "perforce_root", None), company.id, current_project_id
        )

    return {
        "request": request,
        "projects": projects,
        "current_project": current_project,
        "companies": accessible_companies(),
        "current_company": company,
        "identity": identity,
        "unreal_session": unreal_session,
        **nav_context(view_mode),
        "counts": counts,
        "has_script": has_script,
        "project_id": current_project_id,
        "company_id": company.id if company else None,
        "current_path_debug": current_path,
        **deadline,  # spreads progress, active_count, rendering_count, done_today, has_errors
    }

resolve_asset_code

resolve_asset_code(asset_type: str) -> str | None

Maps any variant of an asset type label to its 3-letter code.

Source code in app\context.py
def resolve_asset_code(asset_type: str) -> str | None:
    """Maps any variant of an asset type label to its 3-letter code."""
    return ASSET_TYPE_MAP.get(asset_type)

app.navigation

Workspace (view-mode) + sidebar navigation registry.

A workspace is a function, not a role: "what am I here to do?" — work my tasks, browse the project, plan it, or read the dashboard. It controls WHAT shows up in the production sidebar (and eventually per-workspace landing pages), never what a user is allowed to do. Roles (WorkOS admin/member) come in later only to pick a sensible DEFAULT workspace and, eventually, which workspaces are offered — anyone can switch from the header, and the choice persists per-browser in a cookie.

Add workspaces or retag items here; templates render from this registry and need no edits.

ToolPath dataclass

ToolPath(
    path: str,
    scope: str = "project",
    label: str = "",
    icon: str = "",
)

Where a sidebar tool lives, for deep-link restore.

path is the tool's canonical sub-path relative to its scope root: for a project tool everything after /c/{cid}/project/{pid}/; for a company tool everything after /c/{cid}/. scope selects which. Matching is segment-prefixed — an incoming URL belongs to this tool when its sub-path equals path or extends it on a segment boundary, so a deep view like scenes/010/read still restores the Scenes tool and its lens.

label and icon are the tool's human identity — the name and Font Awesome glyph the sidebar shows. Carried here so a page can be REFERENCED as data (e.g. a walkthrough step that links to "the My Tasks page" and has Alchemy confirm it) instead of every referrer re-typing the label. label falls back to a title-cased id; icon to a generic page glyph.

page_catalog

page_catalog() -> list[tuple[str, 'ToolPath']]

(nav_id, ToolPath) for every linkable Alchemy page, in sidebar order — the set a walkthrough step may point at. Ordered by the sidebar SECTIONS so the picker reads top-to-bottom the way the app does; anything not in a section trails at the end.

Source code in app\navigation.py
def page_catalog() -> list[tuple[str, "ToolPath"]]:
    """(nav_id, ToolPath) for every linkable Alchemy page, in sidebar order — the
    set a walkthrough step may point at. Ordered by the sidebar SECTIONS so the
    picker reads top-to-bottom the way the app does; anything not in a section
    trails at the end."""
    ordered = [i for items in SECTIONS.values() for i in items if i in TOOL_PATHS]
    ordered += [i for i in TOOL_PATHS if i not in ordered]
    return [(i, TOOL_PATHS[i]) for i in ordered]

page

page(nav_id: str) -> 'ToolPath | None'

The page a nav id names, or None — the confirmation gate. A step's page reference is valid exactly when this returns a ToolPath.

Source code in app\navigation.py
def page(nav_id: str) -> "ToolPath | None":
    """The page a nav id names, or None — the confirmation gate. A step's page
    reference is valid exactly when this returns a ToolPath."""
    return TOOL_PATHS.get(nav_id)

restore_segments

restore_segments(scope: str = 'project') -> frozenset[str]

Head segments the shell may boot for a scope, derived from TOOL_PATHS. Replaces the old hand-kept _NAV_SEGMENTS allowlist — a permissive first gate (per-tool matching happens in resolve_tool).

Source code in app\navigation.py
def restore_segments(scope: str = "project") -> frozenset[str]:
    """Head segments the shell may boot for a scope, derived from TOOL_PATHS.
    Replaces the old hand-kept _NAV_SEGMENTS allowlist — a permissive first gate
    (per-tool matching happens in `resolve_tool`)."""
    return frozenset(
        tp.path.split("/", 1)[0] for tp in TOOL_PATHS.values() if tp.scope == scope
    )

resolve_tool

resolve_tool(
    rest: str, scope: str = "project"
) -> str | None

The sidebar item id owning an incoming sub-path (already stripped of the scope root), by longest segment-prefix. None when no tool claims it.

Source code in app\navigation.py
def resolve_tool(rest: str, scope: str = "project") -> str | None:
    """The sidebar item id owning an incoming sub-path (already stripped of the
    scope root), by longest segment-prefix. None when no tool claims it."""
    rest = rest.split("?", 1)[0].strip("/")
    for nav_id, path in _scoped_paths(scope):
        if _segments_match(rest, path):
            return nav_id
    return None

restore_view_mode

restore_view_mode(
    current_mode: str | None, nav_id: str | None
) -> str | None

The workspace lens to render the restored shell in: keep the user's current one when it already shows this tool, else fall to the first workspace (switcher order) that does. Unregistered tools leave the current mode untouched, so nothing regresses for paths outside the registry.

Source code in app\navigation.py
def restore_view_mode(current_mode: str | None, nav_id: str | None) -> str | None:
    """The workspace lens to render the restored shell in: keep the user's
    current one when it already shows this tool, else fall to the first
    workspace (switcher order) that does. Unregistered tools leave the current
    mode untouched, so nothing regresses for paths outside the registry."""
    if not nav_id:
        return current_mode
    owners = SIDEBAR_NAV.get(nav_id)
    if not owners or current_mode in owners:
        return current_mode
    for mode in VIEW_MODES:  # dict insertion order == header switcher order
        if mode in owners:
            return mode
    return current_mode

visible_nav

visible_nav(mode: str) -> set[str]

Sidebar item ids the given workspace sees. Any unknown mode (stale cookie) sees everything; filtering must fail open.

Source code in app\navigation.py
def visible_nav(mode: str) -> set[str]:
    """Sidebar item ids the given workspace sees. Any unknown mode (stale
    cookie) sees everything; filtering must fail open."""
    if mode not in VIEW_MODES:
        return set(SIDEBAR_NAV)
    return {item for item, modes in SIDEBAR_NAV.items() if mode in modes}

default_view_mode

default_view_mode(identity, company) -> str

The workspace a user lands on before they've ever touched the switcher.

This is the seam where roles meet workspaces: org admins (and local/ no-auth solo studios) land on Browse; plain members land on My Tasks.

Source code in app\navigation.py
def default_view_mode(identity, company) -> str:
    """The workspace a user lands on before they've ever touched the switcher.

    This is the seam where roles meet workspaces: org admins (and local/
    no-auth solo studios) land on Browse; plain members land on My Tasks.
    """
    org_id = getattr(company, "org_id", None) if company else None
    if identity is None or not org_id:
        return "browse"
    for m in getattr(identity, "memberships", []):
        if m.org_id == org_id:
            return "browse" if m.role == "admin" else "tasks"
    return "tasks"

resolve_view_mode

resolve_view_mode(
    request: Request, identity=None, company=None
) -> str

URL ?view= wins (so a workspace is linkable / Ctrl+click-openable in a new tab); then the cookie choice; then the identity default.

Source code in app\navigation.py
def resolve_view_mode(request: Request, identity=None, company=None) -> str:
    """URL `?view=` wins (so a workspace is linkable / Ctrl+click-openable in a
    new tab); then the cookie choice; then the identity default."""
    query = request.query_params.get("view")
    if query in VIEW_MODES:
        return query
    cookie = request.cookies.get(VIEW_MODE_COOKIE)
    if cookie in VIEW_MODES:
        return cookie
    return default_view_mode(identity, company)

nav_context

nav_context(mode: str) -> dict

Template context for the header switcher + workspace-filtered sidebar.

Source code in app\navigation.py
def nav_context(mode: str) -> dict:
    """Template context for the header switcher + workspace-filtered sidebar."""
    visible = visible_nav(mode)
    return {
        "view_mode": mode,
        "view_modes": VIEW_MODES,
        "visible_nav": visible,
        "nav_sections": {
            name: any(item in visible for item in items)
            for name, items in SECTIONS.items()
        },
    }