forked from retoor/devplacepy
The workspace editor hung for 60s and then 504'd. Three independent faults were
stacked behind that one symptom.
Reachability: editor_target delegated to proxy_target, which returns
CONTAINER_PROXY_HOST plus the published host port and never falls back to the
container. From inside the app container that address crosses docker0 into the
host INPUT chain, whose policy is DROP with an allow-list that does not include
the published port range, so the packet was dropped and the request hung rather
than being refused. Measured from the app container: container_ip:8443 answers
302, gateway:20006 is dropped. One shared reachable_target now prefers the
direct container leg and falls back to the published port, and editor_target
uses tunnel_target as services/containers/CLAUDE.md already required. The same
defect affected /p/{slug} ingress and every tunnel, since all three resolved
through proxy_target.
The recorded measurement that motivated the old order (container_ip times out,
gateway connects) no longer holds: make docker-attach puts the app on the
instances' bridge network, which is what makes the direct leg work.
Duplicate response headers: the forwarding core relayed the upstream Date and
Server alongside the ones the serving layer generates, so every proxied
response carried two of each. Both are singleton headers and duplicating them
is malformed HTTP.
Serialization: WorkspaceViewOut declared flag_reason and three sibling strings
as str, so a NULL column made the workspace page 500 for JSON clients.
Documents the two public hostnames and the devplace.net SSH tunnel, so a future
session does not conclude the site is down after pointing curl --resolve at an
address the hostname does not resolve to, and adds the layered procedure for
diagnosing a production failure.
Verified on production with Playwright over both hostnames: the code-server
login renders and the workbench loads. Suite: 3345 passed.
262 lines
13 KiB
HTML
262 lines
13 KiB
HTML
{% 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 %}
|