fix: correct typo in commit message from "UPDATXE" to proper format

This commit is contained in:
2026-06-09 20:52:51 +00:00
parent caeefea042
commit e698663975
15 changed files with 495 additions and 19 deletions
+48 -6
View File
@@ -402,6 +402,10 @@ def backfill_api_keys() -> int:
users = db["users"]
if not users.has_column("api_key"):
users.create_column_by_example("api_key", "")
if not users.has_column("cust_disable_global"):
users.create_column_by_example("cust_disable_global", 0)
if not users.has_column("cust_disable_pagetype"):
users.create_column_by_example("cust_disable_pagetype", 0)
import uuid_utils
updated = 0
@@ -493,13 +497,14 @@ def _in_clause(uids, prefix="p"):
def get_users_by_uids(uids):
if not uids:
if not uids or "users" not in db.tables:
return {}
users = db["users"]
if "uid" not in users.columns:
return {}
seen = set()
unique = [u for u in uids if u not in seen and not seen.add(u)]
return {
u["uid"]: u for u in db["users"].find(db["users"].table.columns.uid.in_(unique))
}
return {u["uid"]: u for u in users.find(users.table.columns.uid.in_(unique))}
def get_comment_counts_by_post_uids(post_uids):
@@ -874,6 +879,37 @@ def _customization_key(owner_kind: str, owner_id: str, page_type: str) -> str:
return f"{owner_kind}\x1f{owner_id}\x1f{page_type}"
CUSTOMIZATION_PREF_COLUMNS = {
"global": "cust_disable_global",
"pagetype": "cust_disable_pagetype",
}
def get_customization_prefs(owner_kind: str, owner_id: str) -> dict:
if owner_kind != "user" or "users" not in db.tables:
return {"disable_global": False, "disable_pagetype": False}
user = db["users"].find_one(uid=owner_id)
if user is None:
return {"disable_global": False, "disable_pagetype": False}
return {
"disable_global": bool(user.get("cust_disable_global", 0)),
"disable_pagetype": bool(user.get("cust_disable_pagetype", 0)),
}
def set_customization_pref(owner_id: str, category: str, disabled: bool) -> None:
column = CUSTOMIZATION_PREF_COLUMNS.get(category)
if column is None:
raise ValueError(f"Unknown customization category: {category}")
from devplacepy.utils import clear_user_cache
get_table("users").update(
{"uid": owner_id, column: 1 if disabled else 0}, ["uid"]
)
clear_user_cache(owner_id)
bump_cache_version("customizations")
def get_custom_overrides(owner_kind: str, owner_id: str, page_type: str) -> dict:
sync_local_cache("customizations", _customizations_cache)
key = _customization_key(owner_kind, owner_id, page_type)
@@ -882,6 +918,7 @@ def get_custom_overrides(owner_kind: str, owner_id: str, page_type: str) -> dict
return cached
result = {"css": "", "js": ""}
if "user_customizations" in db.tables:
prefs = get_customization_prefs(owner_kind, owner_id)
scopes = (CUSTOMIZATION_GLOBAL_SCOPE, page_type)
rows = db["user_customizations"].find(
owner_kind=owner_kind,
@@ -892,8 +929,13 @@ def get_custom_overrides(owner_kind: str, owner_id: str, page_type: str) -> dict
for row in rows:
lang = row.get("lang")
scope = row.get("scope")
if lang in pieces and scope in scopes:
pieces[lang][scope] = row.get("code") or ""
if lang not in pieces or scope not in scopes:
continue
if scope == CUSTOMIZATION_GLOBAL_SCOPE and prefs["disable_global"]:
continue
if scope != CUSTOMIZATION_GLOBAL_SCOPE and prefs["disable_pagetype"]:
continue
pieces[lang][scope] = row.get("code") or ""
for lang in CUSTOMIZATION_LANGS:
ordered = [pieces[lang][scope] for scope in scopes if scope in pieces[lang]]
result[lang] = "\n".join(part for part in ordered if part.strip())
+64
View File
@@ -1422,6 +1422,62 @@ four ways to sign requests.
],
sample_response={"api_key": "NEW_UUID"},
),
endpoint(
id="profile-customization-global",
method="POST",
path="/profile/{username}/customization/global",
title="Toggle site-wide customizations",
summary="Show or suppress your site-wide custom CSS and JS without deleting it. Admins may target any user.",
auth="user",
encoding="form",
destructive=True,
params=[
field(
"username",
"path",
"string",
True,
"bob_test",
"Profile owner. Must be yourself unless you are an admin.",
),
field(
"value",
"form",
"boolean",
False,
"0",
"1 to show your site-wide customizations, 0 to suppress them.",
),
],
),
endpoint(
id="profile-customization-pagetype",
method="POST",
path="/profile/{username}/customization/pagetype",
title="Toggle per-page customizations",
summary="Show or suppress your per-page custom CSS and JS without deleting it. Admins may target any user.",
auth="user",
encoding="form",
destructive=True,
params=[
field(
"username",
"path",
"string",
True,
"bob_test",
"Profile owner. Must be yourself unless you are an admin.",
),
field(
"value",
"form",
"boolean",
False,
"0",
"1 to show your per-page customizations, 0 to suppress them.",
),
],
),
endpoint(
id="follow-user",
method="POST",
@@ -3185,6 +3241,14 @@ _ACTION_RESPONSES = {
),
"gists-delete": ("/gists", None),
"profile-update": ("/profile/YOUR_USERNAME", None),
"profile-customization-global": (
"/profile/YOUR_USERNAME",
{"url": "/profile/YOUR_USERNAME", "cust_disable_global": True},
),
"profile-customization-pagetype": (
"/profile/YOUR_USERNAME",
{"url": "/profile/YOUR_USERNAME", "cust_disable_pagetype": True},
),
"follow-user": ("/profile/bob_test", None),
"unfollow-user": ("/profile/bob_test", None),
"messages-send": ("/messages?with_uid=RECEIVER_UID", {"uid": "MESSAGE_UID"}),
+4
View File
@@ -169,6 +169,10 @@ class ProjectFlagForm(BaseModel):
value: bool = False
class CustomizationToggleForm(BaseModel):
value: bool = False
class ForkForm(BaseModel):
title: str = Field(min_length=1, max_length=200)
+62 -2
View File
@@ -1,11 +1,13 @@
import logging
from typing import Annotated
from fastapi import APIRouter, Request, Form
from devplacepy.models import ProfileForm
from fastapi.responses import HTMLResponse, JSONResponse
from devplacepy.models import ProfileForm, CustomizationToggleForm
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from devplacepy.database import (
get_table,
db,
get_customization_prefs,
set_customization_pref,
get_user_stars,
get_user_rank,
get_comment_counts_by_post_uids,
@@ -250,6 +252,12 @@ async def profile_page(
if (is_owner or viewer_is_admin)
else None
)
can_manage_customization = is_owner or viewer_is_admin
customization_prefs = (
get_customization_prefs("user", profile_user["uid"])
if can_manage_customization
else {"disable_global": False, "disable_pagetype": False}
)
base = site_url(request)
robots = "noindex,follow" if posts_count < 2 else "index,follow"
@@ -296,6 +304,9 @@ async def profile_page(
"is_owner": is_owner,
"can_view_api_key": can_view_api_key,
"api_key": api_key,
"can_manage_customization": can_manage_customization,
"cust_disable_global": customization_prefs["disable_global"],
"cust_disable_pagetype": customization_prefs["disable_pagetype"],
"ai_quota": ai_quota,
"activities": activities,
"rank": rank,
@@ -339,3 +350,52 @@ async def regenerate_api_key(request: Request):
clear_user_cache(user["uid"])
logger.info(f"API key regenerated for {user['username']}")
return JSONResponse({"api_key": new_key})
def _resolve_customization_target(request: Request, username: str):
from devplacepy.responses import wants_json, json_error
current_user = require_user(request)
target = get_table("users").find_one(username=username)
if not target:
raise not_found("User not found")
is_owner = current_user["uid"] == target["uid"]
is_admin = current_user.get("role") == "Admin"
if not (is_owner or is_admin):
if wants_json(request):
return None, json_error(403, "Not allowed")
return None, RedirectResponse(url=f"/profile/{username}", status_code=302)
return target, None
def _toggle_customization(
request: Request, username: str, category: str, value: bool
):
target, denied = _resolve_customization_target(request, username)
if denied is not None:
return denied
set_customization_pref(target["uid"], category, disabled=not value)
column = "cust_disable_global" if category == "global" else "cust_disable_pagetype"
logger.info(
f"Customization {category} {'enabled' if value else 'disabled'} for {target['username']}"
)
url = f"/profile/{target['username']}"
return action_result(request, url, data={"url": url, column: not value})
@router.post("/{username}/customization/global")
async def set_customization_global(
request: Request,
username: str,
data: Annotated[CustomizationToggleForm, Form()],
):
return _toggle_customization(request, username, "global", data.value)
@router.post("/{username}/customization/pagetype")
async def set_customization_pagetype(
request: Request,
username: str,
data: Annotated[CustomizationToggleForm, Form()],
):
return _toggle_customization(request, username, "pagetype", data.value)
+3
View File
@@ -505,6 +505,9 @@ class ProfileOut(_Out):
is_owner: bool = False
can_view_api_key: bool = False
api_key: Optional[str] = None
can_manage_customization: bool = False
cust_disable_global: bool = False
cust_disable_pagetype: bool = False
ai_quota: Optional[dict] = None
activities: list[Any] = []
rank: Optional[int] = None
@@ -131,4 +131,31 @@ CUSTOMIZATION_ACTIONS: tuple[Action, ...] = (
),
),
),
Action(
name="customize_set_enabled",
method="LOCAL",
path="",
summary="Show or suppress the user's customizations without deleting them",
description=(
"Toggles whether the user's saved customizations render, keeping the stored code intact. "
"category='global' controls the site-wide customizations; category='pagetype' controls every "
"per-page-type customization at once. enabled=false hides them; enabled=true shows them again. "
"Mirrors the toggle buttons on the user's profile page."
),
handler="customization",
requires_auth=True,
params=(
arg(
"category",
"Which set to toggle: 'global' (whole site) or 'pagetype' (all per-page customizations).",
required=True,
),
arg(
"enabled",
"true to show the customizations, false to suppress them.",
required=True,
kind="boolean",
),
),
),
)
+3 -1
View File
@@ -137,7 +137,9 @@ SYSTEM_PROMPT = (
"after the application boots, with access to window, document and the global 'app'. After saving, "
"call reload_page so the server-applied version is shown. List existing customizations with "
"customize_list, and remove them with customize_reset (confirm=true; scope='all' removes "
"everything) to restore the default look and behaviour.\n\n"
"everything) to restore the default look and behaviour. To hide saved customizations WITHOUT "
"deleting them (the same toggles on the user's profile page), call customize_set_enabled with "
"category='global' or 'pagetype' and enabled=false; enabled=true brings them back.\n\n"
"USER-DEFINED TOOLS (VIBE TOOLS)\n"
"The user can invent new tools for you by describing them ('when I say X, do Y'). When they do, "
"call tool_create with a short trigger-oriented description (so you know when to use it), the "
@@ -8,10 +8,13 @@ from typing import Any
from devplacepy.database import (
CUSTOMIZATION_LANGS,
CUSTOMIZATION_PREF_COLUMNS,
delete_custom_override,
get_custom_override,
get_customization_prefs,
list_custom_overrides,
set_custom_override,
set_customization_pref,
)
from devplacepy.services.devii.errors import ToolInputError
@@ -36,6 +39,8 @@ class CustomizationController:
return self._set(arguments, "js", "js")
if name == "customize_reset":
return self._reset(arguments)
if name == "customize_set_enabled":
return self._set_enabled(arguments)
raise ToolInputError(f"Unknown customization tool: {name}")
def _scope(self, arguments: dict[str, Any]) -> str:
@@ -68,8 +73,15 @@ class CustomizationController:
}
for row in rows
]
prefs = get_customization_prefs(self._owner_kind, self._owner_id)
return json.dumps(
{"status": "success", "customizations": items}, ensure_ascii=False
{
"status": "success",
"customizations": items,
"disabled_global": prefs["disable_global"],
"disabled_pagetype": prefs["disable_pagetype"],
},
ensure_ascii=False,
)
def _get(self, arguments: dict[str, Any]) -> str:
@@ -111,6 +123,36 @@ class CustomizationController:
ensure_ascii=False,
)
def _category(self, arguments: dict[str, Any]) -> str:
category = str(arguments.get("category", "")).strip().lower()
if category not in CUSTOMIZATION_PREF_COLUMNS:
raise ToolInputError("'category' must be 'global' or 'pagetype'.")
return category
def _enabled(self, arguments: dict[str, Any]) -> bool:
raw = arguments.get("enabled")
if isinstance(raw, bool):
return raw
return str(raw).strip().lower() in ("true", "1", "yes", "on")
def _set_enabled(self, arguments: dict[str, Any]) -> str:
if self._owner_kind != "user":
raise ToolInputError(
"Suppressing customizations is only available for signed-in users."
)
category = self._category(arguments)
enabled = self._enabled(arguments)
set_customization_pref(self._owner_id, category, disabled=not enabled)
return json.dumps(
{
"status": "success",
"category": category,
"enabled": enabled,
"note": "Saved. Call reload_page so the user sees it applied by the server.",
},
ensure_ascii=False,
)
def _reset(self, arguments: dict[str, Any]) -> str:
scope = self._scope(arguments)
lang = self._lang(arguments)
+43
View File
@@ -367,6 +367,49 @@ a.profile-stat-value:hover {
color: var(--text-secondary);
}
.customization-card {
margin-top: 1rem;
padding: 0.875rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--bg-elevated, var(--bg-card));
}
.customization-head {
margin-bottom: 0.5rem;
}
.customization-title {
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-secondary);
}
.customization-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
padding: 0.4rem 0;
}
.customization-label {
font-size: 0.8125rem;
color: var(--text-primary);
}
.customization-row form {
margin: 0;
}
.customization-hint {
margin-top: 0.5rem;
font-size: 0.75rem;
color: var(--text-secondary);
}
.follow-row {
display: flex;
align-items: center;
+2
View File
@@ -16,6 +16,7 @@ import { ReactionBar } from "./ReactionBar.js";
import { BookmarkManager } from "./BookmarkManager.js";
import { PollManager } from "./PollManager.js";
import { ApiKeyManager } from "./ApiKeyManager.js";
import { CustomizationToggle } from "./CustomizationToggle.js";
import { UserAiUsage } from "./UserAiUsage.js";
import { Tabs } from "./Tabs.js";
import { DeviiTerminal } from "./DeviiTerminal.js";
@@ -47,6 +48,7 @@ class Application {
this.bookmarks = new BookmarkManager();
this.polls = new PollManager();
this.apiKey = new ApiKeyManager();
this.customizationToggle = new CustomizationToggle();
this.userAiUsage = new UserAiUsage();
this.tabs = new Tabs();
this.windows = new WindowManager();
+23
View File
@@ -160,6 +160,29 @@
<p class="api-key-hint">Use this key with <code>X-API-KEY</code>, <code>Authorization: Bearer</code>, or HTTP Basic auth on any request. {% if is_owner %}Regenerating immediately invalidates the old key.{% endif %}</p>
</div>
{% endif %}
{% if can_manage_customization %}
<div class="customization-card" data-customization data-username="{{ profile_user['username'] }}">
<div class="customization-head">
<span class="customization-title">{% if is_owner %}My customizations{% else %}{{ profile_user['username'] }}'s customizations{% endif %}</span>
</div>
<div class="customization-row">
<span class="customization-label">Site-wide custom CSS and JS</span>
<form method="POST" action="/profile/{{ profile_user['username'] }}/customization/global" data-customization-form>
<input type="hidden" name="value" value="{{ 1 if cust_disable_global else 0 }}">
<button type="submit" class="btn btn-sm {% if cust_disable_global %}btn-primary{% endif %}" data-customization-toggle="global" data-confirm="{% if cust_disable_global %}Re-enable your site-wide custom CSS and JS?{% else %}Disable your site-wide custom CSS and JS? Your saved code is kept and can be re-enabled.{% endif %}">{% if cust_disable_global %}Enable{% else %}Disable{% endif %}</button>
</form>
</div>
<div class="customization-row">
<span class="customization-label">Per-page custom CSS and JS</span>
<form method="POST" action="/profile/{{ profile_user['username'] }}/customization/pagetype" data-customization-form>
<input type="hidden" name="value" value="{{ 1 if cust_disable_pagetype else 0 }}">
<button type="submit" class="btn btn-sm {% if cust_disable_pagetype %}btn-primary{% endif %}" data-customization-toggle="pagetype" data-confirm="{% if cust_disable_pagetype %}Re-enable your per-page custom CSS and JS?{% else %}Disable your per-page custom CSS and JS? Your saved code is kept and can be re-enabled.{% endif %}">{% if cust_disable_pagetype %}Enable{% else %}Disable{% endif %}</button>
</form>
</div>
<p class="customization-hint">Disabling hides your custom code without deleting it.</p>
</div>
{% endif %}
</aside>
<div class="profile-content">