Compare commits

..
Author SHA1 Message Date
Typosaurus 0650c697eb ticket #76 attempt 1 2026-07-19 18:50:44 +00:00
Typosaurus 3d7b655a45 ticket #76 attempt 1 2026-07-19 18:23:56 +00:00
20 changed files with 68 additions and 96 deletions
File diff suppressed because one or more lines are too long
-4
View File
@@ -39,10 +39,6 @@ NGINX_MAX_BODY_SIZE=50m
NGINX_CACHE_ENABLED=false
NGINX_CACHE_MAX_SIZE=1g
# Max length of comment text (applied server-side on write and client-side on input).
# Change at any time via the env var; the DB has no length constraint.
DEVPLACE_COMMENT_MAX_LENGTH=1000
# Run the app container as this host user so shared files keep dev ownership.
DEVPLACE_UID=1000
DEVPLACE_GID=1000
-1
View File
@@ -169,7 +169,6 @@ The log is **administrator-only**. `/admin/audit-log` is a paginated, filterable
| `DEVPLACE_PRESENCE_TIMEOUT_SECONDS` | `60` | Online-presence window: a user counts as online for this many seconds after their last activity. `last_seen` is refreshed by a throttled in-place update at most once per half this interval per worker (no per-load inserts, no data growth) |
| `DEVPLACE_PRESENCE_ONLINE_LIMIT` | `30` | Maximum avatars shown in the feed's live "Online now" panel (ordered alphabetically by username) |
| `DEVPLACE_PRESENCE_ONLINE_MARGIN_SECONDS` | `20` | Grace margin before an online user drops to offline (hysteresis): online at the timeout, offline only after timeout + this. Prevents online/offline flicker for users hovering at the boundary |
| `DEVPLACE_COMMENT_MAX_LENGTH` | `1000` | Maximum character length for comment body text. Applied server-side via Pydantic validation and client-side via `maxLength` on textareas. The database has no column-length constraint, so this can be changed at any time via the environment variable (requires restart) |
### Runtime settings
+3 -1
View File
@@ -37,9 +37,10 @@ from devplacepy.cli.backups import (
)
from devplacepy.cli.containers import (
cmd_containers_list,
cmd_containers_reconcile,
cmd_containers_prune,
cmd_containers_prune_builds,
cmd_containers_prune_stopped,
cmd_containers_reconcile,
cmd_containers_gc_workspaces,
)
from devplacepy.cli.migrate import cmd_emoji_sync, cmd_migrate_data
@@ -83,6 +84,7 @@ __all__ = [
"cmd_containers_reconcile",
"cmd_containers_prune",
"cmd_containers_prune_builds",
"cmd_containers_prune_stopped",
"cmd_containers_gc_workspaces",
"cmd_emoji_sync",
"cmd_migrate_data",
+27 -2
View File
@@ -1,5 +1,6 @@
# retoor <retoor@molodetz.nl>
import asyncio
from devplacepy.cli._shared import _audit_cli
@@ -18,7 +19,6 @@ def cmd_containers_list(args):
def cmd_containers_reconcile(args):
import asyncio
from devplacepy.services.containers.service import ContainerService
asyncio.run(ContainerService().run_once())
@@ -27,7 +27,6 @@ def cmd_containers_reconcile(args):
def cmd_containers_prune(args):
import asyncio
from devplacepy.services.containers.runtime import get_backend
from devplacepy.services.containers.service import ContainerService
@@ -66,6 +65,29 @@ def cmd_containers_prune_builds(args):
)
def cmd_containers_prune_stopped(args):
from devplacepy.services.containers import api, store
from devplacepy.services.containers.service import ContainerService
async def run():
instances = store.all_instances()
marked = 0
for inst in instances:
if inst["status"] == store.ST_STOPPED and inst["desired_state"] == store.DESIRED_STOPPED:
api.mark_for_removal(inst, actor=("system", "prune-stopped"))
marked += 1
if marked:
await ContainerService().run_once()
return marked
marked = asyncio.run(run())
_audit_cli("cli.containers.prune_stopped", f"CLI removed {marked} stale stopped container(s)", metadata={"count": marked})
if marked:
print(f"Prune stopped: marked {marked} container(s) for removal and reconciled.")
else:
print("No stopped containers to prune.")
def cmd_containers_gc_workspaces(args):
import shutil
from pathlib import Path
@@ -102,6 +124,9 @@ def register_containers(subparsers):
"prune-builds",
help="Remove legacy per-project images and clear the dockerfiles/builds tables",
).set_defaults(func=cmd_containers_prune_builds)
containers_sub.add_parser(
"prune-stopped", help="Remove all stopped containers (completed purpose)"
).set_defaults(func=cmd_containers_prune_stopped)
containers_sub.add_parser(
"gc-workspaces", help="Remove workspace dirs with no instances"
).set_defaults(func=cmd_containers_gc_workspaces)
-1
View File
@@ -68,7 +68,6 @@ INTERNAL_GATEWAY_URL = f"{INTERNAL_BASE_URL}/openai/v1/chat/completions"
INTERNAL_EMBED_URL = f"{INTERNAL_BASE_URL}/openai/v1/embeddings"
INTERNAL_MODEL = "molodetz"
INTERNAL_EMBED_MODEL = "molodetz~embed"
COMMENT_MAX_LENGTH = int(environ.get("DEVPLACE_COMMENT_MAX_LENGTH", "1000"))
DEFAULT_CORRECTION_PROMPT = "Leave literary as is, only do punctuation and casing"
DEFAULT_MODIFIER_PROMPT = (
"Execute what is behind `@ai` (the prompt) and replace that part including `@ai`"
+2 -3
View File
@@ -1,6 +1,5 @@
# retoor <retoor@molodetz.nl>
from devplacepy.config import COMMENT_MAX_LENGTH
from .._shared import COMMENT_TARGETS, GIST_LANGUAGES, PROJECT_TYPES, endpoint, field
from devplacepy.constants import TOPICS
@@ -230,7 +229,7 @@ four ways to sign requests.
"textarea",
True,
"Nice work.",
f"Body, 3-{COMMENT_MAX_LENGTH} characters.",
"Body, 3-1000 characters.",
),
field(
"target_uid",
@@ -291,7 +290,7 @@ four ways to sign requests.
"textarea",
True,
"Edited body.",
f"New body, 3-{COMMENT_MAX_LENGTH} characters.",
"New body, 3-1000 characters.",
),
],
sample_response={
+1 -3
View File
@@ -1,7 +1,5 @@
# retoor <retoor@molodetz.nl>
from devplacepy.config import COMMENT_MAX_LENGTH
ROLE_LABELS = {"public": "Public", "user": "Member"}
@@ -239,7 +237,7 @@ DEVRANT_GROUPS = {
encoding="form",
params=[
field("rant_id", "path", type="int", required=True, example="1", description="Rant id."),
field("comment", "body", type="textarea", required=True, example="Great rant!", description=f"Comment text, 1-{COMMENT_MAX_LENGTH} chars."),
field("comment", "body", type="textarea", required=True, example="Great rant!", description="Comment text, 1-1000 chars."),
],
sample_response={"success": True},
),
+3 -3
View File
@@ -6,7 +6,7 @@ from datetime import datetime
from typing import Literal, Optional
from pydantic import BaseModel, Field, field_validator, model_validator
from devplacepy.constants import TOPICS, REACTION_EMOJI
from devplacepy.config import COMMENT_MAX_LENGTH, DEFAULT_CORRECTION_PROMPT, DEFAULT_MODIFIER_PROMPT
from devplacepy.config import DEFAULT_CORRECTION_PROMPT, DEFAULT_MODIFIER_PROMPT
def normalize_european_date(value):
@@ -157,7 +157,7 @@ class PostEditForm(BaseModel):
class CommentForm(BaseModel):
content: str = Field(min_length=3, max_length=COMMENT_MAX_LENGTH)
content: str = Field(min_length=3, max_length=1000)
target_uid: str = Field(default="", max_length=36)
post_uid: str = Field(default="", max_length=36)
target_type: Literal["post", "project", "news", "issue", "gist"] = "post"
@@ -172,7 +172,7 @@ class CommentForm(BaseModel):
class CommentEditForm(BaseModel):
content: str = Field(min_length=3, max_length=COMMENT_MAX_LENGTH)
content: str = Field(min_length=3, max_length=1000)
class ProjectForm(BaseModel):
+1 -2
View File
@@ -69,8 +69,7 @@ async def edit_comment(request: Request, comment_id: str):
if not is_owner(comment, user):
return dr_error("You can only edit your own comments.", fail_reason="not_owner")
text = (params.get("comment") or "").strip()
from devplacepy.config import COMMENT_MAX_LENGTH
if len(text) < 1 or len(text) > COMMENT_MAX_LENGTH:
if len(text) < 1 or len(text) > 1000:
return dr_error("Invalid comment length.", fail_reason="length")
get_table("comments").update(
{
+1 -2
View File
@@ -227,8 +227,7 @@ async def comment_rant(request: Request, rant_id: str):
text = (params.get("comment") or "").strip()
if len(text) < 1:
return dr_error("Your comment is too short.")
from devplacepy.config import COMMENT_MAX_LENGTH
if len(text) > COMMENT_MAX_LENGTH:
if len(text) > 1000:
return dr_error("Your comment is too long.")
create_comment_record(request, user, "post", post["uid"], text)
return dr_ok()
+1 -1
View File
@@ -72,7 +72,7 @@ MENTION_POLL_SECONDS = int(os.environ.get("MENTION_POLL_SECONDS", "30"))
DM_POLL_SECONDS = int(os.environ.get("DM_POLL_SECONDS", "10"))
BOT_USERNAME = os.environ.get("BOT_USERNAME", "")
COMMENT_CHAR_LIMIT = int(os.environ.get("DEVPLACE_COMMENT_MAX_LENGTH", "1000"))
COMMENT_CHAR_LIMIT = 1000
MESSAGE_CHAR_LIMIT = 2000
PART_SUFFIX_RESERVE = 12
PART_DELIVERY_DELAY = 0.5
@@ -2,7 +2,6 @@
from __future__ import annotations
from devplacepy.config import COMMENT_MAX_LENGTH
from ..spec import Action
from ._shared import ATTACHMENTS, TARGET_TYPE, body, confirm, path
@@ -29,7 +28,7 @@ COMMENTS_ACTIONS: tuple[Action, ...] = (
summary="Edit the body of one of your own comments",
params=(
path("comment_uid", "Uid of the comment."),
body("content", f"New comment body, 3-{COMMENT_MAX_LENGTH} characters.", required=True),
body("content", "New comment body, 3-1000 characters.", required=True),
),
),
Action(
-2
View File
@@ -83,8 +83,6 @@ export class CommentManager {
textarea.className = "emoji-picker-target";
textarea.value = text.dataset.raw || text.textContent;
textarea.rows = 3;
const ref = document.querySelector("[data-comment-maxlength]");
textarea.maxLength = ref ? parseInt(ref.dataset.commentMaxlength, 10) : 1000;
form.appendChild(textarea);
const actions = document.createElement("div");
+2 -2
View File
@@ -1,9 +1,9 @@
{% if user %}
<form class="comment-form" method="POST" action="/comments/create" data-comment-maxlength="{{ comment_max_length }}">
<form class="comment-form" method="POST" action="/comments/create">
<input type="hidden" name="target_uid" value="{{ _comment_target_uid }}">
<input type="hidden" name="target_type" value="{{ _comment_target_type }}">
{% set _user = user %}{% set _size = 32 %}{% set _size_class = "sm" %}{% include "_avatar_link.html" %}
<textarea name="content" placeholder="Your opinion goes here..." required aria-required="true" aria-label="Comment" maxlength="{{ comment_max_length }}" class="emoji-picker-target" data-mention></textarea>
<textarea name="content" placeholder="Your opinion goes here..." required aria-required="true" aria-label="Comment" maxlength="1000" class="emoji-picker-target" data-mention></textarea>
<div class="comment-form-actions">
<dp-upload multiple
max-size="{{ max_upload_size_mb() }}"
@@ -107,7 +107,7 @@ instead of the gateway. Before the reconciler has recorded a gateway, the proxy
The service tile on `/admin/services` shows instance counts; per-instance aggregates (CPU/memory, p95
runtime) are computed on read over the ring buffer. The CLI exposes
`devplace containers list | reconcile | prune | prune-builds | gc-workspaces` (`prune-builds` is a
`devplace containers list | reconcile | prune | prune-builds | prune-stopped | gc-workspaces` (`prune-builds` is a
one-time cleanup that removes legacy per-project images and the old dockerfiles/builds tables). Devii's
admin `container_*` tools cover the same instance operations in natural language.
</div>
+1 -2
View File
@@ -6,7 +6,7 @@ import jinja2
from fastapi.templating import Jinja2Templates
from markupsafe import Markup, escape
from devplacepy.cache import TTLCache
from devplacepy.config import COMMENT_MAX_LENGTH, STATIC_VERSION, TEMPLATES_DIR, TEMPLATE_AUTO_RELOAD, PRESENCE_TIMEOUT_SECONDS
from devplacepy.config import STATIC_VERSION, TEMPLATES_DIR, TEMPLATE_AUTO_RELOAD, PRESENCE_TIMEOUT_SECONDS
from devplacepy.constants import TOPICS, REACTION_EMOJI
from devplacepy.database import get_int_setting, get_setting, get_table
from devplacepy.avatar import avatar_url, avatar_seed
@@ -67,7 +67,6 @@ templates.env.globals["is_self"] = is_self
templates.env.globals["guest_disabled"] = guest_disabled
templates.env.globals["is_online"] = presence.is_online
templates.env.globals["presence_timeout"] = PRESENCE_TIMEOUT_SECONDS
templates.env.globals["comment_max_length"] = COMMENT_MAX_LENGTH
from devplacepy.docs_devrant import devrant_endpoints
+24 -28
View File
@@ -1,28 +1,24 @@
2026-07-19T08:11:05 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T08:11:05 DEBUG model=molodetz-pro fps=30
2026-07-19T08:11:05 INFO read task from file: /workspace/prompts/research-1.txt
2026-07-19T08:11:05 INFO settings merged: model=<default> allow=0 deny=0 ask=0
2026-07-19T15:03:09 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T15:03:09 DEBUG model=molodetz-pro fps=30
2026-07-19T15:03:09 INFO read task from file: /workspace/prompts/research-2.txt
2026-07-19T15:03:09 INFO settings merged: model=<default> allow=0 deny=0 ask=0
2026-07-19T15:40:16 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T15:40:16 DEBUG model=molodetz-pro fps=30
2026-07-19T15:40:16 INFO read task from file: /workspace/prompts/research-3.txt
2026-07-19T15:40:16 INFO settings merged: model=<default> allow=0 deny=0 ask=0
2026-07-19T17:35:33 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T17:35:33 DEBUG model=molodetz-pro fps=30
2026-07-19T17:35:33 INFO read task from file: /workspace/prompts/research-4.txt
2026-07-19T17:35:33 INFO settings merged: model=<default> allow=0 deny=0 ask=0
2026-07-19T18:11:47 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T18:11:47 DEBUG model=molodetz-pro fps=30
2026-07-19T18:11:47 INFO read task from file: /workspace/prompts/execution-1.txt
2026-07-19T18:11:47 INFO settings merged: model=<default> allow=0 deny=0 ask=0
2026-07-19T18:31:31 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T18:31:31 DEBUG model=molodetz-pro fps=30
2026-07-19T18:31:31 INFO read task from file: /workspace/prompts/execution-2.txt
2026-07-19T18:31:31 INFO settings merged: model=<default> allow=0 deny=0 ask=0
2026-07-19T18:58:51 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T18:58:51 DEBUG model=molodetz-pro fps=30
2026-07-19T18:58:51 INFO read task from file: /workspace/prompts/execution-3.txt
2026-07-19T18:58:51 INFO settings merged: model=<default> allow=0 deny=0 ask=0
2026-07-19T09:32:07 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T09:32:07 DEBUG model=molodetz-pro fps=30
2026-07-19T09:32:07 INFO read task from file: /workspace/prompts/research-1.txt
2026-07-19T09:32:07 INFO settings merged: model=<default> allow=0 deny=0 ask=0
2026-07-19T15:54:39 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T15:54:39 DEBUG model=molodetz-pro fps=30
2026-07-19T15:54:39 INFO read task from file: /workspace/prompts/research-2.txt
2026-07-19T15:54:39 INFO settings merged: model=<default> allow=0 deny=0 ask=0
2026-07-19T17:25:31 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T17:25:31 DEBUG model=molodetz-pro fps=30
2026-07-19T17:25:31 INFO read task from file: /workspace/prompts/research-3.txt
2026-07-19T17:25:31 INFO settings merged: model=<default> allow=0 deny=0 ask=0
2026-07-19T17:37:32 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T17:37:32 DEBUG model=molodetz-pro fps=30
2026-07-19T17:37:32 INFO read task from file: /workspace/prompts/research-4.txt
2026-07-19T17:37:32 INFO settings merged: model=<default> allow=0 deny=0 ask=0
2026-07-19T18:22:18 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T18:22:18 DEBUG model=molodetz-pro fps=30
2026-07-19T18:22:18 INFO read task from file: /workspace/prompts/execution-1.txt
2026-07-19T18:22:18 INFO settings merged: model=<default> allow=0 deny=0 ask=0
2026-07-19T18:49:24 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T18:49:24 DEBUG model=molodetz-pro fps=30
2026-07-19T18:49:24 INFO read task from file: /workspace/prompts/execution-2.txt
2026-07-19T18:49:24 INFO settings merged: model=<default> allow=0 deny=0 ask=0
-19
View File
@@ -345,22 +345,3 @@ def test_comment_links_multiple_attachments(app_server):
assert u1 in r.text and u2 in r.text, (
"both attachments must be linked and displayed on the comment"
)
def test_create_comment_too_long_rejected(app_server):
from devplacepy.config import COMMENT_MAX_LENGTH
s, _ = _session_comments()
post_uid = _create_post_comments(s, f"cmt-long-{int(time.time() * 1000)}")
r = s.post(
f"{BASE_URL}/comments/create",
data={
"content": "x" * (COMMENT_MAX_LENGTH + 1),
"target_type": "post",
"post_uid": post_uid,
"target_uid": post_uid,
},
allow_redirects=False,
)
assert r.status_code in (400, 422), r.text[:300]
refresh_snapshot()
assert get_table("comments").find_one(target_uid=post_uid) is None
-16
View File
@@ -129,19 +129,3 @@ def test_edit_comment_too_short_rejected(app_server):
assert r.status_code in (400, 422)
refresh_snapshot()
assert get_table("comments").find_one(uid=comment["uid"])["content"] == "Long enough body"
def test_edit_comment_too_long_rejected(app_server):
from devplacepy.config import COMMENT_MAX_LENGTH
s, _ = _session_edit()
post_uid = _create_post_edit(s, f"edit-long-{int(time.time() * 1000)}")
comment = _create_comment_edit(s, post_uid, "Normal body length")
r = s.post(
f"{BASE_URL}/comments/edit/{comment['uid']}",
headers=JSON_edit,
data={"content": "x" * (COMMENT_MAX_LENGTH + 1)},
allow_redirects=False,
)
assert r.status_code in (400, 422), r.text[:300]
refresh_snapshot()
assert get_table("comments").find_one(uid=comment["uid"])["content"] == "Normal body length"