docs: add block/mute user relations, emoji-sync CLI, and uid indexes
DevPlace CI / test (push) Failing after 2m7s

- Add `/block`, `/mute` endpoints with block/unblock and mute/unmute functionality in `routers/relations.py`, hiding blocked users' content everywhere except their own profile while muting only suppresses notifications
- Introduce `devplace emoji-sync` CLI command to regenerate `static/js/emoji-shortcodes.js` from the emoji library, documented in `CLAUDE.md` and wired in `cli.py`
- Create `get_blocked_uids()` database helper and apply it in `content.py` `load_detail()` to filter blocked users' posts from detail views
- Implement `_uid_index()` and `_drop_index()` helpers in `database.py` for unique uid indexes across tables, with `user_relations` added to `SOFT_DELETE_TABLES`
- Document new routes in `AGENTS.md` and `README.md`, including emoji shortcodes rendering behavior distinct from the emoji picker
This commit is contained in:
2026-06-19 08:06:09 +00:00
parent f3a4667fce
commit 741d7aade6
136 changed files with 3025 additions and 564 deletions
+24 -22
View File
@@ -7,7 +7,7 @@
<div id="gateway-admin">
<div class="admin-toolbar">
<h2>Gateway routing</h2>
<span class="admin-count" id="gw-count"></span>
<span class="admin-count" id="gw-count" role="status" aria-live="polite"></span>
</div>
<p class="gw-intro">Map any requested model name onto a provider and target model, each with its own pricing economy and an optional vision model for image to text merging. Unmapped requests fall through to the default upstream unchanged.</p>
@@ -16,21 +16,22 @@
<h3>Providers</h3>
</div>
<p class="gw-section-hint">Named upstreams reused across model routes. A model route with a blank provider uses the default below.</p>
<div class="gw-default" id="gw-default"></div>
<div class="gw-default" id="gw-default" role="status" aria-live="polite"></div>
<div class="admin-table-wrap">
<table class="admin-table">
<caption class="sr-only">Providers</caption>
<thead>
<tr><th>Name</th><th>Base URL</th><th>Active</th><th class="gw-actions">Actions</th></tr>
<tr><th scope="col">Name</th><th scope="col">Base URL</th><th scope="col">Active</th><th scope="col" class="gw-actions">Actions</th></tr>
</thead>
<tbody id="gw-providers"></tbody>
</table>
</div>
<form class="gw-form" id="gw-provider-form" autocomplete="off">
<form class="gw-form" id="gw-provider-form" autocomplete="off" role="group" aria-label="Add or update provider">
<p class="gw-form-title">Add or update provider</p>
<div class="gw-field"><label>Name</label><input type="text" name="name" placeholder="openrouter" required></div>
<div class="gw-field"><label>Base URL (chat completions)</label><input type="url" name="base_url" placeholder="https://openrouter.ai/api/v1/chat/completions"></div>
<div class="gw-field"><label>API key</label><input type="password" name="api_key" placeholder="sk-..." autocomplete="new-password"></div>
<div class="gw-field"><label>Active</label><select name="is_active"><option value="1">Yes</option><option value="0">No</option></select></div>
<div class="gw-field"><label for="gw-provider-name">Name</label><input type="text" id="gw-provider-name" name="name" placeholder="openrouter" required aria-required="true"></div>
<div class="gw-field"><label for="gw-provider-base-url">Base URL (chat completions)</label><input type="url" id="gw-provider-base-url" name="base_url" placeholder="https://openrouter.ai/api/v1/chat/completions"></div>
<div class="gw-field"><label for="gw-provider-api-key">API key</label><input type="password" id="gw-provider-api-key" name="api_key" placeholder="sk-..." autocomplete="new-password"></div>
<div class="gw-field"><label for="gw-provider-active">Active</label><select id="gw-provider-active" name="is_active"><option value="1">Yes</option><option value="0">No</option></select></div>
<div class="gw-form-actions"><button type="submit" class="admin-btn admin-btn-primary">Save provider</button></div>
</form>
</section>
@@ -42,26 +43,27 @@
<p class="gw-section-hint">Each source model maps to a provider and target model with its own economy (USD per 1M tokens). Chat uses cache-hit, cache-miss and output prices; embeddings use the input price; a vision model adds input and output pricing for the image description merge.</p>
<div class="admin-table-wrap">
<table class="admin-table">
<caption class="sr-only">Model routes</caption>
<thead>
<tr><th>Source</th><th>Provider</th><th>Target</th><th>Kind</th><th>Vision</th><th class="gw-actions">Actions</th></tr>
<tr><th scope="col">Source</th><th scope="col">Provider</th><th scope="col">Target</th><th scope="col">Kind</th><th scope="col">Vision</th><th scope="col" class="gw-actions">Actions</th></tr>
</thead>
<tbody id="gw-models"></tbody>
</table>
</div>
<form class="gw-form" id="gw-model-form" autocomplete="off">
<form class="gw-form" id="gw-model-form" autocomplete="off" role="group" aria-label="Add or update model route">
<p class="gw-form-title">Add or update model route</p>
<div class="gw-field"><label>Source model (requested)</label><input type="text" name="source_model" placeholder="gpt-4o" required></div>
<div class="gw-field"><label>Provider</label><select name="provider" data-provider-select></select></div>
<div class="gw-field"><label>Target model (upstream)</label><input type="text" name="target_model" placeholder="openai/gpt-4o" required></div>
<div class="gw-field"><label>Kind</label><select name="kind"><option value="chat">chat</option><option value="embed">embed</option></select></div>
<div class="gw-field"><label>Vision provider</label><select name="vision_provider" data-provider-select></select></div>
<div class="gw-field"><label>Vision model</label><input type="text" name="vision_model" placeholder="(optional) google/gemma-3-12b-it"></div>
<div class="gw-field"><label>Context window</label><input type="number" name="context_window" min="0" value="0"></div>
<div class="gw-field"><label>Active</label><select name="is_active"><option value="1">Yes</option><option value="0">No</option></select></div>
<div class="gw-field"><label>Price cache-hit / 1M ($)</label><input type="number" name="price_cache_hit_per_m" min="0" step="0.0001" value="0"></div>
<div class="gw-field"><label>Price cache-miss / 1M ($)</label><input type="number" name="price_cache_miss_per_m" min="0" step="0.0001" value="0"></div>
<div class="gw-field"><label>Price output / 1M ($)</label><input type="number" name="price_output_per_m" min="0" step="0.0001" value="0"></div>
<div class="gw-field"><label>Price input / 1M ($) (embed/vision)</label><input type="number" name="price_input_per_m" min="0" step="0.0001" value="0"></div>
<div class="gw-field"><label for="gw-model-source">Source model (requested)</label><input type="text" id="gw-model-source" name="source_model" placeholder="gpt-4o" required aria-required="true"></div>
<div class="gw-field"><label for="gw-model-provider">Provider</label><select id="gw-model-provider" name="provider" data-provider-select></select></div>
<div class="gw-field"><label for="gw-model-target">Target model (upstream)</label><input type="text" id="gw-model-target" name="target_model" placeholder="openai/gpt-4o" required aria-required="true"></div>
<div class="gw-field"><label for="gw-model-kind">Kind</label><select id="gw-model-kind" name="kind"><option value="chat">chat</option><option value="embed">embed</option></select></div>
<div class="gw-field"><label for="gw-model-vision-provider">Vision provider</label><select id="gw-model-vision-provider" name="vision_provider" data-provider-select></select></div>
<div class="gw-field"><label for="gw-model-vision-model">Vision model</label><input type="text" id="gw-model-vision-model" name="vision_model" placeholder="(optional) google/gemma-3-12b-it"></div>
<div class="gw-field"><label for="gw-model-context">Context window</label><input type="number" id="gw-model-context" name="context_window" min="0" value="0"></div>
<div class="gw-field"><label for="gw-model-active">Active</label><select id="gw-model-active" name="is_active"><option value="1">Yes</option><option value="0">No</option></select></div>
<div class="gw-field"><label for="gw-model-price-cache-hit">Price cache-hit / 1M ($)</label><input type="number" id="gw-model-price-cache-hit" name="price_cache_hit_per_m" min="0" step="0.0001" value="0"></div>
<div class="gw-field"><label for="gw-model-price-cache-miss">Price cache-miss / 1M ($)</label><input type="number" id="gw-model-price-cache-miss" name="price_cache_miss_per_m" min="0" step="0.0001" value="0"></div>
<div class="gw-field"><label for="gw-model-price-output">Price output / 1M ($)</label><input type="number" id="gw-model-price-output" name="price_output_per_m" min="0" step="0.0001" value="0"></div>
<div class="gw-field"><label for="gw-model-price-input">Price input / 1M ($) (embed/vision)</label><input type="number" id="gw-model-price-input" name="price_input_per_m" min="0" step="0.0001" value="0"></div>
<div class="gw-form-actions"><button type="submit" class="admin-btn admin-btn-primary">Save model route</button></div>
</form>
</section>