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:
@@ -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",
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user