Compare commits

..
Author SHA1 Message Date
Typosaurus e5cb4772e0 ticket #10 attempt 2 2026-07-19 19:03:57 +00:00
Typosaurus ea943ac502 ticket #10 attempt 2 2026-07-19 18:34:58 +00:00
Typosaurus 7a93c959df ticket #10 attempt 1 2026-07-19 18:00:04 +00:00
30 changed files with 258 additions and 95 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_ENABLED=false
NGINX_CACHE_MAX_SIZE=1g 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. # Run the app container as this host user so shared files keep dev ownership.
DEVPLACE_UID=1000 DEVPLACE_UID=1000
DEVPLACE_GID=1000 DEVPLACE_GID=1000
+1
View File
@@ -0,0 +1 @@
python3
+1
View File
@@ -0,0 +1 @@
/usr/bin/python3
+1
View File
@@ -0,0 +1 @@
python3
+1
View File
@@ -0,0 +1 @@
lib
+5
View File
@@ -0,0 +1,5 @@
home = /usr/bin
include-system-site-packages = false
version = 3.11.2
executable = /usr/bin/python3.11
command = /usr/bin/python3 -m venv /workspace/repo/.venv
-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_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_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_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 ### Runtime settings
-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_EMBED_URL = f"{INTERNAL_BASE_URL}/openai/v1/embeddings"
INTERNAL_MODEL = "molodetz" INTERNAL_MODEL = "molodetz"
INTERNAL_EMBED_MODEL = "molodetz~embed" 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_CORRECTION_PROMPT = "Leave literary as is, only do punctuation and casing"
DEFAULT_MODIFIER_PROMPT = ( DEFAULT_MODIFIER_PROMPT = (
"Execute what is behind `@ai` (the prompt) and replace that part including `@ai`" "Execute what is behind `@ai` (the prompt) and replace that part including `@ai`"
+2 -1
View File
@@ -16,7 +16,7 @@ from .email import EMAIL_ACCOUNT_DEFAULTS, list_email_accounts, get_email_accoun
from .notifications import NOTIFICATION_TYPES, NOTIFICATION_CHANNELS, _NOTIFICATION_CHANNEL_COLUMNS, _NOTIFICATION_CHANNEL_DEFAULTS, _NOTIFICATION_TYPE_KEYS, _notification_prefs_cache, _notification_default, get_notification_default, set_notification_default, _notification_overrides, notification_enabled, get_notification_prefs, set_notification_pref, reset_notification_prefs, mark_notifications_read_by_target from .notifications import NOTIFICATION_TYPES, NOTIFICATION_CHANNELS, _NOTIFICATION_CHANNEL_COLUMNS, _NOTIFICATION_CHANNEL_DEFAULTS, _NOTIFICATION_TYPE_KEYS, _notification_prefs_cache, _notification_default, get_notification_default, set_notification_default, _notification_overrides, notification_enabled, get_notification_prefs, set_notification_pref, reset_notification_prefs, mark_notifications_read_by_target
from .forks import record_fork, get_fork_parent, count_forks, soft_delete_fork_relations, delete_fork_relations from .forks import record_fork, get_fork_parent, count_forks, soft_delete_fork_relations, delete_fork_relations
from .follows import get_follow_counts, get_follow_list, get_following_among from .follows import get_follow_counts, get_follow_list, get_following_among
from .deepsearch import _ds_now, create_deepsearch_session, update_deepsearch_session, get_deepsearch_session, add_deepsearch_message, get_deepsearch_messages, get_cached_deepsearch_url, upsert_deepsearch_url_cache from .deepsearch import _ds_now, create_deepsearch_session, update_deepsearch_session, get_deepsearch_session, add_deepsearch_message, get_deepsearch_messages, get_cached_deepsearch_url, upsert_deepsearch_url_cache, list_deepsearch_sessions
from .ranking import VOTABLE_TARGETS, STAR_TARGETS, _authors_cache, _ranked_authors, _rank_map, get_top_authors, get_leaderboard, get_user_rank, get_user_stars, clear_user_stars, update_target_stars, soft_delete_engagement, delete_engagement, get_target_owner_uid from .ranking import VOTABLE_TARGETS, STAR_TARGETS, _authors_cache, _ranked_authors, _rank_map, get_top_authors, get_leaderboard, get_user_rank, get_user_stars, clear_user_stars, update_target_stars, soft_delete_engagement, delete_engagement, get_target_owner_uid
from .comments import _drop_blocked, _build_comment_items, load_comments, get_recent_comments_by_target_uids, get_recent_comments_by_post_uids, load_comments_by_target_uids from .comments import _drop_blocked, _build_comment_items, load_comments, get_recent_comments_by_target_uids, get_recent_comments_by_post_uids, load_comments_by_target_uids
from .content import resolve_by_slug, resolve_object_url, get_uids_by_username_match, text_search_clause, get_daily_topic, get_featured_news from .content import resolve_by_slug, resolve_object_url, get_uids_by_username_match, text_search_clause, get_daily_topic, get_featured_news
@@ -182,6 +182,7 @@ __all__ = [
"get_deepsearch_messages", "get_deepsearch_messages",
"get_cached_deepsearch_url", "get_cached_deepsearch_url",
"upsert_deepsearch_url_cache", "upsert_deepsearch_url_cache",
"list_deepsearch_sessions",
"VOTABLE_TARGETS", "VOTABLE_TARGETS",
"STAR_TARGETS", "STAR_TARGETS",
"_authors_cache", "_authors_cache",
+17
View File
@@ -84,6 +84,23 @@ def get_deepsearch_messages(session_uid: str, limit: int = 50) -> list[dict]:
) )
def list_deepsearch_sessions(
owner_kind: str, owner_id: str, limit: int = 50
) -> list[dict]:
if "deepsearch_sessions" not in db.tables:
return []
rows = list(
get_table("deepsearch_sessions").find(
owner_kind=owner_kind,
owner_id=owner_id,
deleted_at=None,
order_by=["created_at desc"],
_limit=limit,
)
)
return sorted(rows, key=lambda r: (r.get("created_at", ""), r.get("uid", "")), reverse=True)
def get_cached_deepsearch_url(url_hash: str) -> dict | None: def get_cached_deepsearch_url(url_hash: str) -> dict | None:
if "deepsearch_url_cache" not in db.tables: if "deepsearch_url_cache" not in db.tables:
return None return None
+2 -3
View File
@@ -1,6 +1,5 @@
# retoor <retoor@molodetz.nl> # retoor <retoor@molodetz.nl>
from devplacepy.config import COMMENT_MAX_LENGTH
from .._shared import COMMENT_TARGETS, GIST_LANGUAGES, PROJECT_TYPES, endpoint, field from .._shared import COMMENT_TARGETS, GIST_LANGUAGES, PROJECT_TYPES, endpoint, field
from devplacepy.constants import TOPICS from devplacepy.constants import TOPICS
@@ -230,7 +229,7 @@ four ways to sign requests.
"textarea", "textarea",
True, True,
"Nice work.", "Nice work.",
f"Body, 3-{COMMENT_MAX_LENGTH} characters.", "Body, 3-1000 characters.",
), ),
field( field(
"target_uid", "target_uid",
@@ -291,7 +290,7 @@ four ways to sign requests.
"textarea", "textarea",
True, True,
"Edited body.", "Edited body.",
f"New body, 3-{COMMENT_MAX_LENGTH} characters.", "New body, 3-1000 characters.",
), ),
], ],
sample_response={ sample_response={
+1 -3
View File
@@ -1,7 +1,5 @@
# retoor <retoor@molodetz.nl> # retoor <retoor@molodetz.nl>
from devplacepy.config import COMMENT_MAX_LENGTH
ROLE_LABELS = {"public": "Public", "user": "Member"} ROLE_LABELS = {"public": "Public", "user": "Member"}
@@ -239,7 +237,7 @@ DEVRANT_GROUPS = {
encoding="form", encoding="form",
params=[ params=[
field("rant_id", "path", type="int", required=True, example="1", description="Rant id."), 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}, sample_response={"success": True},
), ),
+3 -3
View File
@@ -6,7 +6,7 @@ from datetime import datetime
from typing import Literal, Optional from typing import Literal, Optional
from pydantic import BaseModel, Field, field_validator, model_validator from pydantic import BaseModel, Field, field_validator, model_validator
from devplacepy.constants import TOPICS, REACTION_EMOJI 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): def normalize_european_date(value):
@@ -157,7 +157,7 @@ class PostEditForm(BaseModel):
class CommentForm(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) target_uid: str = Field(default="", max_length=36)
post_uid: str = Field(default="", max_length=36) post_uid: str = Field(default="", max_length=36)
target_type: Literal["post", "project", "news", "issue", "gist"] = "post" target_type: Literal["post", "project", "news", "issue", "gist"] = "post"
@@ -172,7 +172,7 @@ class CommentForm(BaseModel):
class CommentEditForm(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): 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): if not is_owner(comment, user):
return dr_error("You can only edit your own comments.", fail_reason="not_owner") return dr_error("You can only edit your own comments.", fail_reason="not_owner")
text = (params.get("comment") or "").strip() text = (params.get("comment") or "").strip()
from devplacepy.config import COMMENT_MAX_LENGTH if len(text) < 1 or len(text) > 1000:
if len(text) < 1 or len(text) > COMMENT_MAX_LENGTH:
return dr_error("Invalid comment length.", fail_reason="length") return dr_error("Invalid comment length.", fail_reason="length")
get_table("comments").update( 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() text = (params.get("comment") or "").strip()
if len(text) < 1: if len(text) < 1:
return dr_error("Your comment is too short.") return dr_error("Your comment is too short.")
from devplacepy.config import COMMENT_MAX_LENGTH if len(text) > 1000:
if len(text) > COMMENT_MAX_LENGTH:
return dr_error("Your comment is too long.") return dr_error("Your comment is too long.")
create_comment_record(request, user, "post", post["uid"], text) create_comment_record(request, user, "post", post["uid"], text)
return dr_ok() return dr_ok()
+31 -2
View File
@@ -11,7 +11,11 @@ from devplacepy import database
from devplacepy.config import DEEPSEARCH_DIR from devplacepy.config import DEEPSEARCH_DIR
from devplacepy.models import DeepsearchChatForm, DeepsearchRunForm from devplacepy.models import DeepsearchChatForm, DeepsearchRunForm
from devplacepy.responses import respond from devplacepy.responses import respond
from devplacepy.schemas import DeepsearchJobOut, DeepsearchSessionOut from devplacepy.schemas import (
DeepsearchJobOut,
DeepsearchListOut,
DeepsearchSessionOut,
)
from devplacepy.seo import base_seo_context, site_url, web_application_schema, website_schema from devplacepy.seo import base_seo_context, site_url, web_application_schema, website_schema
from devplacepy.services.deepsearch.chat import DeepsearchChat from devplacepy.services.deepsearch.chat import DeepsearchChat
from devplacepy.services.deepsearch.export import to_json, to_markdown, to_pdf from devplacepy.services.deepsearch.export import to_json, to_markdown, to_pdf
@@ -100,10 +104,13 @@ async def deepsearch_page(request: Request):
web_application_schema("DeepSearch", description, "/tools/deepsearch", base), web_application_schema("DeepSearch", description, "/tools/deepsearch", base),
], ],
) )
sessions = []
if user:
sessions = database.list_deepsearch_sessions("user", user["uid"])
return templates.TemplateResponse( return templates.TemplateResponse(
request, request,
"tools/deepsearch.html", "tools/deepsearch.html",
{**seo_ctx, "request": request, "user": user}, {**seo_ctx, "request": request, "user": user, "history_sessions": sessions},
) )
@router.post("/run") @router.post("/run")
@@ -197,6 +204,27 @@ def _enqueue(uid: str, payload: dict, owner_kind: str, owner_id: str, query: str
} }
) )
@router.get("/list")
async def deepsearch_list(request: Request):
owner_kind, owner_id = owner_for(request)
sessions = database.list_deepsearch_sessions(owner_kind, owner_id)
return JSONResponse(
DeepsearchListOut.model_validate(
{"sessions": [_session_summary(s) for s in sessions]}
).model_dump(mode="json")
)
def _session_summary(session: dict) -> dict:
return {
"uid": session.get("uid", ""),
"query": session.get("query", ""),
"status": session.get("status", ""),
"created_at": session.get("created_at", ""),
}
@router.get("/{uid}") @router.get("/{uid}")
async def deepsearch_status(request: Request, uid: str): async def deepsearch_status(request: Request, uid: str):
job = queue.get_job(uid) job = queue.get_job(uid)
@@ -322,6 +350,7 @@ async def deepsearch_export_pdf(request: Request, uid: str):
raise not_found("Report not ready") raise not_found("Report not ready")
return Response(content=to_pdf(report), media_type="application/pdf") return Response(content=to_pdf(report), media_type="application/pdf")
@router.websocket("/{uid}/ws") @router.websocket("/{uid}/ws")
async def deepsearch_ws(websocket: WebSocket, uid: str): async def deepsearch_ws(websocket: WebSocket, uid: str):
await websocket.accept() await websocket.accept()
+2
View File
@@ -79,7 +79,9 @@ from devplacepy.schemas.containers import (
from devplacepy.schemas.jobs import ( from devplacepy.schemas.jobs import (
DbQueryJobOut, DbQueryJobOut,
DeepsearchJobOut, DeepsearchJobOut,
DeepsearchListOut,
DeepsearchSessionOut, DeepsearchSessionOut,
DeepsearchSessionSummary,
ForkJobOut, ForkJobOut,
PlanningJobOut, PlanningJobOut,
SeoJobOut, SeoJobOut,
+11
View File
@@ -142,6 +142,17 @@ class DeepsearchSessionOut(_Out):
completed_at: Optional[str] = None completed_at: Optional[str] = None
class DeepsearchSessionSummary(_Out):
uid: str = ""
query: str = ""
status: str = ""
created_at: str = ""
class DeepsearchListOut(_Out):
sessions: list = []
class DbQueryJobOut(_Out): class DbQueryJobOut(_Out):
uid: str = "" uid: str = ""
kind: str = "" kind: str = ""
+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")) DM_POLL_SECONDS = int(os.environ.get("DM_POLL_SECONDS", "10"))
BOT_USERNAME = os.environ.get("BOT_USERNAME", "") 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 MESSAGE_CHAR_LIMIT = 2000
PART_SUFFIX_RESERVE = 12 PART_SUFFIX_RESERVE = 12
PART_DELIVERY_DELAY = 0.5 PART_DELIVERY_DELAY = 0.5
@@ -2,7 +2,6 @@
from __future__ import annotations from __future__ import annotations
from devplacepy.config import COMMENT_MAX_LENGTH
from ..spec import Action from ..spec import Action
from ._shared import ATTACHMENTS, TARGET_TYPE, body, confirm, path 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", summary="Edit the body of one of your own comments",
params=( params=(
path("comment_uid", "Uid of the comment."), 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( Action(
@@ -108,6 +108,18 @@ TOOLS_ACTIONS: tuple[Action, ...] = (
params=(path("uid", "DeepSearch job uid returned by deepsearch."),), params=(path("uid", "DeepSearch job uid returned by deepsearch."),),
requires_auth=False, requires_auth=False,
), ),
Action(
name="deepsearch_list",
method="GET",
path="/tools/deepsearch/list",
summary="List the user's past DeepSearch sessions",
description=(
"Returns the signed-in user's deep research session history, newest first, each "
"with its uid, query, status and created_at."
),
params=(),
requires_auth=True,
),
Action( Action(
name="isslop", name="isslop",
method="POST", method="POST",
-2
View File
@@ -83,8 +83,6 @@ export class CommentManager {
textarea.className = "emoji-picker-target"; textarea.className = "emoji-picker-target";
textarea.value = text.dataset.raw || text.textContent; textarea.value = text.dataset.raw || text.textContent;
textarea.rows = 3; textarea.rows = 3;
const ref = document.querySelector("[data-comment-maxlength]");
textarea.maxLength = ref ? parseInt(ref.dataset.commentMaxlength, 10) : 1000;
form.appendChild(textarea); form.appendChild(textarea);
const actions = document.createElement("div"); const actions = document.createElement("div");
+2 -2
View File
@@ -1,9 +1,9 @@
{% if user %} {% 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_uid" value="{{ _comment_target_uid }}">
<input type="hidden" name="target_type" value="{{ _comment_target_type }}"> <input type="hidden" name="target_type" value="{{ _comment_target_type }}">
{% set _user = user %}{% set _size = 32 %}{% set _size_class = "sm" %}{% include "_avatar_link.html" %} {% 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"> <div class="comment-form-actions">
<dp-upload multiple <dp-upload multiple
max-size="{{ max_upload_size_mb() }}" max-size="{{ max_upload_size_mb() }}"
@@ -95,6 +95,22 @@
<p>Research complete.</p> <p>Research complete.</p>
<a class="btn btn-primary" data-deepsearch-open href="#">Open report and chat</a> <a class="btn btn-primary" data-deepsearch-open href="#">Open report and chat</a>
</section> </section>
{% if user and history_sessions %}
<section class="ds-history mt-6">
<h2 class="text-lg font-semibold mb-2">My Research History</h2>
<ul class="space-y-1">
{% for session in history_sessions %}
<li>
<a href="/tools/deepsearch/{{ session.uid }}/session"
class="text-blue-600 hover:underline text-sm">
{{ session.query }} — <span class="text-gray-500">{{ session.created_at[:16].replace('T', ' ') }}</span>
</a>
</li>
{% endfor %}
</ul>
</section>
{% endif %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
+1 -2
View File
@@ -6,7 +6,7 @@ import jinja2
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from markupsafe import Markup, escape from markupsafe import Markup, escape
from devplacepy.cache import TTLCache 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.constants import TOPICS, REACTION_EMOJI
from devplacepy.database import get_int_setting, get_setting, get_table from devplacepy.database import get_int_setting, get_setting, get_table
from devplacepy.avatar import avatar_url, avatar_seed 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["guest_disabled"] = guest_disabled
templates.env.globals["is_online"] = presence.is_online templates.env.globals["is_online"] = presence.is_online
templates.env.globals["presence_timeout"] = PRESENCE_TIMEOUT_SECONDS templates.env.globals["presence_timeout"] = PRESENCE_TIMEOUT_SECONDS
templates.env.globals["comment_max_length"] = COMMENT_MAX_LENGTH
from devplacepy.docs_devrant import devrant_endpoints from devplacepy.docs_devrant import devrant_endpoints
+16 -28
View File
@@ -1,28 +1,16 @@
2026-07-19T08:11:05 INFO logging initialised at /workspace/repo/dpc.log 2026-07-19T17:32:58 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T08:11:05 DEBUG model=molodetz-pro fps=30 2026-07-19T17:32:58 DEBUG model=molodetz-pro fps=30
2026-07-19T08:11:05 INFO read task from file: /workspace/prompts/research-1.txt 2026-07-19T17:32:58 INFO read task from file: /workspace/prompts/research-9.txt
2026-07-19T08:11:05 INFO settings merged: model=<default> allow=0 deny=0 ask=0 2026-07-19T17:32:58 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-19T17:38:24 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T15:03:09 DEBUG model=molodetz-pro fps=30 2026-07-19T17:38:24 DEBUG model=molodetz-pro fps=30
2026-07-19T15:03:09 INFO read task from file: /workspace/prompts/research-2.txt 2026-07-19T17:38:24 INFO read task from file: /workspace/prompts/execution-1.txt
2026-07-19T15:03:09 INFO settings merged: model=<default> allow=0 deny=0 ask=0 2026-07-19T17:38:24 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-19T18:33:02 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T15:40:16 DEBUG model=molodetz-pro fps=30 2026-07-19T18:33:02 DEBUG model=molodetz-pro fps=30
2026-07-19T15:40:16 INFO read task from file: /workspace/prompts/research-3.txt 2026-07-19T18:33:02 INFO read task from file: /workspace/prompts/execution-2.txt
2026-07-19T15:40:16 INFO settings merged: model=<default> allow=0 deny=0 ask=0 2026-07-19T18:33:02 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-19T19:00:28 INFO logging initialised at /workspace/repo/dpc.log
2026-07-19T17:35:33 DEBUG model=molodetz-pro fps=30 2026-07-19T19:00:28 DEBUG model=molodetz-pro fps=30
2026-07-19T17:35:33 INFO read task from file: /workspace/prompts/research-4.txt 2026-07-19T19:00:28 INFO read task from file: /workspace/prompts/execution-3.txt
2026-07-19T17:35:33 INFO settings merged: model=<default> allow=0 deny=0 ask=0 2026-07-19T19:00:28 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
-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, ( assert u1 in r.text and u2 in r.text, (
"both attachments must be linked and displayed on the comment" "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) assert r.status_code in (400, 422)
refresh_snapshot() refresh_snapshot()
assert get_table("comments").find_one(uid=comment["uid"])["content"] == "Long enough body" 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"
+129
View File
@@ -0,0 +1,129 @@
# retoor <retoor@molodetz.nl>
import time
import pytest
import requests
from tests.conftest import BASE_URL
from devplacepy.database import (
create_deepsearch_session,
get_table,
refresh_snapshot,
set_setting,
)
from devplacepy.services.jobs import queue
JSON = {"Accept": "application/json"}
_counter = [0]
@pytest.fixture(scope="module", autouse=True)
def _settings(app_server):
set_setting("rate_limit_per_minute", "1000000")
set_setting("registration_open", "1")
yield
def _user():
_counter[0] += 1
name = f"dsl{int(time.time() * 1000)}{_counter[0]}"
requests.post(
f"{BASE_URL}/auth/signup",
data={
"username": name,
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
},
allow_redirects=True,
)
refresh_snapshot()
user = get_table("users").find_one(username=name)
return user["uid"], user["api_key"]
def _seed_session(owner_id, query="test query"):
uid = queue.enqueue(
"deepsearch",
{"query": query, "depth": 1, "max_pages": 5},
"user",
owner_id,
"DeepSearch",
)
create_deepsearch_session(
uid, "user", owner_id, query, 1, 5, f"ds_{uid.replace('-', '')}"
)
refresh_snapshot()
return uid
def _clear():
refresh_snapshot()
jobs = get_table("jobs")
for row in list(jobs.find(kind="deepsearch")):
jobs.delete(uid=row["uid"])
sessions = get_table("deepsearch_sessions")
for row in list(sessions.find()):
sessions.delete(uid=row["uid"])
def test_list_returns_own_sessions(app_server):
try:
owner_uid, api_key = _user()
uid = _seed_session(owner_uid, query="first research")
headers = {"X-API-KEY": api_key, **JSON}
r = requests.get(f"{BASE_URL}/tools/deepsearch/list", headers=headers)
assert r.status_code == 200, r.text
body = r.json()
assert "sessions" in body
assert len(body["sessions"]) == 1
assert body["sessions"][0]["uid"] == uid
assert body["sessions"][0]["query"] == "first research"
assert body["sessions"][0]["status"] == "pending"
assert "created_at" in body["sessions"][0]
finally:
_clear()
def test_list_multiple_sessions_sorted_newest_first(app_server):
try:
owner_uid, api_key = _user()
uids = []
for i in range(3):
uids.append(_seed_session(owner_uid, query=f"research {i}"))
headers = {"X-API-KEY": api_key, **JSON}
r = requests.get(f"{BASE_URL}/tools/deepsearch/list", headers=headers)
assert r.status_code == 200, r.text
body = r.json()
assert len(body["sessions"]) == 3
uids_in_response = [s["uid"] for s in body["sessions"]]
assert sorted(uids_in_response, reverse=True) == uids_in_response
actual_queries = [s["query"] for s in body["sessions"]]
assert len(set(actual_queries)) == 3
finally:
_clear()
def test_list_does_not_include_other_users_sessions(app_server):
try:
owner_uid, api_key = _user()
_seed_session(owner_uid, query="my research")
other_uid, _ = _user()
_seed_session(other_uid, query="other research")
headers = {"X-API-KEY": api_key, **JSON}
r = requests.get(f"{BASE_URL}/tools/deepsearch/list", headers=headers)
assert r.status_code == 200, r.text
body = r.json()
assert len(body["sessions"]) == 1
assert body["sessions"][0]["query"] == "my research"
finally:
_clear()
def test_list_empty_for_no_sessions(app_server):
owner_uid, api_key = _user()
headers = {"X-API-KEY": api_key, **JSON}
r = requests.get(f"{BASE_URL}/tools/deepsearch/list", headers=headers)
assert r.status_code == 200, r.text
body = r.json()
assert "sessions" in body
assert len(body["sessions"]) == 0