2026-06-08 17:38:33 +02:00
from devplacepy . constants import TOPICS , REACTION_EMOJI
VOTE_TARGETS = [ " post " , " comment " , " gist " , " project " ]
REACTION_TARGETS = [ " post " , " comment " , " gist " , " project " ]
BOOKMARK_TARGETS = [ " post " , " gist " , " project " , " news " ]
COMMENT_TARGETS = [ " post " , " project " , " news " , " bug " , " gist " ]
PROJECT_TYPES = [ " game " , " game_asset " , " software " , " mobile_app " , " website " ]
GIST_LANGUAGES = [
" python " , " javascript " , " typescript " , " html " , " css " , " c " , " cpp " , " java " ,
" go " , " rust " , " sql " , " bash " , " json " , " markdown " , " plaintext " ,
]
SERVICE_ACTIONS = [
( " start " , " Start the service " ) ,
( " stop " , " Stop the service " ) ,
( " run " , " Trigger a single run now " ) ,
( " clear-logs " , " Clear the in-memory log buffer " ) ,
]
def field ( name , location , type = " string " , required = False , example = " " , description = " " , options = None ) :
spec = {
" name " : name ,
" location " : location ,
" type " : type ,
" required " : required ,
" example " : example ,
" description " : description ,
}
if options :
spec [ " options " ] = list ( options )
return spec
ROLE_LABELS = { " public " : " Public " , " user " : " Member " , " admin " : " Admin " }
def endpoint ( id , method , path , title , summary , auth = " user " , ajax = False ,
encoding = " none " , interactive = True , destructive = False ,
params = None , notes = None , sample_response = None , negotiation = None ) :
return {
" id " : id ,
" method " : method ,
" path " : path ,
" title " : title ,
" summary " : summary ,
" auth " : auth ,
" min_role " : ROLE_LABELS . get ( auth , auth . title ( ) ) ,
" ajax " : ajax ,
" encoding " : encoding ,
" interactive " : interactive ,
" destructive " : destructive ,
" params " : params or [ ] ,
" notes " : notes or [ ] ,
" sample_response " : sample_response ,
" negotiation " : negotiation ,
}
NON_BODY_ENDPOINTS = { " avatar " , " gateway-passthrough " , " notifications-open " }
def _classify ( ep ) :
if ep [ " id " ] in _PAGE_RESPONSES or ep [ " id " ] in _ACTION_RESPONSES :
return " negotiable "
if ep . get ( " ajax " ) :
return " ajax "
if ep [ " id " ] in NON_BODY_ENDPOINTS :
return " none "
return " json "
def _service_control_endpoints ( ) :
endpoints = [ ]
for action , summary in SERVICE_ACTIONS :
endpoints . append ( endpoint (
id = f " services- { action . replace ( ' - ' , ' ' ) } " ,
method = " POST " ,
path = f " /admin/services/ {{ name }} / { action } " ,
title = f " Service: { action } " ,
summary = summary + " . " ,
auth = " admin " ,
interactive = True ,
destructive = True ,
params = [ field ( " name " , " path " , required = True , example = " news " , description = " Registered service name. " ) ] ,
sample_response = { " ok " : True } ,
) )
return endpoints
API_GROUPS = [
{
" slug " : " conventions " ,
" title " : " Conventions & Errors " ,
" intro " : """
# Conventions & Errors
Shared rules that apply to every endpoint in this reference .
## Base URL
Every example uses your current host :
` ` `
{ { base } }
` ` `
## Authentication
Each endpoint is tagged * * public * * , * * user * * , or * * admin * * . Authenticate user and admin
endpoints with any of the four methods in [ Authentication ] ( / docs / authentication . html ) : the
` session ` cookie , an ` X - API - KEY ` header , a ` Bearer ` token , or HTTP Basic credentials . The
interactive panels on this site pre - fill your own API key , so you can run user - level calls
immediately .
## Request bodies
POST endpoints accept ` application / x - www - form - urlencoded ` form fields ( the same fields the
website submits ) . File uploads use ` multipart / form - data ` . A small number of endpoints accept
a JSON body ; those are noted explicitly .
## HTML or JSON (content negotiation)
* * Every * * endpoint that renders a page or returns a redirect can also answer in JSON - the
website keeps working exactly as before , and automation gets structured data from the same
URLs . A request is served JSON when it sends either of :
- ` Accept : application / json `
- ` Content - Type : application / json `
A normal browser navigation ( ` Accept : text / html ` ) always receives HTML , so nothing existing
changes . Responses are defined by Pydantic models , so each page returns the same data the
template renders .
* * Page reads * * ( GET ) return the page payload as a JSON object ( lists include a ` next_cursor `
for pagination ; detail pages embed author , comments , reactions , poll , and attachments ) .
* * Actions * * ( the form POSTs : create / edit / delete / follow / send / mark - read … ) return a
uniform envelope instead of a ` 302 ` redirect :
` ` ` json
{ " ok " : true , " redirect " : " /posts/abc-my-post " , " data " : { " uid " : " … " , " slug " : " … " , " url " : " … " } }
` ` `
` data ` carries the created / affected resource where applicable , or ` null ` . Cookies ( e . g . the
session set on login / signup ) are still set on JSON responses .
* * Errors * * are JSON too when JSON is requested :
` ` ` json
{ " error " : { " status " : 404 , " message " : " Not found " } }
` ` `
Validation failures return ` 422 ` with ` { " error " : " validation " , " fields " : { " field " : [ " msg " ] } } ` .
Unauthenticated JSON requests to a protected endpoint return ` 401 ` ( browsers are redirected to
the login page instead ) ; non - admins calling an admin endpoint get ` 403 ` .
## Trying it here
Every endpoint below has a live panel . Pick the response format ( * * JSON * * by default , or
* * HTML * * where the endpoint negotiates ) and the panel sets the matching ` Accept ` header on the
request and the generated cURL / JavaScript / Python snippets . The * * Expected * * tab always shows the
modeled response shape ; the * * Live response * * tab shows the real result after you press
* * Send request * * .
## AJAX responses (legacy shape)
The [ Votes , Reactions , Bookmarks & Polls ] ( / docs / social - actions . html ) endpoints predate the
envelope and keep their original flat JSON shapes ( e . g . ` { " saved " : true } ` ) . They return JSON
when the request carries an ` X - Requested - With : fetch ` header ( a subset of the rule above ) ;
without it they ` 302 ` redirect , mirroring the browser flow . The interactive panels send the
header for you .
## Pagination
Most list endpoints page with an opaque cursor . Pass the ` before ` query parameter set to the
` created_at ` ( or ` synced_at ` ) value of the last item you received to fetch the next page ; the
JSON payload returns a ` next_cursor ` to use as the next ` before ` . This covers the feed , the
post / project / gist / news lists , notifications , and saved bookmarks .
The follower and following lists are the exception : ` GET / profile / { username } / followers ` and
` GET / profile / { username } / following ` use classic page - based pagination via the ` page ` query
parameter ( 25 per page ) , not a cursor .
## Identifiers
Posts , projects , gists , and news articles accept either their slug or their bare UUID in the
path . Slugs embed the first eight characters of the UUID .
## Dates
All dates rendered to users are ` DD / MM / YYYY ` . Timestamps in stored records are ISO - 8601 UTC .
## Status codes
| Code | Meaning |
| - - - - - - | - - - - - - - - - |
| ` 200 ` | Success ( JSON or HTML ) |
| ` 201 ` | Resource created ( uploads ) |
| ` 302 ` | Redirect ( browser - style success for form posts ) |
| ` 400 ` | Invalid request body or parameters |
| ` 401 ` | Credentials supplied but invalid |
| ` 403 ` | Authenticated but not allowed |
| ` 404 ` | Resource not found |
| ` 413 ` | Upload exceeds the configured size limit ( see [ Uploads ] ( / docs / uploads . html ) ) |
| ` 415 ` | Upload file type not allowed ( see [ Uploads ] ( / docs / uploads . html ) ) |
| ` 422 ` | Form / body validation failed ( JSON clients ) |
| ` 429 ` | Rate limit exceeded ( see Rate limiting ) |
| ` 503 ` | Maintenance mode |
## Rate limiting
Mutating requests ( ` POST ` / ` PUT ` / ` DELETE ` / ` PATCH ` ) are rate limited per client IP over a
rolling window ; reads are not limited . The limit and window are configurable by an
administrator ( defaults : 60 requests per 60 seconds ) . When you exceed the limit you receive a
` 429 ` whose ` Retry - After ` header gives the number of seconds to wait before retrying . The
OpenAI gateway ( ` / openai / . . . ` ) is exempt .
## Troubleshooting
* * I get HTML back instead of JSON . * * Send ` Accept : application / json ` ( or
` Content - Type : application / json ` on a body ) . A request is only served JSON when it asks for it
and does not also accept ` text / html ` ; a normal browser navigation always gets HTML .
` X - Requested - With : fetch ` is * * not * * a general JSON switch - it only applies to the legacy
engagement actions ( votes , reactions , bookmarks , polls ) .
* * An action returns ` 302 ` instead of the JSON envelope . * * Same cause : the request did not ask
for JSON . Add the ` Accept : application / json ` header and the action returns
` { " ok " : true , " redirect " : " ... " , " data " : { . . . } } ` instead of redirecting .
* * A protected endpoint redirects me to the login page . * * Browser - style ( HTML ) requests to a
` user ` / ` admin ` endpoint without valid credentials are redirected to login ; the same request
with ` Accept : application / json ` returns ` 401 ` instead . Non - admins calling an ` admin ` endpoint
get ` 403 ` ( JSON ) or a redirect to the feed ( HTML ) .
* * An upload is rejected with ` 413 ` or ` 415 ` . * * ` 413 ` means the file exceeds the configured
size limit ; ` 415 ` means the file type is not in the allowed list . Both limits are set by an
administrator .
""" ,
" endpoints " : [ ] ,
} ,
{
" slug " : " lookups " ,
" title " : " Search & Lookups " ,
" intro " : """
# Search & Lookups
Type - ahead lookups that power mentions and the message composer . Both return JSON and accept
a single ` q ` query parameter . These feed [ Messaging ] ( / docs / messaging . html ) ( the recipient
composer ) and [ Profiles & Social Graph ] ( / docs / profiles . html ) ( mentions and user pages ) .
Every endpoint follows the shared [ Conventions & Errors ] ( / docs / conventions . html ) ( auth , content
negotiation , pagination , status codes ) ; see [ Authentication ] ( / docs / authentication . html ) for the
four ways to sign requests .
""" ,
" endpoints " : [
endpoint (
id = " search-users " ,
method = " GET " ,
path = " /profile/search " ,
title = " Search users " ,
summary = " Find up to ten users whose username matches a query. " ,
auth = " user " ,
params = [ field ( " q " , " query " , required = True , example = " al " , description = " Partial username to match. " ) ] ,
sample_response = { " results " : [ { " uid " : " 8f14e45f-... " , " username " : " alice_test " } ] } ,
) ,
endpoint (
id = " search-message-recipients " ,
method = " GET " ,
path = " /messages/search " ,
title = " Search message recipients " ,
summary = " Like user search, but excludes yourself; used by the message composer. " ,
auth = " user " ,
params = [ field ( " q " , " query " , required = True , example = " bo " , description = " Partial username to match. " ) ] ,
sample_response = { " results " : [ { " uid " : " 0cc175b9-... " , " username " : " bob_test " } ] } ,
) ,
] ,
} ,
{
" slug " : " social-actions " ,
" title " : " Votes, Reactions, Bookmarks & Polls " ,
" intro " : """
# Votes, Reactions, Bookmarks & Polls
Lightweight engagement actions . The POST endpoints here are * * toggles * * - sending the same
action again removes it . They return JSON when called with ` X - Requested - With : fetch ` ( sent
automatically by the panels below ) ; the [ Conventions & Errors ] ( / docs / conventions . html ) page
explains that header rule and the response envelope .
Every endpoint follows the shared [ Conventions & Errors ] ( / docs / conventions . html ) ( auth , content
negotiation , pagination , status codes ) ; see [ Authentication ] ( / docs / authentication . html ) for the
four ways to sign requests .
""" ,
" endpoints " : [
endpoint (
id = " votes-cast " ,
method = " POST " ,
path = " /votes/ {target_type} / {target_uid} " ,
title = " Cast or toggle a vote " ,
summary = " Upvote or downvote a target. Re-sending the same value removes the vote. " ,
auth = " user " ,
ajax = True ,
encoding = " form " ,
params = [
field ( " target_type " , " path " , " enum " , True , " post " , " Type of content being voted on. " , VOTE_TARGETS ) ,
field ( " target_uid " , " path " , " string " , True , " POST_UID " , " UID of the target. " ) ,
field ( " value " , " form " , " enum " , True , " 1 " , " 1 to upvote, -1 to downvote. " , [ " 1 " , " -1 " ] ) ,
] ,
sample_response = { " net " : 3 , " up " : 4 , " down " : 1 , " value " : 1 } ,
) ,
endpoint (
id = " reactions-toggle " ,
method = " POST " ,
path = " /reactions/ {target_type} / {target_uid} " ,
title = " Toggle an emoji reaction " ,
summary = " Add or remove an emoji reaction on a target. " ,
auth = " user " ,
ajax = True ,
encoding = " form " ,
params = [
field ( " target_type " , " path " , " enum " , True , " post " , " Type of content being reacted to. " , REACTION_TARGETS ) ,
field ( " target_uid " , " path " , " string " , True , " POST_UID " , " UID of the target. " ) ,
field ( " emoji " , " form " , " enum " , True , REACTION_EMOJI [ 0 ] , " One of the allowed reaction emoji. " , REACTION_EMOJI ) ,
] ,
sample_response = { " counts " : { REACTION_EMOJI [ 0 ] : 2 } , " mine " : [ REACTION_EMOJI [ 0 ] ] } ,
) ,
endpoint (
id = " bookmarks-toggle " ,
method = " POST " ,
path = " /bookmarks/ {target_type} / {target_uid} " ,
title = " Toggle a bookmark " ,
summary = " Save or unsave a target to your bookmarks. " ,
auth = " user " ,
ajax = True ,
encoding = " none " ,
params = [
field ( " target_type " , " path " , " enum " , True , " post " , " Type of content to bookmark. " , BOOKMARK_TARGETS ) ,
field ( " target_uid " , " path " , " string " , True , " POST_UID " , " UID of the target. " ) ,
] ,
sample_response = { " saved " : True } ,
) ,
endpoint (
id = " bookmarks-saved " ,
method = " GET " ,
path = " /bookmarks/saved " ,
title = " View saved bookmarks " ,
summary = " Render your saved content. Returns an HTML page. " ,
auth = " user " ,
interactive = True ,
params = [ field ( " before " , " query " , " string " , False , " " , " Pagination cursor (created_at of the last item). " ) ] ,
notes = [ " Bookmarks target posts, projects, gists, and news; see [Posts, Comments, Projects, Gists & News](/docs/content.html). " ] ,
) ,
endpoint (
id = " polls-vote " ,
method = " POST " ,
path = " /polls/ {poll_uid} /vote " ,
title = " Vote in a poll " ,
summary = " Cast, change, or clear your vote on a poll option. " ,
auth = " user " ,
ajax = True ,
encoding = " form " ,
params = [
field ( " poll_uid " , " path " , " string " , True , " POLL_UID " , " UID of the poll. " ) ,
field ( " option_uid " , " form " , " string " , True , " OPTION_UID " , " UID of the chosen option. " ) ,
] ,
notes = [
" You hold at most one vote per poll, and only your latest vote counts. "
" Voting a different option replaces your previous choice; voting your current "
" option again removes the vote. " ,
] ,
sample_response = { " question " : " Best editor? " , " options " : [ { " uid " : " OPTION_UID " , " label " : " Vim " , " votes " : 5 } ] , " total " : 5 , " voted " : " OPTION_UID " } ,
) ,
] ,
} ,
{
" slug " : " content " ,
" title " : " Posts, Comments, Projects, Gists & News " ,
" intro " : """
# Posts, Comments, Projects, Gists & News
The core content types . Read endpoints render HTML pages ; write endpoints accept form fields
and redirect to the new or updated resource . List fields such as ` attachment_uids ` are
repeated form keys - upload files first via [ Uploads ] ( / docs / uploads . html ) and pass the returned
uids here . Engage with this content through [ Votes , Reactions , Bookmarks & Polls ] ( / docs / social - actions . html ) .
Every endpoint follows the shared [ Conventions & Errors ] ( / docs / conventions . html ) ( auth , content
negotiation , pagination , status codes ) ; see [ Authentication ] ( / docs / authentication . html ) for the
four ways to sign requests .
""" ,
" endpoints " : [
endpoint (
id = " feed-list " ,
method = " GET " ,
path = " /feed " ,
title = " Browse the feed " ,
summary = " The main post feed. Returns an HTML page. " ,
auth = " public " ,
interactive = True ,
params = [
field ( " tab " , " query " , " enum " , False , " all " , " Feed selector. " , [ " all " , " trending " , " following " ] ) ,
field ( " topic " , " query " , " enum " , False , " " , " Filter by topic. " , TOPICS ) ,
field ( " before " , " query " , " string " , False , " " , " Pagination cursor. " ) ,
] ,
) ,
endpoint (
id = " posts-create " ,
method = " POST " ,
path = " /posts/create " ,
title = " Create a post " ,
summary = " Publish a post, optionally with a poll. Redirects to the new post. " ,
auth = " user " ,
encoding = " form " ,
destructive = True ,
params = [
field ( " content " , " form " , " textarea " , True , " Posted from a script. " , " Body, 10-2000 characters. " ) ,
field ( " title " , " form " , " string " , False , " Hello " , " Optional title, up to 500 characters. " ) ,
field ( " topic " , " form " , " enum " , False , " random " , " Post topic. " , TOPICS ) ,
field ( " project_uid " , " form " , " string " , False , " " , " Attach to a project. " ) ,
field ( " poll_question " , " form " , " string " , False , " " , " Optional poll question. " ) ,
field ( " poll_options " , " form " , " string " , False , " " , " Repeat the field for each poll option, or send a single newline- or comma-separated string (2-6 options). " ) ,
] ,
notes = [ " Returns a `302` redirect to `/posts/ {slug} ` on success. " ] ,
) ,
endpoint (
id = " posts-detail " ,
method = " GET " ,
path = " /posts/ {post_slug} " ,
title = " View a post " ,
summary = " Render a post with comments. Returns an HTML page. " ,
auth = " public " ,
interactive = True ,
params = [ field ( " post_slug " , " path " , " string " , True , " POST_SLUG " , " Slug or UID of the post. " ) ] ,
) ,
endpoint (
id = " posts-edit " ,
method = " POST " ,
path = " /posts/edit/ {post_slug} " ,
title = " Edit a post " ,
summary = " Update a post you own, optionally adding a poll if it has none. " ,
auth = " user " ,
encoding = " form " ,
destructive = True ,
params = [
field ( " post_slug " , " path " , " string " , True , " POST_SLUG " , " Slug or UID of the post. " ) ,
field ( " content " , " form " , " textarea " , True , " Updated body. " , " Body, 10-2000 characters. " ) ,
field ( " title " , " form " , " string " , False , " Updated title " , " Optional title. " ) ,
field ( " topic " , " form " , " enum " , False , " random " , " Post topic. " , TOPICS ) ,
field ( " poll_question " , " form " , " string " , False , " " , " Optional poll question. Adds a poll only when the post has none. " ) ,
field ( " poll_options " , " form " , " string " , False , " " , " Repeat the field for each poll option, or send a single newline- or comma-separated string (2-6 options). " ) ,
] ,
) ,
endpoint (
id = " posts-delete " ,
method = " POST " ,
path = " /posts/delete/ {post_slug} " ,
title = " Delete a post " ,
summary = " Delete a post you own, cascading its comments and votes. " ,
auth = " user " ,
destructive = True ,
params = [ field ( " post_slug " , " path " , " string " , True , " POST_SLUG " , " Slug or UID of the post. " ) ] ,
) ,
endpoint (
id = " comments-create " ,
method = " POST " ,
path = " /comments/create " ,
title = " Create a comment " ,
summary = " Comment on any commentable target. Supports nested replies. " ,
auth = " user " ,
encoding = " form " ,
destructive = True ,
params = [
field ( " content " , " form " , " textarea " , True , " Nice work. " , " Body, 3-1000 characters. " ) ,
field ( " target_uid " , " form " , " string " , False , " POST_UID " , " UID of the target (or use post_uid). " ) ,
field ( " post_uid " , " form " , " string " , False , " " , " Convenience alias for a post target. " ) ,
field ( " target_type " , " form " , " enum " , False , " post " , " Type of the target. " , COMMENT_TARGETS ) ,
field ( " parent_uid " , " form " , " string " , False , " " , " Parent comment UID for a reply. " ) ,
] ,
notes = [ " Either `target_uid` or `post_uid` is required. " ] ,
) ,
endpoint (
id = " comments-delete " ,
method = " POST " ,
path = " /comments/delete/ {comment_uid} " ,
title = " Delete a comment " ,
summary = " Delete a comment you own. " ,
auth = " user " ,
destructive = True ,
params = [ field ( " comment_uid " , " path " , " string " , True , " COMMENT_UID " , " UID of the comment. " ) ] ,
) ,
endpoint (
id = " projects-list " ,
method = " GET " ,
path = " /projects " ,
title = " Browse projects " ,
summary = " List projects. Returns an HTML page. " ,
auth = " public " ,
interactive = True ,
params = [
field ( " tab " , " query " , " enum " , False , " recent " , " Sort selector. " , [ " recent " , " popular " , " released " ] ) ,
field ( " search " , " query " , " string " , False , " " , " Search title and description. " ) ,
field ( " project_type " , " query " , " enum " , False , " " , " Filter by type. " , PROJECT_TYPES ) ,
field ( " before " , " query " , " string " , False , " " , " Pagination cursor. " ) ,
] ,
) ,
endpoint (
id = " projects-detail " ,
method = " GET " ,
path = " /projects/ {project_slug} " ,
title = " View a project " ,
summary = " Render a project with comments. Returns an HTML page. " ,
auth = " public " ,
interactive = True ,
params = [ field ( " project_slug " , " path " , " string " , True , " PROJECT_SLUG " , " Slug or UID of the project. " ) ] ,
) ,
endpoint (
id = " projects-create " ,
method = " POST " ,
path = " /projects/create " ,
title = " Create a project " ,
summary = " Publish a project. Redirects to the new project. " ,
auth = " user " ,
encoding = " form " ,
destructive = True ,
params = [
field ( " title " , " form " , " string " , True , " My Project " , " Title, 1-200 characters. " ) ,
field ( " description " , " form " , " textarea " , True , " What it does. " , " Description, 1-5000 characters. " ) ,
field ( " project_type " , " form " , " enum " , False , " software " , " Project type. " , PROJECT_TYPES ) ,
field ( " status " , " form " , " string " , False , " In Development " , " Free-form status label. " ) ,
field ( " platforms " , " form " , " string " , False , " Linux, Web " , " Comma-separated platforms. " ) ,
2026-06-09 00:30:25 +02:00
field ( " release_date " , " form " , " string " , False , " 31/12/2026 " , " Optional release date in DD/MM/YYYY format. " ) ,
field ( " demo_date " , " form " , " string " , False , " 31/12/2026 " , " Optional demo date in DD/MM/YYYY format. " ) ,
2026-06-08 17:38:33 +02:00
] ,
) ,
endpoint (
id = " projects-delete " ,
method = " POST " ,
path = " /projects/delete/ {project_slug} " ,
title = " Delete a project " ,
summary = " Delete a project you own. " ,
auth = " user " ,
destructive = True ,
params = [ field ( " project_slug " , " path " , " string " , True , " PROJECT_SLUG " , " Slug or UID of the project. " ) ] ,
) ,
2026-06-09 06:41:27 +02:00
endpoint (
id = " projects-private " ,
method = " POST " ,
path = " /projects/ {project_slug} /private " ,
title = " Set project visibility " ,
summary = " Mark a project you own private (only you and administrators can see it) or public. Send value=1 for private, value=0 for public. " ,
auth = " user " ,
encoding = " form " ,
params = [
field ( " project_slug " , " path " , " string " , True , " PROJECT_SLUG " , " Slug or UID of the project. " ) ,
field ( " value " , " form " , " boolean " , True , " 1 " , " 1 to make the project private, 0 to make it public. " ) ,
] ,
) ,
endpoint (
id = " projects-readonly " ,
method = " POST " ,
path = " /projects/ {project_slug} /readonly " ,
title = " Set project read-only " ,
summary = " Mark a project you own read-only so all of its files become immutable (no writes, edits, moves, deletes, or uploads succeed), or writable again. Send value=1 for read-only, value=0 for writable. " ,
auth = " user " ,
encoding = " form " ,
destructive = True ,
params = [
field ( " project_slug " , " path " , " string " , True , " PROJECT_SLUG " , " Slug or UID of the project. " ) ,
field ( " value " , " form " , " boolean " , True , " 1 " , " 1 to make the project read-only, 0 to make it writable. " ) ,
] ,
) ,
2026-06-09 01:32:57 +02:00
endpoint (
id = " projects-zip " ,
method = " POST " ,
path = " /projects/ {project_slug} /zip " ,
title = " Queue a project zip " ,
summary = " Start a background job that archives the whole project. Returns the job uid and status URL to poll. " ,
auth = " public " ,
params = [ field ( " project_slug " , " path " , " string " , True , " PROJECT_SLUG " , " Slug or UID of the project. " ) ] ,
sample_response = { " uid " : " ZIP_JOB_UID " , " status_url " : " /zips/ZIP_JOB_UID " } ,
) ,
endpoint (
id = " zips-status " ,
method = " GET " ,
path = " /zips/ {uid} " ,
title = " Zip job status " ,
summary = " Poll a zip job. While pending or running download_url is null; once done it points at the archive. " ,
auth = " public " ,
params = [ field ( " uid " , " path " , " string " , True , " ZIP_JOB_UID " , " Zip job uid returned when the job was queued. " ) ] ,
sample_response = {
" uid " : " ZIP_JOB_UID " ,
" kind " : " zip " ,
" status " : " done " ,
" preferred_name " : " my-project " ,
" download_url " : " /zips/ZIP_JOB_UID/download " ,
" error " : None ,
" bytes_in " : 20480 ,
" bytes_out " : 8192 ,
" item_count " : 12 ,
" file_count " : 10 ,
" dir_count " : 2 ,
" created_at " : " 2026-06-09T10:00:00+00:00 " ,
" completed_at " : " 2026-06-09T10:00:03+00:00 " ,
} ,
) ,
endpoint (
id = " zips-download " ,
method = " GET " ,
path = " /zips/ {uid} /download " ,
title = " Download a zip archive " ,
summary = " Stream the finished archive as application/zip. Each access extends the retention window. " ,
auth = " public " ,
interactive = True ,
params = [ field ( " uid " , " path " , " string " , True , " ZIP_JOB_UID " , " Zip job uid of a finished job. " ) ] ,
) ,
2026-06-08 17:38:33 +02:00
endpoint (
id = " gists-list " ,
method = " GET " ,
path = " /gists " ,
title = " Browse gists " ,
summary = " List code gists. Returns an HTML page. " ,
auth = " public " ,
interactive = True ,
params = [
field ( " language " , " query " , " enum " , False , " " , " Filter by language. " , GIST_LANGUAGES ) ,
field ( " user_uid " , " query " , " string " , False , " " , " Filter by author UID. " ) ,
field ( " before " , " query " , " string " , False , " " , " Pagination cursor. " ) ,
] ,
) ,
endpoint (
id = " gists-detail " ,
method = " GET " ,
path = " /gists/ {gist_slug} " ,
title = " View a gist " ,
summary = " Render a gist with comments. Returns an HTML page. " ,
auth = " public " ,
interactive = True ,
params = [ field ( " gist_slug " , " path " , " string " , True , " GIST_SLUG " , " Slug or UID of the gist. " ) ] ,
) ,
endpoint (
id = " gists-create " ,
method = " POST " ,
path = " /gists/create " ,
title = " Create a gist " ,
summary = " Publish a code snippet. Redirects to the new gist. " ,
auth = " user " ,
encoding = " form " ,
destructive = True ,
params = [
field ( " title " , " form " , " string " , True , " Quick sort " , " Title, 1-200 characters. " ) ,
field ( " source_code " , " form " , " textarea " , True , " print( ' hello ' ) " , " Source, 1-400000 characters. " ) ,
field ( " language " , " form " , " enum " , False , " python " , " Syntax language. " , GIST_LANGUAGES ) ,
field ( " description " , " form " , " string " , False , " " , " Optional description. " ) ,
] ,
) ,
endpoint (
id = " gists-edit " ,
method = " POST " ,
path = " /gists/edit/ {gist_slug} " ,
title = " Edit a gist " ,
summary = " Update a gist you own. " ,
auth = " user " ,
encoding = " form " ,
destructive = True ,
params = [
field ( " gist_slug " , " path " , " string " , True , " GIST_SLUG " , " Slug or UID of the gist. " ) ,
field ( " title " , " form " , " string " , True , " Quick sort " , " Title, 1-200 characters. " ) ,
field ( " source_code " , " form " , " textarea " , True , " print( ' hi ' ) " , " Source, 1-400000 characters. " ) ,
field ( " language " , " form " , " enum " , False , " python " , " Syntax language. " , GIST_LANGUAGES ) ,
field ( " description " , " form " , " string " , False , " " , " Optional description. " ) ,
] ,
) ,
endpoint (
id = " gists-delete " ,
method = " POST " ,
path = " /gists/delete/ {gist_slug} " ,
title = " Delete a gist " ,
summary = " Delete a gist you own. " ,
auth = " user " ,
destructive = True ,
params = [ field ( " gist_slug " , " path " , " string " , True , " GIST_SLUG " , " Slug or UID of the gist. " ) ] ,
) ,
endpoint (
id = " news-list " ,
method = " GET " ,
path = " /news " ,
title = " Browse news " ,
summary = " Curated developer news. Returns an HTML page. " ,
auth = " public " ,
interactive = True ,
params = [ field ( " before " , " query " , " string " , False , " " , " Pagination cursor (synced_at). " ) ] ,
) ,
endpoint (
id = " news-detail " ,
method = " GET " ,
path = " /news/ {news_slug} " ,
title = " View a news article " ,
summary = " Render a news article with comments. Returns an HTML page. " ,
auth = " public " ,
interactive = True ,
params = [ field ( " news_slug " , " path " , " string " , True , " NEWS_SLUG " , " Slug or UID of the article. " ) ] ,
) ,
] ,
} ,
{
" slug " : " profiles " ,
" title " : " Profiles & Social Graph " ,
" intro " : """
# Profiles & Social Graph
Profile data , the follow graph , the leaderboard , and avatar generation . Find users with
[ Search & Lookups ] ( / docs / lookups . html ) ; follows generate entries in [ Notifications ] ( / docs / notifications . html ) .
Every endpoint follows the shared [ Conventions & Errors ] ( / docs / conventions . html ) ( auth , content
negotiation , pagination , status codes ) ; see [ Authentication ] ( / docs / authentication . html ) for the
four ways to sign requests .
""" ,
" endpoints " : [
endpoint (
id = " profile-detail " ,
method = " GET " ,
path = " /profile/ {username} " ,
title = " View a profile " ,
summary = " Render a user profile. Returns an HTML page. " ,
auth = " public " ,
interactive = True ,
params = [
field ( " username " , " path " , " string " , True , " {{ username }} " , " Target username. " ) ,
field ( " tab " , " query " , " enum " , False , " posts " , " Profile tab. " , [ " posts " , " activity " , " followers " , " following " ] ) ,
] ,
) ,
endpoint (
id = " profile-update " ,
method = " POST " ,
path = " /profile/update " ,
title = " Update your profile " ,
summary = " Update your own bio and links. " ,
auth = " user " ,
encoding = " form " ,
destructive = True ,
params = [
field ( " bio " , " form " , " textarea " , False , " Building things. " , " Bio, up to 500 characters. " ) ,
field ( " location " , " form " , " string " , False , " Earth " , " Location, up to 200 characters. " ) ,
field ( " git_link " , " form " , " string " , False , " " , " Git profile URL. " ) ,
field ( " website " , " form " , " string " , False , " " , " Personal website URL. " ) ,
] ,
) ,
endpoint (
id = " profile-regenerate-key " ,
method = " POST " ,
path = " /profile/regenerate-api-key " ,
title = " Regenerate your API key " ,
summary = " Issue a new API key and invalidate the current one. " ,
auth = " user " ,
interactive = False ,
destructive = True ,
notes = [
" > Running this invalidates the key these documentation panels use. Do it from your "
" [profile page](/profile/ {{ username }}) instead, then reload these docs. " ,
] ,
sample_response = { " api_key " : " NEW_UUID " } ,
) ,
endpoint (
id = " follow-user " ,
method = " POST " ,
path = " /follow/ {username} " ,
title = " Follow a user " ,
summary = " Follow another user. Idempotent. " ,
auth = " user " ,
destructive = True ,
params = [ field ( " username " , " path " , " string " , True , " bob_test " , " Username to follow. " ) ] ,
) ,
endpoint (
id = " unfollow-user " ,
method = " POST " ,
path = " /follow/unfollow/ {username} " ,
title = " Unfollow a user " ,
summary = " Stop following a user. " ,
auth = " user " ,
destructive = True ,
params = [ field ( " username " , " path " , " string " , True , " bob_test " , " Username to unfollow. " ) ] ,
) ,
endpoint (
id = " list-followers " ,
method = " GET " ,
path = " /profile/ {username} /followers " ,
title = " List followers " ,
summary = " List the users who follow a profile, 25 per page. Returns JSON. " ,
auth = " public " ,
interactive = True ,
params = [
field ( " username " , " path " , " string " , True , " {{ username }} " , " Target username. " ) ,
field ( " page " , " query " , " integer " , False , " 1 " , " Page number, 25 per page. " ) ,
] ,
sample_response = {
" username " : " {{ username }} " ,
" mode " : " followers " ,
" count " : 2 ,
" page " : 1 ,
" total_pages " : 1 ,
" followers " : [
{ " uid " : " UUID " , " username " : " bob_test " , " bio " : " Building things. " , " is_following " : False } ,
] ,
} ,
) ,
endpoint (
id = " list-following " ,
method = " GET " ,
path = " /profile/ {username} /following " ,
title = " List following " ,
summary = " List the users a profile follows, 25 per page. Returns JSON. " ,
auth = " public " ,
interactive = True ,
params = [
field ( " username " , " path " , " string " , True , " {{ username }} " , " Target username. " ) ,
field ( " page " , " query " , " integer " , False , " 1 " , " Page number, 25 per page. " ) ,
] ,
sample_response = {
" username " : " {{ username }} " ,
" mode " : " following " ,
" count " : 1 ,
" page " : 1 ,
" total_pages " : 1 ,
" following " : [
{ " uid " : " UUID " , " username " : " alice_test " , " bio " : " " , " is_following " : True } ,
] ,
} ,
) ,
endpoint (
id = " leaderboard " ,
method = " GET " ,
path = " /leaderboard " ,
title = " View the leaderboard " ,
summary = " Top contributors by stars. Returns an HTML page. " ,
auth = " public " ,
interactive = True ,
) ,
endpoint (
id = " avatar " ,
method = " GET " ,
path = " /avatar/ {style} / {seed} " ,
title = " Generate an avatar " ,
summary = " Deterministic SVG avatar for a seed. Returns an image. " ,
auth = " public " ,
interactive = False ,
params = [
field ( " style " , " path " , " enum " , True , " multiavatar " , " Avatar style. " , [ " multiavatar " ] ) ,
field ( " seed " , " path " , " string " , True , " {{ username }} " , " Seed string, usually a username. " ) ,
field ( " size " , " query " , " int " , False , " 128 " , " Pixel size. " ) ,
] ,
) ,
] ,
} ,
{
" slug " : " messaging " ,
" title " : " Messaging " ,
" intro " : """
# Messaging
Direct messages between users . The inbox renders HTML ; sending uses form fields . Look up
recipients with [ Search & Lookups ] ( / docs / lookups . html ) and attach files via [ Uploads ] ( / docs / uploads . html ) .
Every endpoint follows the shared [ Conventions & Errors ] ( / docs / conventions . html ) ( auth , content
negotiation , pagination , status codes ) ; see [ Authentication ] ( / docs / authentication . html ) for the
four ways to sign requests .
""" ,
" endpoints " : [
endpoint (
id = " messages-inbox " ,
method = " GET " ,
path = " /messages " ,
title = " Open the inbox " ,
summary = " Render conversations. Returns an HTML page. " ,
auth = " user " ,
interactive = True ,
params = [
field ( " with_uid " , " query " , " string " , False , " " , " Open a specific conversation by user UID. " ) ,
field ( " search " , " query " , " string " , False , " " , " Jump to a conversation by username. " ) ,
] ,
) ,
endpoint (
id = " messages-send " ,
method = " POST " ,
path = " /messages/send " ,
title = " Send a message " ,
summary = " Send a direct message to a user. " ,
auth = " user " ,
encoding = " form " ,
destructive = True ,
params = [
field ( " content " , " form " , " textarea " , True , " Hello there. " , " Body, 1-2000 characters. " ) ,
field ( " receiver_uid " , " form " , " string " , True , " RECEIVER_UID " , " Recipient user UID. " ) ,
] ,
) ,
] ,
} ,
{
" slug " : " notifications " ,
" title " : " Notifications " ,
" intro " : """
# Notifications
Read your notification feed and mark items read . The unread counts endpoint backs the badges
in the navigation bar . Deliver these to the browser with [ Web Push ] ( / docs / push . html ) .
Every endpoint follows the shared [ Conventions & Errors ] ( / docs / conventions . html ) ( auth , content
negotiation , pagination , status codes ) ; see [ Authentication ] ( / docs / authentication . html ) for the
four ways to sign requests .
""" ,
" endpoints " : [
endpoint (
id = " notifications-list " ,
method = " GET " ,
path = " /notifications " ,
title = " View notifications " ,
summary = " Render your notifications. Returns an HTML page. " ,
auth = " user " ,
interactive = True ,
params = [ field ( " before " , " query " , " string " , False , " " , " Pagination cursor. " ) ] ,
) ,
endpoint (
id = " notifications-counts " ,
method = " GET " ,
path = " /notifications/counts " ,
title = " Unread counts " ,
summary = " Unread notification and message counts. " ,
auth = " public " ,
notes = [ " Guests receive ` { \" notifications \" : 0, \" messages \" : 0 }` instead of an error, so the navigation badge works before login. " ] ,
sample_response = { " notifications " : 2 , " messages " : 1 } ,
) ,
endpoint (
id = " notifications-open " ,
method = " GET " ,
path = " /notifications/open/ {notification_uid} " ,
title = " Open a notification " ,
summary = " Mark a notification read and redirect to its target. " ,
auth = " user " ,
interactive = False ,
params = [ field ( " notification_uid " , " path " , " string " , True , " NOTIFICATION_UID " , " UID of the notification. " ) ] ,
) ,
endpoint (
id = " notifications-mark-read " ,
method = " POST " ,
path = " /notifications/mark-read/ {notification_uid} " ,
title = " Mark one read " ,
summary = " Mark a single notification as read. " ,
auth = " user " ,
params = [ field ( " notification_uid " , " path " , " string " , True , " NOTIFICATION_UID " , " UID of the notification. " ) ] ,
) ,
endpoint (
id = " notifications-mark-all-read " ,
method = " POST " ,
path = " /notifications/mark-all-read " ,
title = " Mark all read " ,
summary = " Mark every notification as read. " ,
auth = " user " ,
) ,
] ,
} ,
{
" slug " : " uploads " ,
" title " : " Uploads " ,
" intro " : """
# Uploads
Attachment storage . Upload a file to receive an attachment record , then reference its ` uid `
in an ` attachment_uids ` field when creating a post , comment , project , gist , message , or bug -
2026-06-08 22:51:09 +02:00
see [ Posts , Comments , Projects , Gists & News ] ( / docs / content . html ) . Images and videos embed and
play inline once posted ; other types render as download links . The record ' s `is_image` and
` is_video ` flags indicate how the file is displayed .
2026-06-08 17:38:33 +02:00
Every endpoint follows the shared [ Conventions & Errors ] ( / docs / conventions . html ) ( auth , content
negotiation , pagination , status codes ) ; see [ Authentication ] ( / docs / authentication . html ) for the
four ways to sign requests .
""" ,
" endpoints " : [
endpoint (
id = " uploads-upload " ,
method = " POST " ,
path = " /uploads/upload " ,
title = " Upload a file " ,
summary = " Store a file and return its attachment record. " ,
auth = " user " ,
encoding = " multipart " ,
params = [ field ( " file " , " form " , " file " , True , " " , " The file to upload. " ) ] ,
notes = [
2026-06-08 22:51:09 +02:00
" Allowed file types and the size limit are configured by administrators. Images and common video formats (mp4, webm, ogv, mov, m4v) are accepted by default. " ,
2026-06-08 17:38:33 +02:00
" Returns `201` on success, `413` if too large, `415` if the type is not allowed. " ,
] ,
2026-06-08 22:51:09 +02:00
sample_response = { " uid " : " ATTACHMENT_UID " , " filename " : " clip.mp4 " , " url " : " /static/uploads/attachments/ab/cd/ATTACHMENT_UID.mp4 " , " size " : 20480 , " is_image " : False , " is_video " : True , " mime_type " : " video/mp4 " } ,
2026-06-08 17:38:33 +02:00
) ,
endpoint (
id = " uploads-delete " ,
method = " DELETE " ,
path = " /uploads/delete/ {attachment_uid} " ,
title = " Delete an attachment " ,
summary = " Delete an attachment you own. " ,
auth = " user " ,
destructive = True ,
params = [ field ( " attachment_uid " , " path " , " string " , True , " ATTACHMENT_UID " , " UID of the attachment. " ) ] ,
sample_response = { " status " : " deleted " } ,
) ,
] ,
} ,
2026-06-08 22:51:09 +02:00
{
" slug " : " project-files " ,
" title " : " Project Filesystem " ,
" intro " : """
# Project Filesystem
Each project carries a full virtual filesystem - directories and files - so a project can hold a
complete software project . Reading is public ( anyone can browse a project ' s tree); creating,
editing , uploading , moving and deleting require the project owner . Text files are editable inline ;
binary files are uploaded and served from ` / static / uploads / project_files / . . . ` .
Paths are relative POSIX paths inside the project ( for example ` src / main . py ` ) . Parent directories
are created automatically on write , upload and mkdir . Paths containing ` . . ` , null bytes or empty
segments are rejected .
Every endpoint follows the shared [ Conventions & Errors ] ( / docs / conventions . html ) ( auth , content
negotiation , status codes ) ; see [ Authentication ] ( / docs / authentication . html ) for the four ways to
sign requests .
""" ,
" endpoints " : [
endpoint (
id = " project-files-list " ,
method = " GET " ,
path = " /projects/ {project_slug} /files " ,
title = " List a project ' s files " ,
summary = " Return the flat list of files and directories in a project. " ,
auth = " public " ,
params = [ field ( " project_slug " , " path " , " string " , True , " PROJECT_SLUG " , " Project slug or uid. " ) ] ,
sample_response = { " project " : { " uid " : " PROJECT_UID " , " slug " : " PROJECT_SLUG " } , " files " : [ { " path " : " src/main.py " , " name " : " main.py " , " type " : " file " , " is_binary " : False , " size " : 42 } ] , " is_owner " : False } ,
) ,
endpoint (
id = " project-files-raw " ,
method = " GET " ,
path = " /projects/ {project_slug} /files/raw " ,
title = " Read a project file " ,
summary = " Return one file ' s metadata and (for text files) its content. " ,
auth = " public " ,
params = [
field ( " project_slug " , " path " , " string " , True , " PROJECT_SLUG " , " Project slug or uid. " ) ,
field ( " path " , " query " , " string " , True , " src/main.py " , " Relative file path inside the project. " ) ,
] ,
sample_response = { " path " : " src/main.py " , " name " : " main.py " , " type " : " file " , " is_binary " : False , " mime_type " : " text/plain " , " size " : 42 , " url " : None , " content " : " print( ' hello ' ) \n " } ,
) ,
endpoint (
id = " project-files-write " ,
method = " POST " ,
path = " /projects/ {project_slug} /files/write " ,
title = " Write a text file " ,
summary = " Create or overwrite a text file; parent directories are created automatically. " ,
auth = " user " ,
encoding = " form " ,
destructive = True ,
params = [
field ( " project_slug " , " path " , " string " , True , " PROJECT_SLUG " , " Project slug or uid. " ) ,
field ( " path " , " form " , " string " , True , " src/main.py " , " Relative file path. " ) ,
field ( " content " , " form " , " textarea " , True , " print( ' hello ' ) " , " Full file content (max 400000 chars). " ) ,
] ,
notes = [ " Owner only; non-owners get `403`. Invalid paths return `400`. " ] ,
sample_response = { " ok " : True , " redirect " : " /projects/PROJECT_SLUG/files " , " data " : { " path " : " src/main.py " , " type " : " file " } } ,
) ,
2026-06-09 06:41:27 +02:00
endpoint (
id = " project-files-lines " ,
method = " GET " ,
path = " /projects/ {project_slug} /files/lines " ,
title = " Read a line range " ,
summary = " Read a 1-indexed inclusive line range of a text file. Returns lines plus total_lines for targeting edits. " ,
auth = " public " ,
params = [
field ( " project_slug " , " path " , " string " , True , " PROJECT_SLUG " , " Project slug or uid. " ) ,
field ( " path " , " query " , " string " , True , " src/main.py " , " Relative file path. " ) ,
field ( " start " , " query " , " integer " , False , " 1 " , " First line, 1-indexed (default 1). " ) ,
field ( " end " , " query " , " integer " , False , " 50 " , " Last line inclusive; omit or -1 for end of file. " ) ,
] ,
notes = [ " Text files only; binary, directory, or missing paths return `404`. " ] ,
sample_response = { " path " : " src/main.py " , " start " : 1 , " end " : 2 , " total_lines " : 2 , " lines " : [ " import os " , " print(os.getcwd()) " ] , " content " : " import os \n print(os.getcwd()) " } ,
) ,
endpoint (
id = " project-files-replace-lines " ,
method = " POST " ,
path = " /projects/ {project_slug} /files/replace-lines " ,
title = " Replace a line range " ,
summary = " Replace lines start..end (inclusive) with new content; empty content deletes the range. Leaves the rest of the file untouched. " ,
auth = " user " ,
encoding = " form " ,
destructive = True ,
params = [
field ( " project_slug " , " path " , " string " , True , " PROJECT_SLUG " , " Project slug or uid. " ) ,
field ( " path " , " form " , " string " , True , " src/main.py " , " Relative file path. " ) ,
field ( " start " , " form " , " integer " , True , " 10 " , " First line to replace (1-indexed). " ) ,
field ( " end " , " form " , " integer " , True , " 12 " , " Last line to replace (inclusive). " ) ,
field ( " content " , " form " , " textarea " , False , " new code " , " Replacement text (empty deletes the range). " ) ,
] ,
notes = [ " Owner only. The preferred way to edit a large file; avoids rewriting the whole file. " ] ,
sample_response = { " ok " : True , " redirect " : " /projects/PROJECT_SLUG/files " , " data " : { " path " : " src/main.py " , " type " : " file " } } ,
) ,
endpoint (
id = " project-files-insert-lines " ,
method = " POST " ,
path = " /projects/ {project_slug} /files/insert-lines " ,
title = " Insert lines " ,
summary = " Insert content before a 1-indexed line. Use at=1 to prepend and at=total_lines+1 to append. " ,
auth = " user " ,
encoding = " form " ,
destructive = True ,
params = [
field ( " project_slug " , " path " , " string " , True , " PROJECT_SLUG " , " Project slug or uid. " ) ,
field ( " path " , " form " , " string " , True , " src/main.py " , " Relative file path. " ) ,
field ( " at " , " form " , " integer " , True , " 1 " , " Insert before this 1-indexed line. " ) ,
field ( " content " , " form " , " textarea " , True , " # header " , " Text to insert. " ) ,
] ,
sample_response = { " ok " : True , " redirect " : " /projects/PROJECT_SLUG/files " , " data " : { " path " : " src/main.py " , " type " : " file " } } ,
) ,
endpoint (
id = " project-files-delete-lines " ,
method = " POST " ,
path = " /projects/ {project_slug} /files/delete-lines " ,
title = " Delete a line range " ,
summary = " Delete lines start..end (inclusive) from a text file. " ,
auth = " user " ,
encoding = " form " ,
destructive = True ,
params = [
field ( " project_slug " , " path " , " string " , True , " PROJECT_SLUG " , " Project slug or uid. " ) ,
field ( " path " , " form " , " string " , True , " src/main.py " , " Relative file path. " ) ,
field ( " start " , " form " , " integer " , True , " 5 " , " First line to delete (1-indexed). " ) ,
field ( " end " , " form " , " integer " , True , " 7 " , " Last line to delete (inclusive). " ) ,
] ,
sample_response = { " ok " : True , " redirect " : " /projects/PROJECT_SLUG/files " , " data " : { " path " : " src/main.py " , " type " : " file " } } ,
) ,
endpoint (
id = " project-files-append " ,
method = " POST " ,
path = " /projects/ {project_slug} /files/append " ,
title = " Append to a file " ,
summary = " Append content as new lines at the end of a text file; grow a large file across calls without resending it. " ,
auth = " user " ,
encoding = " form " ,
destructive = True ,
params = [
field ( " project_slug " , " path " , " string " , True , " PROJECT_SLUG " , " Project slug or uid. " ) ,
field ( " path " , " form " , " string " , True , " log.txt " , " Relative file path. " ) ,
field ( " content " , " form " , " textarea " , True , " next chunk " , " Text to append. " ) ,
] ,
sample_response = { " ok " : True , " redirect " : " /projects/PROJECT_SLUG/files " , " data " : { " path " : " log.txt " , " type " : " file " } } ,
) ,
2026-06-08 22:51:09 +02:00
endpoint (
id = " project-files-upload " ,
method = " POST " ,
path = " /projects/ {project_slug} /files/upload " ,
title = " Upload a file into a project " ,
summary = " Upload a file into a directory (parents created); text decodes to an editable file, otherwise stored as binary. " ,
auth = " user " ,
encoding = " multipart " ,
destructive = True ,
params = [
field ( " project_slug " , " path " , " string " , True , " PROJECT_SLUG " , " Project slug or uid. " ) ,
field ( " file " , " form " , " file " , True , " " , " The file to upload. " ) ,
field ( " path " , " form " , " string " , False , " assets " , " Target directory, empty for the root. " ) ,
] ,
sample_response = { " ok " : True , " redirect " : " /projects/PROJECT_SLUG/files " , " data " : { " path " : " assets/logo.png " , " type " : " file " , " is_binary " : True } } ,
) ,
endpoint (
id = " project-files-mkdir " ,
method = " POST " ,
path = " /projects/ {project_slug} /files/mkdir " ,
title = " Create a directory " ,
summary = " Create a directory and any missing parents. " ,
auth = " user " ,
encoding = " form " ,
destructive = True ,
params = [
field ( " project_slug " , " path " , " string " , True , " PROJECT_SLUG " , " Project slug or uid. " ) ,
field ( " path " , " form " , " string " , True , " src/components " , " Relative directory path. " ) ,
] ,
sample_response = { " ok " : True , " redirect " : " /projects/PROJECT_SLUG/files " , " data " : { " path " : " src/components " , " type " : " dir " } } ,
) ,
endpoint (
id = " project-files-move " ,
method = " POST " ,
path = " /projects/ {project_slug} /files/move " ,
title = " Move or rename " ,
summary = " Move or rename a file or directory (and its descendants). " ,
auth = " user " ,
encoding = " form " ,
destructive = True ,
params = [
field ( " project_slug " , " path " , " string " , True , " PROJECT_SLUG " , " Project slug or uid. " ) ,
field ( " from_path " , " form " , " string " , True , " src/old.py " , " Existing path. " ) ,
field ( " to_path " , " form " , " string " , True , " src/new.py " , " New path. " ) ,
] ,
sample_response = { " ok " : True , " redirect " : " /projects/PROJECT_SLUG/files " , " data " : { " path " : " src/new.py " } } ,
) ,
endpoint (
id = " project-files-delete " ,
method = " POST " ,
path = " /projects/ {project_slug} /files/delete " ,
title = " Delete a file or directory " ,
summary = " Delete a file, or a directory and everything under it. " ,
auth = " user " ,
encoding = " form " ,
destructive = True ,
params = [
field ( " project_slug " , " path " , " string " , True , " PROJECT_SLUG " , " Project slug or uid. " ) ,
field ( " path " , " form " , " string " , True , " src/old.py " , " Relative path to delete. " ) ,
] ,
sample_response = { " ok " : True , " redirect " : " /projects/PROJECT_SLUG/files " , " data " : { " path " : " src/old.py " } } ,
) ,
2026-06-09 01:32:57 +02:00
endpoint (
id = " project-files-zip " ,
method = " POST " ,
path = " /projects/ {project_slug} /files/zip " ,
title = " Queue a zip of files " ,
summary = " Archive the whole tree, or a subtree via the path query. Returns the job uid and status URL to poll with /zips/ {uid} . " ,
auth = " public " ,
params = [
field ( " project_slug " , " path " , " string " , True , " PROJECT_SLUG " , " Project slug or uid. " ) ,
field ( " path " , " query " , " string " , False , " src " , " Relative file or directory to archive; empty for the whole project. " ) ,
] ,
sample_response = { " uid " : " ZIP_JOB_UID " , " status_url " : " /zips/ZIP_JOB_UID " } ,
) ,
2026-06-08 22:51:09 +02:00
] ,
} ,
2026-06-09 06:41:27 +02:00
{
" slug " : " containers " ,
" title " : " Container Manager " ,
" intro " : """
# Container Manager
Build versioned Docker images for a project and run supervised container instances . Every endpoint is
* * administrator only * * ( running arbitrary Dockerfiles with docker socket access is root - equivalent ) .
Mutations flip desired state ; a single reconciler converges containers to it .
""" ,
" endpoints " : [
endpoint (
id = " containers-page " , method = " GET " , path = " /projects/ {project_slug} /containers " ,
title = " Container manager page " , summary = " The admin container manager UI for a project. " ,
auth = " admin " , interactive = True ,
params = [ field ( " project_slug " , " path " , " string " , True , " PROJECT_SLUG " , " Project slug or uid. " ) ] ,
) ,
endpoint (
id = " containers-create-dockerfile " , method = " POST " ,
path = " /projects/ {project_slug} /containers/dockerfiles " ,
title = " Create a Dockerfile " , summary = " Create a Dockerfile and queue its first build (default template if content is empty). " ,
auth = " admin " , encoding = " form " , destructive = True ,
params = [
field ( " project_slug " , " path " , " string " , True , " PROJECT_SLUG " , " Project slug or uid. " ) ,
field ( " name " , " form " , " string " , True , " myapp " , " Image name (lowercase). " ) ,
field ( " description " , " form " , " string " , False , " " , " Optional description. " ) ,
field ( " tags " , " form " , " string " , False , " " , " Optional comma separated tags. " ) ,
field ( " content " , " form " , " textarea " , False , " " , " Optional Dockerfile content. " ) ,
] ,
sample_response = { " ok " : True , " data " : { " dockerfile " : { " uid " : " DF_UID " , " name " : " myapp " } , " build_uid " : " BUILD_UID " } } ,
) ,
endpoint (
id = " containers-save-version " , method = " POST " ,
path = " /projects/ {project_slug} /containers/dockerfiles/ {df_ref} /version " ,
title = " Save a new version " , summary = " Create a new immutable version and auto-build if content changed. " ,
auth = " admin " , encoding = " form " , destructive = True ,
params = [
field ( " project_slug " , " path " , " string " , True , " PROJECT_SLUG " , " Project slug or uid. " ) ,
field ( " df_ref " , " path " , " string " , True , " DF_REF " , " Dockerfile name, slug, or uid. " ) ,
field ( " content " , " form " , " textarea " , True , " FROM python:3.13-slim " , " New content. " ) ,
] ,
) ,
endpoint (
id = " containers-build-status " , method = " GET " ,
path = " /projects/ {project_slug} /containers/builds/ {uid} " ,
title = " Build status " , summary = " Status, image tag, and stored logs for a build. " ,
auth = " admin " ,
params = [
field ( " project_slug " , " path " , " string " , True , " PROJECT_SLUG " , " Project slug or uid. " ) ,
field ( " uid " , " path " , " string " , True , " BUILD_UID " , " Build uid. " ) ,
] ,
sample_response = { " uid " : " BUILD_UID " , " image_tag " : " myapp:1 " , " status " : " success " , " logs " : " ... " } ,
) ,
endpoint (
id = " containers-create-instance " , method = " POST " ,
path = " /projects/ {project_slug} /containers/instances " ,
title = " Create an instance " , summary = " Create and (by default) start an instance from a build; /app is mounted to the project workspace. " ,
auth = " admin " , encoding = " form " , destructive = True ,
params = [
field ( " project_slug " , " path " , " string " , True , " PROJECT_SLUG " , " Project slug or uid. " ) ,
field ( " dockerfile_uid " , " form " , " string " , True , " DF_UID " , " Dockerfile uid. " ) ,
field ( " build_uid " , " form " , " string " , False , " " , " Specific build uid (defaults to latest success). " ) ,
field ( " name " , " form " , " string " , True , " staging " , " Instance name. " ) ,
field ( " boot_command " , " form " , " string " , False , " python app.py " , " Optional boot command. " ) ,
field ( " env " , " form " , " textarea " , False , " KEY=VALUE " , " Env vars, one KEY=VALUE per line. " ) ,
field ( " ports " , " form " , " string " , False , " 80 " , " Port maps. Bare container port auto-assigns a unique host port above 20000; host:container pins one. " ) ,
field ( " cpu_limit " , " form " , " string " , False , " 1.5 " , " CPU limit. " ) ,
field ( " mem_limit " , " form " , " string " , False , " 512m " , " Memory limit. " ) ,
field ( " restart_policy " , " form " , " enum " , False , " never " , " Restart policy. " ,
[ " never " , " always " , " on-failure " , " unless-stopped " ] ) ,
field ( " ingress_slug " , " form " , " string " , False , " my-service " , " Publish at /p/<slug> (optional). " ) ,
field ( " ingress_port " , " form " , " integer " , False , " 8899 " , " Container port to publish (must be a mapped port). " ) ,
] ,
) ,
endpoint (
id = " containers-ingress " ,
method = " GET " ,
path = " /p/ {slug} " ,
title = " Container ingress proxy " ,
summary = " Public reverse proxy (HTTP and WebSocket) to a running instance published via ingress_slug. The /p/<slug> prefix is stripped before forwarding. " ,
auth = " public " ,
interactive = True ,
params = [ field ( " slug " , " path " , " string " , True , " my-service " , " The instance ' s ingress_slug. " ) ] ,
) ,
endpoint (
id = " containers-instance-action " , method = " POST " ,
path = " /projects/ {project_slug} /containers/instances/ {uid} / {action} " ,
title = " Instance lifecycle " , summary = " start, stop, restart, pause, or resume an instance (flips desired state). " ,
auth = " admin " , destructive = True ,
params = [
field ( " project_slug " , " path " , " string " , True , " PROJECT_SLUG " , " Project slug or uid. " ) ,
field ( " uid " , " path " , " string " , True , " INSTANCE_UID " , " Instance uid. " ) ,
field ( " action " , " path " , " enum " , True , " start " , " Lifecycle action. " ,
[ " start " , " stop " , " restart " , " pause " , " resume " ] ) ,
] ,
) ,
endpoint (
id = " containers-instance-logs " , method = " GET " ,
path = " /projects/ {project_slug} /containers/instances/ {uid} /logs " ,
title = " Instance logs " , summary = " Recent docker logs of a running instance. " ,
auth = " admin " ,
params = [
field ( " project_slug " , " path " , " string " , True , " PROJECT_SLUG " , " Project slug or uid. " ) ,
field ( " uid " , " path " , " string " , True , " INSTANCE_UID " , " Instance uid. " ) ,
field ( " tail " , " query " , " integer " , False , " 200 " , " Number of lines. " ) ,
] ,
sample_response = { " logs " : " ... " } ,
) ,
endpoint (
id = " containers-instance-sync " , method = " POST " ,
path = " /projects/ {project_slug} /containers/instances/ {uid} /sync " ,
title = " Sync workspace " , summary = " Import the container /app workspace back into the project files. " ,
auth = " admin " , destructive = True ,
params = [
field ( " project_slug " , " path " , " string " , True , " PROJECT_SLUG " , " Project slug or uid. " ) ,
field ( " uid " , " path " , " string " , True , " INSTANCE_UID " , " Instance uid. " ) ,
] ,
) ,
] ,
} ,
2026-06-08 17:38:33 +02:00
{
" slug " : " push " ,
" title " : " Web Push " ,
" intro " : """
# Web Push
Browser push notifications via the Web Push protocol . Fetch the public VAPID key , then
register a ` PushSubscription ` obtained from the browser ' s `PushManager`.
There is no server - side unsubscribe endpoint : unsubscription is handled entirely in the
browser by calling ` PushManager . unsubscribe ( ) ` on the subscription . The server stops delivering
to a subscription once its push endpoint reports it as gone . These mirror the in - app
[ Notifications ] ( / docs / notifications . html ) feed .
Every endpoint follows the shared [ Conventions & Errors ] ( / docs / conventions . html ) ( auth , content
negotiation , pagination , status codes ) ; see [ Authentication ] ( / docs / authentication . html ) for the
four ways to sign requests .
""" ,
" endpoints " : [
endpoint (
id = " push-key " ,
method = " GET " ,
path = " /push.json " ,
title = " Get the public key " ,
summary = " Return the VAPID public key for subscribing. " ,
auth = " public " ,
sample_response = { " publicKey " : " BASE64_VAPID_KEY " } ,
) ,
endpoint (
id = " push-register " ,
method = " POST " ,
path = " /push.json " ,
title = " Register a subscription " ,
summary = " Register a browser push subscription. Sends a welcome notification. " ,
auth = " user " ,
encoding = " json " ,
interactive = False ,
params = [
field ( " endpoint " , " json " , " string " , True , " https://fcm.googleapis.com/... " , " Subscription endpoint URL. " ) ,
field ( " keys " , " json " , " string " , True , ' { " p256dh " : " ... " , " auth " : " ... " } ' , " Subscription keys object. " ) ,
] ,
notes = [ " The body must be JSON: ` { \" endpoint \" : \" ... \" , \" keys \" : { \" p256dh \" : \" ... \" , \" auth \" : \" ... \" }}`. " ] ,
sample_response = { " registered " : True } ,
) ,
] ,
} ,
{
" slug " : " bugs " ,
" title " : " Bug Reports " ,
" intro " : """
# Bug Reports
A public board for reporting issues . The list renders HTML ; reporting uses form fields , and a
report can carry files uploaded via [ Uploads ] ( / docs / uploads . html ) .
Every endpoint follows the shared [ Conventions & Errors ] ( / docs / conventions . html ) ( auth , content
negotiation , pagination , status codes ) ; see [ Authentication ] ( / docs / authentication . html ) for the
four ways to sign requests .
""" ,
" endpoints " : [
endpoint (
id = " bugs-list " ,
method = " GET " ,
path = " /bugs " ,
title = " View bug reports " ,
summary = " Render the bug board. Returns an HTML page. " ,
auth = " public " ,
interactive = True ,
) ,
endpoint (
id = " bugs-create " ,
method = " POST " ,
path = " /bugs/create " ,
title = " Report a bug " ,
summary = " File a bug report. " ,
auth = " user " ,
encoding = " form " ,
destructive = True ,
params = [
field ( " title " , " form " , " string " , True , " Login button misaligned " , " Title, 1-200 characters. " ) ,
field ( " description " , " form " , " textarea " , True , " Steps to reproduce... " , " Description, 1-5000 characters. " ) ,
] ,
) ,
] ,
} ,
{
" slug " : " gateway " ,
" title " : " OpenAI Gateway " ,
" admin " : True ,
" intro " : """
# OpenAI Gateway
An OpenAI - compatible proxy mounted at ` / openai / v1 ` . It forwards requests to a configured
upstream using the gateway ' s own credentials, so no DevPlace key is required - but an
administrator must enable the ` openai ` service first . Point any OpenAI - compatible client at
` { { base } } / openai / v1 ` .
This gateway is the * * single point of truth for AI * * on the platform . Every other DevPlace
service ( news , bots , Devii ) calls it by default instead of an external provider , sends the
generic model name ` molodetz ` , and authenticates with an internal key that is auto - generated
on first boot . The real provider URLs , models , and keys ( DeepSeek , OpenRouter ) live only here ,
so an operator switches providers or backends in one place . ` DEEPSEEK_API_KEY ` and
` OPENROUTER_API_KEY ` are migrated into the editable settings on boot and the value in use is
shown . Because ` Force model ` is on by default , the upstream always receives the configured
model regardless of what a client ( or ` molodetz ` ) requests .
2026-06-08 22:51:09 +02:00
The gateway also performs * * vision * * augmentation : when a request includes an image , the gateway
describes the image with a configured vision model and rewrites it to text , so a vision - less
upstream still works . The vision model , URL , and key are configured alongside the other gateway
settings .
2026-06-08 17:38:33 +02:00
Administrators enable and configure this gateway under [ Background Services ] ( / docs / services . html )
( the ` openai ` service ) .
The gateway is exempt from rate limiting , but every other endpoint follows the shared
[ Conventions & Errors ] ( / docs / conventions . html ) ; see [ Authentication ] ( / docs / authentication . html )
for signing DevPlace ' s own requests.
""" ,
" endpoints " : [
endpoint (
id = " gateway-chat " ,
method = " POST " ,
path = " /openai/v1/chat/completions " ,
title = " Chat completions " ,
summary = " OpenAI-compatible chat completion. Supports streaming. " ,
2026-06-08 22:51:09 +02:00
auth = " user " ,
2026-06-08 17:38:33 +02:00
encoding = " json " ,
params = [
field ( " model " , " json " , " string " , False , " gpt-4o-mini " , " Model id; the gateway may override it. " ) ,
field ( " messages " , " json " , " string " , True , ' [ { " role " : " user " , " content " : " Hello " }] ' , " Chat messages array. " ) ,
field ( " stream " , " json " , " string " , False , " false " , " Set true for a streamed SSE response. " ) ,
] ,
notes = [ " Returns `503` when the gateway service is not running. " ] ,
) ,
endpoint (
id = " gateway-passthrough " ,
method = " POST " ,
path = " /openai/v1/ {path} " ,
title = " Passthrough " ,
summary = " Any other /v1 path is forwarded to the upstream as-is. " ,
2026-06-08 22:51:09 +02:00
auth = " user " ,
2026-06-08 17:38:33 +02:00
interactive = False ,
params = [ field ( " path " , " path " , " string " , True , " models " , " Upstream API path after /v1/. " ) ] ,
) ,
] ,
} ,
{
" slug " : " services " ,
" title " : " Background Services " ,
" admin " : True ,
" dynamic " : True ,
" intro " : " " ,
" endpoints " : [ ] ,
} ,
{
" slug " : " admin " ,
" title " : " Admin API " ,
" admin " : True ,
" intro " : """
# Admin API
Site administration endpoints for users , news curation , and settings . Every call requires an
* * admin * * account . Background service management lives on the
[ Background Services ] ( / docs / services . html ) page .
Every endpoint follows the shared [ Conventions & Errors ] ( / docs / conventions . html ) ( auth , content
negotiation , pagination , status codes ) ; see [ Authentication ] ( / docs / authentication . html ) for the
four ways to sign requests .
""" ,
" endpoints " : [
endpoint (
id = " admin-users " ,
method = " GET " ,
path = " /admin/users " ,
title = " List users " ,
summary = " Paginated user management page. Returns HTML. " ,
auth = " admin " ,
interactive = True ,
params = [ field ( " page " , " query " , " int " , False , " 1 " , " Page number. " ) ] ,
) ,
endpoint (
id = " admin-analytics " ,
method = " GET " ,
path = " /admin/analytics " ,
title = " Site analytics " ,
summary = (
" One-call aggregate analytics, returned as JSON: total members, active users "
" in the last 24h/7d/30d, users signed in now, new signups (24h/7d/30d), content "
" totals (posts, comments, gists, projects, news), and top authors. Use this "
" instead of paging the user list to count or measure activity. "
) ,
auth = " admin " ,
interactive = True ,
params = [ field ( " top_n " , " query " , " int " , False , " 10 " , " How many top authors to include (1-50). " ) ] ,
notes = [ " This is the endpoint the Devii assistant calls as `site_analytics`; see [Devii internals](/docs/devii-internals.html). " ] ,
) ,
endpoint (
id = " admin-ai-usage " ,
method = " GET " ,
path = " /admin/ai-usage/data " ,
title = " AI gateway usage analytics " ,
summary = (
" One-call AI gateway metrics, returned as JSON for a bounded time window: request "
" volume and throughput, token usage with averages and percentiles (p50/p90/p95/p99), "
" latency (upstream, gateway overhead, queue wait, connection establishment), error "
" rates by category, cost in USD (per model, per caller, input vs output, projected "
" monthly burn, caching savings), caller behavior, and an hourly breakdown. Cost is "
" taken from the upstream native cost when present (OpenRouter) and computed from the "
" configured per-million pricing otherwise (DeepSeek). "
) ,
auth = " admin " ,
interactive = True ,
params = [
field ( " hours " , " query " , " int " , False , " 48 " , " Lookback window in hours (1-168). " ) ,
field ( " top_n " , " query " , " int " , False , " 10 " , " How many rows in each top-N breakdown. " ) ,
] ,
notes = [ " TTFT and inter-token latency are not reported: the gateway forwards non-streaming to the upstream. " ] ,
) ,
2026-06-08 22:51:09 +02:00
endpoint (
id = " admin-user-ai-usage " ,
method = " GET " ,
path = " /admin/users/ {uid} /ai-usage " ,
title = " Per-user AI usage " ,
summary = (
" One user ' s AI gateway usage over the last 24 hours, returned as JSON: request "
" volume, success and error rates, token totals, cost (window, per hour, per request, "
" and a 30-day projection from the full 24h spend), average latency and throughput, a "
" per-model breakdown, and an hourly cost series. Because Devii operates a signed-in "
" user ' s account with that user ' s own API key, this is the user ' s complete gateway "
" spend, whether driven through Devii or direct API calls. Shown admin-only on the "
" user ' s profile page. "
) ,
auth = " admin " ,
interactive = True ,
params = [
field ( " uid " , " path " , " string " , True , " USER_UID " , " Target user UID. " ) ,
field ( " hours " , " query " , " int " , False , " 24 " , " Lookback window in hours (1-168). " ) ,
] ,
sample_response = {
" owner_id " : " USER_UID " , " window_hours " : 24 , " requests " : 42 , " success " : 41 ,
" failed " : 1 , " success_pct " : 97.6 , " error_pct " : 2.4 ,
" tokens " : { " prompt " : 120000 , " completion " : 38000 , " total " : 158000 } ,
" cost " : { " window_usd " : 0.214 , " per_hour_usd " : 0.0089 , " per_request_usd " : 0.0051 , " projected_30d_usd " : 6.42 } ,
" latency " : { " avg_ms " : 1830.0 , " avg_tps " : 41.2 } ,
" by_model " : [ { " key " : " molodetz " , " requests " : 42 , " total_tokens " : 158000 , " cost_usd " : 0.214 } ] ,
" hourly " : [ { " hour " : " 2026-06-08T16 " , " requests " : 6 , " cost_usd " : 0.031 , " total_tokens " : 22000 } ] ,
} ,
) ,
2026-06-08 17:38:33 +02:00
endpoint (
id = " admin-user-role " ,
method = " POST " ,
path = " /admin/users/ {uid} /role " ,
title = " Set a user role " ,
summary = " Promote or demote a user. You cannot change your own role. " ,
auth = " admin " ,
encoding = " form " ,
destructive = True ,
params = [
field ( " uid " , " path " , " string " , True , " USER_UID " , " Target user UID. " ) ,
field ( " role " , " form " , " enum " , True , " member " , " New role. " , [ " member " , " admin " ] ) ,
] ,
) ,
endpoint (
id = " admin-user-password " ,
method = " POST " ,
path = " /admin/users/ {uid} /password " ,
title = " Reset a user password " ,
summary = " Set a new password for a user. " ,
auth = " admin " ,
encoding = " form " ,
destructive = True ,
params = [
field ( " uid " , " path " , " string " , True , " USER_UID " , " Target user UID. " ) ,
field ( " password " , " form " , " string " , True , " newpassword " , " New password, 6+ characters. " ) ,
] ,
) ,
endpoint (
id = " admin-user-toggle " ,
method = " POST " ,
path = " /admin/users/ {uid} /toggle " ,
title = " Enable or disable a user " ,
summary = " Toggle a user ' s active state. You cannot disable yourself. " ,
auth = " admin " ,
destructive = True ,
params = [ field ( " uid " , " path " , " string " , True , " USER_UID " , " Target user UID. " ) ] ,
) ,
endpoint (
id = " admin-news-list " ,
method = " GET " ,
path = " /admin/news " ,
title = " List news articles " ,
summary = " Paginated news management page. Returns HTML. " ,
auth = " admin " ,
interactive = True ,
params = [ field ( " page " , " query " , " int " , False , " 1 " , " Page number. " ) ] ,
) ,
endpoint (
id = " admin-news-toggle " ,
method = " POST " ,
path = " /admin/news/ {uid} /toggle " ,
title = " Toggle featured " ,
summary = " Toggle an article ' s featured flag. " ,
auth = " admin " ,
destructive = True ,
params = [ field ( " uid " , " path " , " string " , True , " NEWS_UID " , " Article UID. " ) ] ,
) ,
endpoint (
id = " admin-news-publish " ,
method = " POST " ,
path = " /admin/news/ {uid} /publish " ,
title = " Toggle published " ,
summary = " Switch an article between draft and published. " ,
auth = " admin " ,
destructive = True ,
params = [ field ( " uid " , " path " , " string " , True , " NEWS_UID " , " Article UID. " ) ] ,
) ,
endpoint (
id = " admin-news-landing " ,
method = " POST " ,
path = " /admin/news/ {uid} /landing " ,
title = " Toggle landing " ,
summary = " Toggle whether an article shows on the landing page. " ,
auth = " admin " ,
destructive = True ,
params = [ field ( " uid " , " path " , " string " , True , " NEWS_UID " , " Article UID. " ) ] ,
) ,
endpoint (
id = " admin-news-delete " ,
method = " POST " ,
path = " /admin/news/ {uid} /delete " ,
title = " Delete a news article " ,
summary = " Delete an article and its images. " ,
auth = " admin " ,
destructive = True ,
params = [ field ( " uid " , " path " , " string " , True , " NEWS_UID " , " Article UID. " ) ] ,
) ,
endpoint (
id = " admin-settings-get " ,
method = " GET " ,
path = " /admin/settings " ,
title = " Read site settings " ,
summary = " Return the current site and operational settings. Negotiates HTML or JSON. " ,
auth = " admin " ,
interactive = True ,
) ,
endpoint (
id = " admin-settings " ,
method = " POST " ,
path = " /admin/settings " ,
title = " Save site settings " ,
summary = " Update operational and site settings. Empty fields are skipped. " ,
auth = " admin " ,
encoding = " form " ,
destructive = True ,
params = [
field ( " site_name " , " form " , " string " , False , " DevPlace " , " Site name. " ) ,
field ( " rate_limit_per_minute " , " form " , " string " , False , " 60 " , " Requests per window. " ) ,
field ( " registration_open " , " form " , " enum " , False , " 1 " , " Allow signups. " , [ " 1 " , " 0 " ] ) ,
field ( " maintenance_mode " , " form " , " enum " , False , " 0 " , " Maintenance gate. " , [ " 1 " , " 0 " ] ) ,
] ,
notes = [ " Accepts every field on the admin settings form; only non-empty values are written. " ] ,
) ,
] ,
} ,
]
from devplacepy import schemas
from devplacepy . docs_examples import schema_example , action_example
_PAGE_RESPONSES = {
" feed-list " : schemas . FeedOut ,
" posts-detail " : schemas . PostDetailOut ,
" projects-list " : schemas . ProjectsOut ,
" projects-detail " : schemas . ProjectDetailOut ,
" gists-list " : schemas . GistsOut ,
" gists-detail " : schemas . GistDetailOut ,
" news-list " : schemas . NewsListOut ,
" news-detail " : schemas . NewsDetailOut ,
" profile-detail " : schemas . ProfileOut ,
" leaderboard " : schemas . LeaderboardOut ,
" messages-inbox " : schemas . MessagesOut ,
" notifications-list " : schemas . NotificationsOut ,
" bugs-list " : schemas . BugsOut ,
" bookmarks-saved " : schemas . SavedOut ,
" admin-users " : schemas . AdminUsersOut ,
" admin-news-list " : schemas . AdminNewsOut ,
" admin-settings-get " : schemas . AdminSettingsOut ,
}
_ACTION_RESPONSES = {
" posts-create " : ( " /posts/POST_SLUG " , { " uid " : " POST_UID " , " slug " : " POST_SLUG " , " url " : " /posts/POST_SLUG " } ) ,
" posts-edit " : ( " /posts/POST_SLUG " , { " uid " : " POST_UID " , " slug " : " POST_SLUG " , " url " : " /posts/POST_SLUG " } ) ,
" posts-delete " : ( " /feed " , None ) ,
" comments-create " : ( " /posts/POST_SLUG#comment-COMMENT_UID " , { " uid " : " COMMENT_UID " , " url " : " /posts/POST_SLUG#comment-COMMENT_UID " } ) ,
" comments-delete " : ( " /posts/POST_SLUG " , None ) ,
" projects-create " : ( " /projects/PROJECT_SLUG " , { " uid " : " PROJECT_UID " , " slug " : " PROJECT_SLUG " , " url " : " /projects/PROJECT_SLUG " } ) ,
" projects-delete " : ( " /projects " , None ) ,
" gists-create " : ( " /gists/GIST_SLUG " , { " uid " : " GIST_UID " , " slug " : " GIST_SLUG " , " url " : " /gists/GIST_SLUG " } ) ,
" gists-edit " : ( " /gists/GIST_SLUG " , { " uid " : " GIST_UID " , " slug " : " GIST_SLUG " , " url " : " /gists/GIST_SLUG " } ) ,
" gists-delete " : ( " /gists " , None ) ,
" profile-update " : ( " /profile/YOUR_USERNAME " , None ) ,
" follow-user " : ( " /profile/bob_test " , None ) ,
" unfollow-user " : ( " /profile/bob_test " , None ) ,
" messages-send " : ( " /messages?with_uid=RECEIVER_UID " , { " uid " : " MESSAGE_UID " } ) ,
" notifications-open " : ( " /posts/POST_SLUG#comment-COMMENT_UID " , None ) ,
" notifications-mark-read " : ( " /notifications " , None ) ,
" notifications-mark-all-read " : ( " /notifications " , None ) ,
" bugs-create " : ( " /bugs " , { " uid " : " BUG_UID " } ) ,
" admin-user-role " : ( " /admin/users " , None ) ,
" admin-user-password " : ( " /admin/users " , None ) ,
" admin-user-toggle " : ( " /admin/users " , None ) ,
" admin-news-toggle " : ( " /admin/news " , None ) ,
" admin-news-publish " : ( " /admin/news " , None ) ,
" admin-news-landing " : ( " /admin/news " , None ) ,
" admin-news-delete " : ( " /admin/news " , None ) ,
" admin-settings " : ( " /admin/settings " , None ) ,
}
def _apply_negotiated_responses ( ) :
for group in API_GROUPS :
for ep in group . get ( " endpoints " , [ ] ) or [ ] :
if ep . get ( " sample_response " ) is None :
if ep [ " id " ] in _PAGE_RESPONSES :
ep [ " sample_response " ] = schema_example ( _PAGE_RESPONSES [ ep [ " id " ] ] )
elif ep [ " id " ] in _ACTION_RESPONSES :
redirect , data = _ACTION_RESPONSES [ ep [ " id " ] ]
ep [ " sample_response " ] = action_example ( redirect , data )
ep [ " negotiation " ] = _classify ( ep )
_apply_negotiated_responses ( )
_GROUPS_BY_SLUG = { group [ " slug " ] : group for group in API_GROUPS }
def get_group ( slug ) :
return _GROUPS_BY_SLUG . get ( slug )
def api_doc_pages ( ) :
return [
{
" slug " : group [ " slug " ] ,
" title " : group [ " title " ] ,
" kind " : " api " ,
" admin " : group . get ( " admin " , False ) ,
" dynamic " : group . get ( " dynamic " , False ) ,
}
for group in API_GROUPS
]
def _field_to_param ( spec ) :
description = spec [ " label " ]
if spec . get ( " help " ) :
description = f " { spec [ ' label ' ] } - { spec [ ' help ' ] } "
value = " " if spec . get ( " secret " ) else str ( spec . get ( " value " , " " ) )
if spec . get ( " secret " ) :
description + = " Leave blank to keep the current value. "
if spec [ " type " ] == " bool " :
return field ( spec [ " key " ] , " form " , " enum " , False , value or " 0 " , description , [ " 1 " , " 0 " ] )
if spec [ " type " ] == " select " :
options = [ option [ " value " ] for option in spec . get ( " options " ) or [ ] ]
return field ( spec [ " key " ] , " form " , " enum " , False , value or ( options [ 0 ] if options else " " ) , description , options )
if spec [ " type " ] in ( " int " , " float " ) :
return field ( spec [ " key " ] , " form " , " int " , False , value , description )
return field ( spec [ " key " ] , " form " , " string " , False , value , description )
def _service_section ( service ) :
enabled = " yes " if service . get ( " enabled " ) else " no "
groups = " , " . join ( group [ " name " ] for group in service . get ( " field_groups " , [ ] ) )
lines = [
f " ## { service . get ( ' title ' ) or service [ ' name ' ] } " ,
" " ,
service . get ( " description " , " " ) ,
" " ,
f " - Service name: ` { service [ ' name ' ] } ` " ,
f " - Enabled: { enabled } " ,
f " - Run interval: { service . get ( ' interval_seconds ' , 0 ) } s " ,
]
if groups :
lines . append ( f " - Configuration groups: { groups } " )
return " \n " . join ( lines )
def build_services_group ( services , base ) :
intro_parts = [
" # Background Services " ,
" " ,
" DevPlace runs background services supervised by a single manager. Each service has its "
" own enable flag, run interval, live status, metrics, and a rolling log buffer. Manage "
" them from the admin panel at `/admin/services` or with the endpoints below. " ,
" " ,
" Control a service by its `name` (shown per service): start or stop it, trigger a single "
" run, clear its logs, or save its configuration. `GET /admin/services/data` returns the "
" live status, metrics, and log tail for every service. " ,
" " ,
" See the [Admin API](/docs/admin.html) for the rest of administration; the Devii assistant "
" is itself a service, configured in [Configuration and CLI](/docs/devii-config.html). " ,
]
config_endpoints = [ ]
for service in services :
intro_parts . append ( " " )
intro_parts . append ( _service_section ( service ) )
params = [
_field_to_param ( spec )
for spec in service . get ( " fields " , [ ] )
if not spec [ " key " ] . endswith ( " _enabled " )
]
config_endpoints . append ( endpoint (
id = f " services-config- { service [ ' name ' ] } " ,
method = " POST " ,
path = f " /admin/services/ { service [ ' name ' ] } /config " ,
title = f " Configure { service . get ( ' title ' ) or service [ ' name ' ] } " ,
summary = f " Save configuration for the { service [ ' name ' ] } service. Empty values keep the current setting. " ,
auth = " admin " ,
encoding = " form " ,
destructive = True ,
params = params ,
notes = [ " Use start/stop to enable or disable the service; this saves the remaining settings. " ] ,
) )
control_endpoints = [
endpoint (
id = " services-data " ,
method = " GET " ,
path = " /admin/services/data " ,
title = " Service status " ,
summary = " Live status, metrics, and log tail for every background service. " ,
auth = " admin " ,
sample_response = { " services " : [ { " name " : " news " , " status " : " running " , " enabled " : True } ] } ,
) ,
endpoint (
id = " services-detail-data " ,
method = " GET " ,
path = " /admin/services/ {name} /data " ,
title = " One service status " ,
summary = " Live status, metrics, and log tail for a single background service. " ,
auth = " admin " ,
params = [ field ( " name " , " path " , " string " , True , " news " , " Service name. " ) ] ,
sample_response = { " service " : { " name " : " news " , " status " : " running " , " enabled " : True } } ,
) ,
* _service_control_endpoints ( ) ,
]
endpoints = control_endpoints + config_endpoints
for ep in endpoints :
ep [ " negotiation " ] = _classify ( ep )
return {
" slug " : " services " ,
" title " : " Background Services " ,
" admin " : True ,
" intro " : " \n " . join ( intro_parts ) ,
" endpoints " : endpoints ,
}
def _substitute ( value , replacements ) :
if isinstance ( value , str ) :
for token , replacement in replacements . items ( ) :
value = value . replace ( token , replacement )
return value
if isinstance ( value , list ) :
return [ _substitute ( item , replacements ) for item in value ]
if isinstance ( value , dict ) :
return { key : _substitute ( item , replacements ) for key , item in value . items ( ) }
return value
def render_group ( slug , base , username , api_key ) :
group = get_group ( slug )
if not group :
return None
replacements = {
" {{ base }} " : base ,
" {{ username }} " : username or " YOUR_USERNAME " ,
" {{ api_key }} " : api_key or " YOUR_API_KEY " ,
}
return _substitute ( group , replacements )