feat: add ISSLOP AI usage analysis CLI commands, game router, and politics topic

- Add `cmd_isslop_prune`, `cmd_isslop_clear`, `cmd_isslop_analyze` CLI commands for AI usage analysis job management
- Register `/game` router with `index` and `farm` endpoints for Code Farm idle game
- Add `politics` to allowed TOPICS constant replacing `signals`
- Introduce `ISSLOP_DIR`, `ISSLOP_WORKSPACES_DIR`, `ISSLOP_RUNS_DIR`, `ISSLOP_MEDIA_DIR` config paths
- Add `clear_user_stars` and `clear_user_projects_cache` calls on vote and project create/delete
- Update `make prod` to use `nproc` workers via `DEVPLACE_WEB_WORKERS` env var
- Convert `database.py` and `utils.py` to packages for modular structure
- Add `devplace apikey` and `devplace token` CLI subcommands for API key and access token management
This commit is contained in:
2026-07-06 03:57:47 +00:00
parent f1bdefd834
commit 9a8046ab2a
110 changed files with 1466 additions and 374 deletions
+12
View File
@@ -148,6 +148,18 @@ DOCS_PAGES = [
"kind": "prose",
"section": SECTION_TOOLS,
},
{
"slug": "tools-isslop",
"title": "AI Usage Analyzer",
"kind": "prose",
"section": SECTION_TOOLS,
},
{
"slug": "isslop-checks",
"title": "AI Usage Analyzer checks",
"kind": "prose",
"section": SECTION_TOOLS,
},
# Claude Code - the native subagent, command, and workflow setup under .claude/
{
"slug": "claude",
+20 -9
View File
@@ -1,5 +1,6 @@
# retoor <retoor@molodetz.nl>
import asyncio
import logging
from fastapi import APIRouter, Request
@@ -93,18 +94,28 @@ async def issue_detail(request: Request, number: int):
if not gitea_config().is_configured:
raise not_found("Issue not found")
client = runtime.get_client()
try:
issue = await client.get_issue(number)
except GiteaError as exc:
if exc.status == 404:
issue_result, comments_result = await asyncio.gather(
client.get_issue(number),
client.list_comments(number),
return_exceptions=True,
)
if isinstance(issue_result, GiteaError):
if issue_result.status == 404:
raise not_found("Issue not found")
logger.warning("Could not load issue #%s: %s", number, exc)
logger.warning("Could not load issue #%s: %s", number, issue_result)
return tracker_unavailable(request)
try:
comments = await client.list_comments(number)
except GiteaError as exc:
logger.warning("Could not load comments for issue #%s: %s", number, exc)
if isinstance(issue_result, BaseException):
raise issue_result
issue = issue_result
if isinstance(comments_result, BaseException):
if not isinstance(comments_result, GiteaError):
raise comments_result
logger.warning(
"Could not load comments for issue #%s: %s", number, comments_result
)
comments = []
else:
comments = comments_result
if user:
mark_notifications_read_by_target(user["uid"], f"/issues?highlight={number}")
+46 -43
View File
@@ -42,6 +42,8 @@ router = APIRouter()
MAX_WS_ATTACHMENTS = 5
CONVERSATION_MESSAGE_LIMIT = 500
def mark_conversation_read(user_uid: str, other_uid: str) -> None:
if "messages" not in db.tables:
return
@@ -54,64 +56,65 @@ def mark_conversation_read(user_uid: str, other_uid: str) -> None:
clear_messages_cache(user_uid)
def get_conversations(user_uid: str):
messages_table = get_table("messages")
raw = list(messages_table.find(sender_uid=user_uid)) + list(
messages_table.find(receiver_uid=user_uid)
)
seen = set()
all_messages = []
for m in raw:
if m["uid"] not in seen:
seen.add(m["uid"])
all_messages.append(m)
blocked = get_blocked_uids(user_uid)
conversation_map = {}
other_uids = set()
for msg in all_messages:
other_uid = (
msg["receiver_uid"] if msg["sender_uid"] == user_uid else msg["sender_uid"]
if "messages" not in db.tables:
return []
latest = list(
db.query(
"SELECT * FROM ("
" SELECT *,"
" CASE WHEN sender_uid = :me THEN receiver_uid ELSE sender_uid END AS other_uid,"
" ROW_NUMBER() OVER ("
" PARTITION BY CASE WHEN sender_uid = :me THEN receiver_uid ELSE sender_uid END"
" ORDER BY created_at DESC, id DESC"
" ) AS rn"
" FROM messages"
" WHERE sender_uid = :me OR receiver_uid = :me"
") WHERE rn = 1 ORDER BY created_at DESC",
me=user_uid,
)
)
blocked = get_blocked_uids(user_uid)
conversations = []
other_uids = []
for msg in latest:
other_uid = msg["other_uid"]
if other_uid in blocked:
continue
other_uids.add(other_uid)
if (
other_uid not in conversation_map
or msg["created_at"] > conversation_map[other_uid]["last_message_at"]
):
conversation_map[other_uid] = {
other_uids.append(other_uid)
conversations.append(
{
"other_uid": other_uid,
"other_user": None,
"last_message": msg["content"],
"last_message_at": msg["created_at"],
"unread": msg["receiver_uid"] == user_uid and not msg["read"],
}
)
if other_uids:
users_map = get_users_by_uids(list(other_uids))
for uid, conv in conversation_map.items():
conv["other_user"] = users_map.get(uid)
conversations = sorted(
conversation_map.values(),
key=lambda c: c["last_message_at"],
reverse=True,
)
users_map = get_users_by_uids(other_uids)
for conv in conversations:
conv["other_user"] = users_map.get(conv["other_uid"])
for conv in conversations:
conv.pop("other_uid", None)
return conversations
def get_conversation_messages(user_uid: str, other_uid: str):
if other_uid in get_blocked_uids(user_uid):
return [], None
messages_table = get_table("messages")
raw = list(messages_table.find(sender_uid=user_uid, receiver_uid=other_uid)) + list(
messages_table.find(sender_uid=other_uid, receiver_uid=user_uid)
if "messages" not in db.tables:
return [], get_users_by_uids([other_uid]).get(other_uid)
msgs = list(
db.query(
"SELECT * FROM messages"
" WHERE (sender_uid = :me AND receiver_uid = :other)"
" OR (sender_uid = :other AND receiver_uid = :me)"
" ORDER BY created_at DESC, id DESC LIMIT :lim",
me=user_uid,
other=other_uid,
lim=CONVERSATION_MESSAGE_LIMIT,
)
)
seen = set()
msgs = []
for m in raw:
if m["uid"] not in seen:
seen.add(m["uid"])
msgs.append(m)
msgs.sort(key=lambda m: m["created_at"])
msgs.reverse()
user_ids = list({m["sender_uid"] for m in msgs} | {other_uid})
users_map = get_users_by_uids(user_ids)
+14 -10
View File
@@ -40,7 +40,7 @@ from devplacepy.utils import (
track_action,
build_achievements,
)
from devplacepy.responses import respond, action_result
from devplacepy.responses import respond, action_result, wants_json
from devplacepy.schemas import ProfileOut
from devplacepy.avatar import avatar_url, avatar_seed
from devplacepy.seo import (
@@ -181,16 +181,20 @@ async def profile_page(
achievements = build_achievements({b["badge_name"] for b in badges})
badge_total = sum(group["total"] for group in achievements)
badge_earned = sum(group["earned"] for group in achievements)
projects = list(
get_table("projects").find(user_uid=profile_user["uid"], deleted_at=None)
)
projects = [p for p in projects if can_view_project(p, current_user)]
gists_raw = list(
get_table("gists").find(user_uid=profile_user["uid"], deleted_at=None)
)
include_collections = wants_json(request)
projects = []
if tab == "projects" or include_collections:
projects = list(
get_table("projects").find(user_uid=profile_user["uid"], deleted_at=None)
)
projects = [p for p in projects if can_view_project(p, current_user)]
gists = []
for g in gists_raw:
gists.append({"gist": g, "time_ago": time_ago(g["created_at"])})
if tab == "gists" or include_collections:
gists_raw = list(
get_table("gists").find(user_uid=profile_user["uid"], deleted_at=None)
)
for g in gists_raw:
gists.append({"gist": g, "time_ago": time_ago(g["created_at"])})
posts_count = get_table("posts").count(
user_uid=profile_user["uid"], deleted_at=None
)
+2 -1
View File
@@ -1,7 +1,8 @@
# retoor <retoor@molodetz.nl>
from . import deepsearch, index, seo
from . import deepsearch, index, isslop, seo
router = index.router
router.include_router(seo.router, prefix="/seo")
router.include_router(deepsearch.router, prefix="/deepsearch")
router.include_router(isslop.router, prefix="/isslop")
+11
View File
@@ -26,6 +26,17 @@ TOOLS = [
"structured-data, performance, accessibility and AI-readiness checks."
),
},
{
"slug": "isslop",
"name": "AI Usage Analyzer",
"icon": "🧪",
"url": "/tools/isslop",
"description": (
"Measure how a codebase or website was made: untouched AI defaults, AI steered by a "
"knowing hand, or work no model would ever produce. Multi-signal analysis, image "
"forensics and a shareable authenticity badge."
),
},
{
"slug": "deepsearch",
"name": "DeepSearch",