{% extends "base.html" %}
{% block extra_head %}
<link rel="stylesheet" href="{{ static_url('/static/css/containers.css') }}">
<link rel="stylesheet" href="{{ static_url('/static/css/workspace.css') }}">
{% endblock %}
{% block content %}
<div class="workspace-page" data-workspace-root
data-slug="{{ project.slug or project.uid }}"
data-workspace-uid="{{ workspace.uid if has_workspace else '' }}"
data-has-workspace="{{ 1 if has_workspace else 0 }}">
<h1 class="workspace-title">Workspace: {{ project.title }}</h1>
{% if not has_workspace %}
<div class="card workspace-empty">
<p>No workspace yet for this project. Opening one starts a container with your
project files, a browser editor, and a terminal.</p>
<p class="workspace-muted">You are using {{ workspace_count }} of {{ max_workspaces }} workspaces.</p>
<form method="post" action="/projects/{{ project.slug or project.uid }}/workspace">
<button type="submit" class="btn btn-primary">Open workspace</button>
</form>
</div>
{% else %}
{% if workspace.suspended %}
<div class="card workspace-suspended">
<strong>This workspace is suspended.</strong>
<p>{{ workspace.flag_reason or "Contact an administrator." }}</p>
</div>
{% endif %}
{% for flag in workspace.flags %}
<div class="card workspace-flag workspace-flag-{{ flag.severity }}">
<strong>{{ flag.kind }}</strong>
<span>{{ flag.detail }}</span>
</div>
{% endfor %}
<div class="card workspace-summary">
<div class="workspace-state" data-workspace-status>{{ workspace.status }}</div>
<div class="workspace-meters">
<div class="workspace-meter">
<span>Disk</span>
<progress max="100" value="{{ workspace.disk_percent }}"></progress>
<span>{{ workspace.disk_percent }}% of {{ workspace.disk_quota_mb }} MB</span>
</div>
<div class="workspace-meter">
<span>Egress</span>
<progress max="100" value="{{ workspace.egress_percent }}"></progress>
<span>{{ workspace.egress_percent }}% of {{ workspace.egress_quota_mb }} MB</span>
</div>
</div>
<p class="workspace-muted">
Stops after {{ workspace.idle_stop_minutes }} minutes idle. Removed after
{{ workspace.retention_days }} days idle.
{% if workspace.last_active_at %}
Last active {{ dt_ago(workspace.last_active_at) }}.
{% endif %}
</p>
<div class="workspace-actions">
{% if workspace.status == "running" %}
{% set _url = editor_url %}
{% set _uid = workspace.uid %}
{% set _class = "btn btn-primary" %}
{% set _mode = editor.window_mode %}
{% set _width = editor.window_width %}
{% set _height = editor.window_height %}
{% set _label = "Open editor" %}
{% include "_editor_open.html" %}
<form method="post" action="/projects/{{ project.slug or project.uid }}/workspace/stop">
<button type="submit" class="btn">Stop</button>
</form>
{% else %}
<form method="post" action="/projects/{{ project.slug or project.uid }}/workspace">
<button type="submit" class="btn btn-primary">Start</button>
</form>
{% endif %}
{% if editor_password %}
<code class="secret-value" id="editor-password">{{ editor_password }}</code>
<button type="button" class="btn btn-sm" data-copy="editor-password">Copy password</button>
{% endif %}
<form method="post" action="/projects/{{ project.slug or project.uid }}/workspace/delete">
<button type="submit" data-confirm="Delete this workspace?" data-confirm-danger class="btn btn-danger">Delete</button>
</form>
</div>
</div>
<div class="card workspace-editor" data-editor-card>
<h2>Editor</h2>
<p class="workspace-muted">
Your workspace opens a branded DevPlace editor with the
<code>dpc</code> coding agent already running. These preferences are yours and
follow you into every workspace you open.
</p>
{% if restart_required %}
<div class="workspace-editor-restart">
<span>Your editor settings changed. Restart the workspace to apply them.</span>
<form method="post" action="/projects/{{ project.slug or project.uid }}/workspace/stop">
<button type="submit" class="btn btn-sm">Stop</button>
</form>
<form method="post" action="/projects/{{ project.slug or project.uid }}/workspace">
<button type="submit" class="btn btn-sm btn-primary">Start</button>
</form>
</div>
{% endif %}
<ul class="workspace-editor-summary">
<li><span>Theme</span><strong>{{ editor.theme }}</strong><em>{{ editor.sources.theme }}</em></li>
<li><span>Layout</span><strong>{{ editor.layout }}</strong><em>{{ editor.sources.layout }}</em></li>
<li><span>Panel</span><strong>{{ editor.panel_preset }}</strong><em>{{ editor.sources.panel_preset }}</em></li>
<li><span>Editor font</span><strong>{{ editor.font_size }} px</strong><em>{{ editor.sources.font_size }}</em></li>
<li><span>Terminal font</span><strong>{{ editor.terminal_font_size }} px</strong><em>{{ editor.sources.terminal_font_size }}</em></li>
<li><span>Zoom</span><strong>{{ editor.zoom_level }}</strong><em>{{ editor.sources.zoom_level }}</em></li>
<li><span>Agent on boot</span><strong>{{ editor.boot_agent }}</strong><em>{{ editor.sources.boot_agent }}</em></li>
<li><span>Shell on boot</span><strong>{{ "yes" if editor.boot_shell else "no" }}</strong><em>{{ editor.sources.boot_shell }}</em></li>
<li><span>Opens in</span><strong>{{ editor.window_mode }}</strong><em>{{ editor.sources.window_mode }}</em></li>
<li><span>Trusts every folder</span><strong>{{ "yes" if editor.trust_all else "no" }}</strong><em>site</em></li>
</ul>
<h3 class="workspace-editor-heading">Container size</h3>
<p class="workspace-muted">Set by an administrator through your workspace quota.</p>
<ul class="workspace-editor-summary workspace-editor-resources">
<li><span>CPU</span><strong>{{ editor.cpu_cores }} cores</strong><em>quota</em></li>
<li><span>Memory</span><strong>{{ editor.memory_mb }} MB</strong><em>quota</em></li>
<li><span>Disk</span><strong>{{ editor.disk_quota_mb }} MB</strong><em>quota</em></li>
</ul>
<form class="workspace-editor-form" method="post" data-native
action="/projects/{{ project.slug or project.uid }}/workspace/editor">
<label>Theme
<select name="theme">
<option value="">Site default</option>
<option value="devplace-dark" {{ "selected" if editor.sources.theme == "user" and editor.theme == "devplace-dark" }}>DevPlace Dark</option>
<option value="devplace-light" {{ "selected" if editor.sources.theme == "user" and editor.theme == "devplace-light" }}>DevPlace Light</option>
<option value="system" {{ "selected" if editor.sources.theme == "user" and editor.theme == "system" }}>Leave to me</option>
</select>
</label>
<label>Layout
<select name="layout">
<option value="">Site default</option>
<option value="standard" {{ "selected" if editor.sources.layout == "user" and editor.layout == "standard" }}>Standard</option>
<option value="terminal-focus" {{ "selected" if editor.sources.layout == "user" and editor.layout == "terminal-focus" }}>Terminal focus</option>
<option value="zen" {{ "selected" if editor.sources.layout == "user" and editor.layout == "zen" }}>Zen</option>
</select>
</label>
<label>Terminal panel
<select name="panel_preset">
<option value="">Site default</option>
<option value="short" {{ "selected" if editor.sources.panel_preset == "user" and editor.panel_preset == "short" }}>Short</option>
<option value="normal" {{ "selected" if editor.sources.panel_preset == "user" and editor.panel_preset == "normal" }}>Normal</option>
<option value="tall" {{ "selected" if editor.sources.panel_preset == "user" and editor.panel_preset == "tall" }}>Tall</option>
<option value="maximized" {{ "selected" if editor.sources.panel_preset == "user" and editor.panel_preset == "maximized" }}>Maximized</option>
</select>
</label>
<label>Editor font size
<input type="number" name="font_size" min="0" max="48"
value="{{ editor.font_size if editor.sources.font_size == 'user' else 0 }}">
</label>
<label>Terminal font size
<input type="number" name="terminal_font_size" min="0" max="48"
value="{{ editor.terminal_font_size if editor.sources.terminal_font_size == 'user' else 0 }}">
</label>
<label>Zoom level
<input type="number" name="zoom_level" min="-99" max="5"
value="{{ editor.zoom_level if editor.sources.zoom_level == 'user' else -99 }}">
</label>
<label>Agent on boot
<select name="boot_agent">
<option value="">Site default</option>
<option value="dpc" {{ "selected" if editor.sources.boot_agent == "user" and editor.boot_agent == "dpc" }}>DevPlace Code (dpc)</option>
<option value="none" {{ "selected" if editor.sources.boot_agent == "user" and editor.boot_agent == "none" }}>None</option>
</select>
</label>
<label>Shell on boot
<select name="boot_shell">
<option value="-1">Site default</option>
<option value="1" {{ "selected" if editor.sources.boot_shell == "user" and editor.boot_shell }}>Yes</option>
<option value="0" {{ "selected" if editor.sources.boot_shell == "user" and not editor.boot_shell }}>No</option>
</select>
</label>
<label>Open editor in
<select name="window_mode">
<option value="">Site default</option>
<option value="tab" {{ "selected" if editor.sources.window_mode == "user" and editor.window_mode == "tab" }}>A new tab</option>
<option value="window" {{ "selected" if editor.sources.window_mode == "user" and editor.window_mode == "window" }}>A sized window</option>
<option value="fullscreen" {{ "selected" if editor.sources.window_mode == "user" and editor.window_mode == "fullscreen" }}>A fullscreen window</option>
</select>
</label>
<label>Window width
<input type="number" name="window_width" min="0" max="7680"
value="{{ editor.window_width if editor.sources.window_width == 'user' else 0 }}">
</label>
<label>Window height
<input type="number" name="window_height" min="0" max="4320"
value="{{ editor.window_height if editor.sources.window_height == 'user' else 0 }}">
</label>
<div class="workspace-editor-actions">
<button type="submit" class="btn btn-primary">Save preferences</button>
</div>
</form>
<form class="workspace-editor-reset" method="post" data-native
action="/projects/{{ project.slug or project.uid }}/workspace/editor">
<input type="hidden" name="reset" value="true">
<button type="submit" class="btn btn-sm">Reset to site defaults</button>
</form>
</div>
<div class="card workspace-tunnels">
<h2>Public tunnels</h2>
<p class="workspace-muted">
A tunnel publishes a port from inside your container on a public HTTPS address.
Anyone with the link can reach it. You may have up to {{ workspace.max_tunnels }}.
</p>
<ul class="workspace-tunnel-list" data-tunnel-list>
{% for tunnel in workspace.tunnels %}
<li class="workspace-tunnel">
<a href="https://{{ tunnel.hostname }}" rel="noopener" target="_blank">{{ tunnel.hostname }}</a>
<span class="workspace-badge">port {{ tunnel.container_port }}</span>
<span class="workspace-badge workspace-badge-{{ tunnel.status }}">{{ tunnel.status }}</span>
{% if tunnel.last_error %}<span class="workspace-error">{{ tunnel.last_error }}</span>{% endif %}
<form method="post"
action="/projects/{{ project.slug or project.uid }}/workspace/tunnels/{{ tunnel.uid }}/delete">
<button type="submit" data-confirm="Remove this tunnel?" data-confirm-danger class="btn btn-sm">Remove</button>
</form>
</li>
{% else %}
<li class="workspace-muted">No tunnels yet.</li>
{% endfor %}
</ul>
<form class="workspace-tunnel-form" method="post"
action="/projects/{{ project.slug or project.uid }}/workspace/tunnels">
<input type="text" name="label" placeholder="Label (optional)" maxlength="64">
<input type="number" name="container_port" placeholder="Port" min="1" max="65535" required>
<button type="submit" class="btn">Add tunnel</button>
</form>
</div>
<div class="card workspace-help">
<h2>Inside the container</h2>
<ul>
<li>A <strong>DevPlace Code</strong> terminal running <code>dpc</code> opens for you on boot.</li>
<li>Every folder is trusted, so nothing opens in Restricted Mode and project tasks run.</li>
<li><code>sudo</code> and <code>apt install</code> work with no extra setup.</li>
<li>Python, Rust, Nim and Swift toolchains are preinstalled.</li>
<li>Ports below 1024 cannot bind. Use a high port and a tunnel.</li>
<li>Forwarding a port in the editor's <strong>Ports</strong> view publishes it here automatically.</li>
<li>Your public URLs are also in <code>/app/.devplace/tunnels.json</code>.</li>
</ul>
</div>
{% endif %}
</div>
{% endblock %}
{% block extra_js %}
<script type="module" src="{{ static_url('/static/js/WorkspaceManager.js') }}"></script>
{% endblock %}