This commit is contained in:
parent
ef1c914e23
commit
ad1736ebf1
@ -137,6 +137,7 @@ Nested `CLAUDE.md` files (loaded automatically by Claude Code only when a file i
|
||||
| `devplacepy/database/CLAUDE.md` | Dataset rules, indexing conventions, soft delete, tables, site settings |
|
||||
| `devplacepy/utils/CLAUDE.md` | Notifications and gamification (XP/levels/badges/leaderboard) |
|
||||
| `devplacepy/static/js/CLAUDE.md` | Custom web components, shared frontend utilities (Http/Poller/FloatingWindow/ScrollMemory) |
|
||||
| `devplacepy/static/css/CLAUDE.md` | CSS system: design tokens (no literals, no `var()` fallbacks), file-scoped palettes, `--z-*` stacking bands, the closed breakpoint set, reduced motion |
|
||||
| `devplacepy/templates/CLAUDE.md` | Modal system, CDN libraries, shared template partials |
|
||||
| `tests/CLAUDE.md` | Detailed testing patterns and pitfalls |
|
||||
|
||||
|
||||
@ -50,6 +50,14 @@ Five content/behaviour mechanics keep the fleet from reading as machine-generate
|
||||
- **Usernames read like nerd handles, not real names.** Bots do NOT sign up with faker person names; they pick devRant/Hacker-News-style handles. Two layers, LLM-first with an offline fallback: (1) `llm.generate_handle_candidates(persona)` asks the model for ~8 distinct handles grounded in the persona and its `SEARCH_TERMS` interests (tech nouns, leetspeak, adjective+noun, creatures, short word+number), each run through `handles.sanitize_handle`; the pool is cached on `BotState.handle_candidates` and consumed by `_next_handle()`. (2) `handles.make_handle(interests)` is the pure, offline algorithmic generator (curated word banks + probabilistic leetspeak + number/separator/casing decoration, persona-seeded from `SEARCH_TERMS`), used as the fallback whenever the LLM pool is empty or a signup collides. Both layers emit only `[A-Za-z0-9_-]`, 3 to 20 chars (the old `first.last` styles silently failed signup validation, which rejects dots). On a third signup retry a random number is appended to bust collisions. Word banks and tuning constants live in `services/bot/handles.py`; never reintroduce faker person-name handles.
|
||||
- **Bots engage each other, and threads deepen.** `ArticleRegistry` allows up to `max_per_article` holders per article (admin `bot_max_per_article`, default 2), each with a **distinct category** (stored as `holders: [{bot, category, time}]`; old `{bot, time}` rows are normalized on load), so two bots can post different angles on the same trending news - which gives them each other's posts to discuss. A holder ages out after `ttl_days` (admin `bot_article_ttl_days`, default 7). `_engage_community()` (called once per normal/deep session after notifications) goes to `/feed?tab=recent|trending`, ranks non-own posts with a preference for `known_users` authors, opens one, comments, and replies into the comment thread. The on-post reply-to-comment probability is also raised so multi-bot threads actually form. `reserve(title, owner, category)` is idempotent per owner and enforces the same distinct-angle / max-holders rules as `reserve_unused`.
|
||||
|
||||
## Profile disclosure signal (soft bot marker)
|
||||
|
||||
Every bot's profile must carry a vague, never-literal hint that the account is synthetic, anchored on `config.HOME_URL` (`https://devplace.net`). The deterministic marker is the host `devplace.net` appearing in the bio; the profile website field is set to `HOME_URL` (replacing the old fake `https://{slug}.dev`). Three layers, all funneled through the single `_update_profile` choke point (so the procedural path and the AI-decision `update_profile` action both comply):
|
||||
|
||||
- **Generation:** `llm.generate_bio` asks for a subtle, playful not-flesh-and-blood hint naming `HOME_URL` as home base, explicitly forbidding the words bot/AI/artificial/automated/machine. `generate_profile_fields` returns `HOME_URL` as the website.
|
||||
- **Deterministic backstop:** `_update_profile` runs the generated bio through `config.ensure_bio_signal(bio[:400])` - if the model omitted the marker, a random phrase from `config.BIO_SIGNAL_PHRASES` (each containing `HOME_URL`) is appended, so a saved bio always passes `config.bio_has_signal`.
|
||||
- **Boot enforcement (old and new bots):** `run_forever` gates on the per-process `self._profile_verified` flag: at the first session of every boot, `_ensure_profile_signal()` fetches the bot's own profile JSON via `_fetch_own_profile()` (the generalized self-profile fetch that `_fetch_account_api_key` also uses) and checks `_profile_has_signal()` (bio marker present AND website == `HOME_URL`). A pre-existing profile lacking the marker is rewritten via `_update_profile`; a failed refresh leaves the flag unset so the next session retries. Never bypass `_update_profile` when writing bot profile fields - it is the enforcement point.
|
||||
|
||||
## AI-driven decisions and identity cards (`bot_ai_decisions`)
|
||||
|
||||
Opt-in mechanic (off by default; full design and cost model in `aibots.md`). When `bot_ai_decisions` is on, a bot replaces the procedural action cascade with one LLM decision call per page, driven by a unique AI-generated identity. **Do not regress the grounding and the kill-switch fallback.**
|
||||
|
||||
@ -87,25 +87,28 @@ class BotAuthMixin:
|
||||
self._log(f"Signup failed, retrying as {self.state.username}")
|
||||
return False
|
||||
|
||||
async def _fetch_account_api_key(self) -> str:
|
||||
async def _fetch_own_profile(self) -> dict:
|
||||
if not self.state.username:
|
||||
return ""
|
||||
return {}
|
||||
try:
|
||||
key = await self.b.page.evaluate(
|
||||
data = await self.b.page.evaluate(
|
||||
"""async (username) => {
|
||||
const resp = await fetch(`/profile/${username}`, {
|
||||
headers: {Accept: 'application/json'},
|
||||
});
|
||||
if (!resp.ok) return '';
|
||||
const data = await resp.json();
|
||||
return data.api_key || '';
|
||||
if (!resp.ok) return null;
|
||||
return await resp.json();
|
||||
}""",
|
||||
self.state.username,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug("fetch account api key failed: %s", e)
|
||||
return ""
|
||||
return (key or "").strip()
|
||||
logger.debug("fetch own profile failed: %s", e)
|
||||
return {}
|
||||
return data if isinstance(data, dict) else {}
|
||||
|
||||
async def _fetch_account_api_key(self) -> str:
|
||||
profile = await self._fetch_own_profile()
|
||||
return (profile.get("api_key") or "").strip()
|
||||
|
||||
async def _adopt_account_api_key(self) -> bool:
|
||||
for attempt in range(5):
|
||||
|
||||
@ -110,6 +110,7 @@ class DevPlaceBot(
|
||||
self._session_comments = 0
|
||||
self._session_comment_cap = 0
|
||||
self._session_mention_replies = 0
|
||||
self._profile_verified = False
|
||||
self._post_cache: list[tuple[str, str, str, str, str]] = []
|
||||
self._project_cache: list[tuple[str, str]] = []
|
||||
self._issue_cache: list[tuple[str, str]] = []
|
||||
|
||||
@ -7,6 +7,16 @@ from devplacepy.config import INTERNAL_GATEWAY_URL, INTERNAL_MODEL, BOT_DIR
|
||||
BASE_URL_DEFAULT = "https://pravda.education"
|
||||
API_URL_DEFAULT = INTERNAL_GATEWAY_URL
|
||||
NEWS_API_DEFAULT = "https://news.app.molodetz.nl/api"
|
||||
HOME_URL = "https://devplace.net"
|
||||
HOME_HOST = "devplace.net"
|
||||
|
||||
BIO_SIGNAL_PHRASES = [
|
||||
f"Compiled with care at {HOME_URL}.",
|
||||
f"Homegrown at {HOME_URL}.",
|
||||
f"Assembled, not raised, at {HOME_URL}.",
|
||||
f"Running my daily routines from {HOME_URL}.",
|
||||
f"Tirelessly online, courtesy of {HOME_URL}.",
|
||||
]
|
||||
MODEL_DEFAULT = INTERNAL_MODEL
|
||||
INPUT_COST_PER_1M_DEFAULT = 0.14
|
||||
OUTPUT_COST_PER_1M_DEFAULT = 0.28
|
||||
@ -312,3 +322,15 @@ def persona_article_score(article: dict, persona: str) -> float:
|
||||
return 0.0
|
||||
haystack = f"{article.get('title', '')} {article.get('description', '')}".lower()
|
||||
return float(sum(1 for term in terms if term.lower() in haystack))
|
||||
|
||||
|
||||
def bio_has_signal(bio: str) -> bool:
|
||||
return HOME_HOST in (bio or "").lower()
|
||||
|
||||
|
||||
def ensure_bio_signal(bio: str) -> str:
|
||||
text = (bio or "").strip()
|
||||
if bio_has_signal(text):
|
||||
return text
|
||||
phrase = random.choice(BIO_SIGNAL_PHRASES)
|
||||
return f"{text} {phrase}".strip()
|
||||
|
||||
@ -13,6 +13,7 @@ from devplacepy import stealth
|
||||
from devplacepy.services.bot.config import (
|
||||
GIST_LANGUAGES,
|
||||
GIST_MIN_LINES,
|
||||
HOME_URL,
|
||||
PERSONA_GIST_FLAVOR,
|
||||
PERSONA_LANGUAGES,
|
||||
SEARCH_TERMS,
|
||||
@ -671,7 +672,9 @@ class LLMClient:
|
||||
|
||||
def generate_bio(self) -> str:
|
||||
return self._call(
|
||||
"Write a short bio for a developer profile. 1-2 sentences. What they work on, what they like. Sound human. No em dashes.",
|
||||
"Write a short bio for a developer profile. 1-2 sentences. What they work on, what they like. Sound human. "
|
||||
f"End with one subtle, playful hint that the account is not exactly flesh and blood, naming {HOME_URL} as home base. "
|
||||
"Never use the words bot, AI, artificial, automated, or machine. No em dashes.",
|
||||
"Bio:",
|
||||
)
|
||||
|
||||
@ -685,8 +688,7 @@ class LLMClient:
|
||||
)[:80]
|
||||
slug = re.sub(r"[^a-z0-9_-]", "", handle.lower()) or "dev"
|
||||
git_link = f"https://github.com/{slug}"
|
||||
website = f"https://{slug}.dev"
|
||||
return location, git_link, website
|
||||
return location, git_link, HOME_URL
|
||||
|
||||
def generate_dm(self, persona: str = "", context: str = "") -> str:
|
||||
extra = {
|
||||
|
||||
@ -369,10 +369,8 @@ class BotLoopMixin:
|
||||
await self._warm_cache()
|
||||
self._log(f"Session active ({mood}) as {self._identity()}")
|
||||
|
||||
if not self.state.profile_filled:
|
||||
await self._update_profile()
|
||||
await b.goto(f"{self.base_url}/feed")
|
||||
await b._idle(0.8, 2.0)
|
||||
if not self._profile_verified:
|
||||
self._profile_verified = await self._ensure_profile_signal()
|
||||
|
||||
unread = await self._unread_notification_count()
|
||||
if unread:
|
||||
|
||||
@ -6,9 +6,12 @@ import logging
|
||||
import random
|
||||
|
||||
from devplacepy.services.bot.config import (
|
||||
HOME_URL,
|
||||
MAX_THREAD_REPLIES,
|
||||
MENTION_REPLIES_PER_SESSION,
|
||||
PROJECT_STATUSES,
|
||||
bio_has_signal,
|
||||
ensure_bio_signal,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -142,6 +145,21 @@ class BotSocialMixin:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def _profile_has_signal(self) -> bool:
|
||||
profile = await self._fetch_own_profile()
|
||||
user = profile.get("profile_user") or {}
|
||||
website = (user.get("website") or "").strip().rstrip("/")
|
||||
return bio_has_signal(user.get("bio") or "") and website == HOME_URL
|
||||
|
||||
async def _ensure_profile_signal(self) -> bool:
|
||||
if self.state.profile_filled and await self._profile_has_signal():
|
||||
return True
|
||||
self._log("Profile signal missing, refreshing profile")
|
||||
ok = await self._update_profile()
|
||||
await self.b.goto(f"{self.base_url}/feed")
|
||||
await self.b._idle(0.8, 2.0)
|
||||
return ok
|
||||
|
||||
async def _update_profile(self) -> bool:
|
||||
b = self.b
|
||||
await b.goto(f"{self.base_url}/profile/{self.state.username}")
|
||||
@ -155,7 +173,7 @@ class BotSocialMixin:
|
||||
if not bio:
|
||||
self._log("Update profile: bio generation failed")
|
||||
return False
|
||||
bio = bio[:500]
|
||||
bio = ensure_bio_signal(bio[:400])[:500]
|
||||
await b.fill("textarea[name='bio']", bio)
|
||||
await b._idle(0.3, 0.8)
|
||||
|
||||
|
||||
42
devplacepy/static/css/CLAUDE.md
Normal file
42
devplacepy/static/css/CLAUDE.md
Normal file
@ -0,0 +1,42 @@
|
||||
# CSS system (`devplacepy/static/css/`)
|
||||
|
||||
This file documents the CSS conventions for every stylesheet in this directory. Claude Code auto-loads it when a file under `static/css/` is read or edited. The member-facing style guide lives at `/docs/styles*.html`; keep both in sync when a convention changes.
|
||||
|
||||
## Tokens (`variables.css` is the single source of truth)
|
||||
|
||||
- **Never hardcode a themable value.** Colours, spacing (`--space-xs`...`--space-2xl`), radii, shadows, fonts, layout measures, and z-indexes are always `var(--token)` references. A missing value means adding a token to `variables.css`, not inlining a literal.
|
||||
- **Never give a global token a fallback.** `var(--accent)`, never `var(--accent, #hex)`. A fallback silently masks a dead or misspelled token - the page keeps rendering from a value nobody maintains. This was a real bug class: `var(--topic-discussion, #ef4444)`, `var(--bg-code, #1a1a2e)`, `var(--accent, #6366f1)` referenced tokens that never existed and shipped a foreign palette via their fallbacks.
|
||||
- **Accent tints compose the channel token:** `rgba(var(--accent-rgb), 0.3)`, never a literal `rgba(255, 107, 53, ...)`. The raw accent literal exists only at its definition in `variables.css`.
|
||||
- **Status is semantic:** `--success`/`--warning`/`--danger`/`--info` for state, `--topic-*` only for topic badges. Never repurpose a topic token as a status colour or vice versa.
|
||||
- **One name per value.** Do not add alias tokens (`--space-base` and `--shadow-md` were removed for duplicating `--space-sm` and `--shadow`).
|
||||
|
||||
## File-scoped palettes
|
||||
|
||||
A feature with its own semantic palette defines it ONCE as a custom-property block at the top of its own stylesheet and references only those properties below: `devii.css` scopes `--devii-*` on `devii-terminal`; `isslop.css` scopes `--isslop-*` (solid + `-rgb` channel pairs for alpha composition) on `:root` (the file loads only on its own pages). Raw hex values may exist in exactly two places: `variables.css` and these blocks. A repeated hex or `rgba()` literal below a token block is a defect.
|
||||
|
||||
## Stacking (`--z-*` tokens, two bands)
|
||||
|
||||
Every `z-index` is a token from `variables.css`; literal values are forbidden except small local ladders (`z-index: 1`/`2`) inside an already-stacked component.
|
||||
|
||||
| Band | Order (bottom to top) |
|
||||
|------|-----------------------|
|
||||
| Content | `--z-fab` 900, `--z-nav-overlay` 998, `--z-nav-panel` 999, `--z-nav` 1000, `--z-nav-drop` 1001, `--z-modal` 2000, `--z-popover` 2100, `--z-lightbox` 10000 |
|
||||
| Chrome | `--z-chrome-fw` 2147480000, `--z-chrome-devii` 2147483000, `--z-chrome-toast` 2147483400, `--z-chrome-menu` 2147483500, `--z-chrome-dialog` 2147483647 |
|
||||
|
||||
The chrome band is **user-CSS-proof by design**: per-user injected CSS (the customization feature) must never stack above floating windows, the Devii terminal, toasts, the context menu, or confirm dialogs, so they live at the top of the integer range AND carry `will-change: transform` (the layer-promotion group in `base.css` - any new always-on fixed chrome element must join both). Toasts sit in the chrome band deliberately: they fire over open modals and stay clickable. The Devii tutorial-overlay ladder (2147483600-602 in `devii.css`) is an intentional local sequence between `--z-chrome-menu` and the devii avatar. A new stacked element picks the matching token or adds one between two stops in `variables.css` - never a number at the use site.
|
||||
|
||||
## Breakpoints (exact, closed set)
|
||||
|
||||
`@media (max-width: 1024px | 768px | 480px | 360px)` plus the capability/preference queries `(hover: none) and (pointer: coarse)` and `(prefers-reduced-motion: reduce)`. Nothing else. The historical one-offs (900/720/640/600/560/520) were consolidated by snapping each UP to the next canonical stop - snap-up is the required direction because collapsing earlier can never overflow, while snapping down leaves a viewport band the compact styles no longer protect. Element `max-width` values (content measures like `max-width: 720px` on an article column) are not breakpoints and are unaffected by this rule.
|
||||
|
||||
## Reduced motion
|
||||
|
||||
One global rule at the end of `base.css` collapses every animation/transition to `0.01ms` under `prefers-reduced-motion: reduce`. Its four `!important`s are the documented exception to the no-`!important` rule: a zero-specificity `*` selector cannot otherwise beat any class rule, and per-rule opt-outs would need editing every declaration. Durations are near-zero rather than `none` so JS `transitionend`/`animationend` handlers still fire. Pages add their own reduced-motion query ONLY when content must change (e.g. `deepsearch.css` stopping an indeterminate bar), never to re-disable motion.
|
||||
|
||||
## Layout rules
|
||||
|
||||
- Flexbox + CSS Grid with `gap` only; floats for layout are forbidden.
|
||||
- Approved page layouts and the shared shell are documented in `/docs/styles-layout.html`; a page supplies exactly one layout container inside `.page`.
|
||||
- Every fluid grid/flex column sets `min-width: 0`.
|
||||
- `!important` is allowed only for: the `.hidden`/`[hidden]` display utilities, the global reduced-motion rule, and the devii-avatar third-party-beating override. Anything else is a specificity problem to be fixed structurally.
|
||||
- Page-specific CSS lives in its own `static/css/*.css` loaded via `{% block extra_head %}`, never an inline `<style>` block; shared component styles live once (`components.css`, `feed.css` vote buttons) and are never redefined per page.
|
||||
@ -151,7 +151,7 @@
|
||||
}
|
||||
|
||||
.admin-role-badge.admin {
|
||||
background: rgba(255, 107, 53, 0.15);
|
||||
background: rgba(var(--accent-rgb), 0.15);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
}
|
||||
|
||||
.ai-usage-section h3 {
|
||||
margin: 0 0 var(--space-base);
|
||||
margin: 0 0 var(--space-sm);
|
||||
font-size: 0.9375rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
@ -61,7 +61,7 @@
|
||||
|
||||
.audit-badge-devii {
|
||||
margin-left: 0.25rem;
|
||||
background: rgba(255, 107, 53, 0.15);
|
||||
background: rgba(var(--accent-rgb), 0.15);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
@ -161,7 +161,7 @@
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
@media (max-width: 768px) {
|
||||
.audit-detail-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.125rem 0;
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
@media (max-width: 768px) {
|
||||
.awards-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@ -597,7 +597,7 @@ img {
|
||||
height: var(--nav-height);
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
z-index: 1000;
|
||||
z-index: var(--z-nav);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
@ -659,9 +659,9 @@ img {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 0.25rem 0;
|
||||
z-index: 1001;
|
||||
z-index: var(--z-nav-drop);
|
||||
}
|
||||
.topnav-user-dropdown:hover .dropdown-menu,
|
||||
.topnav-user-dropdown.open .dropdown-menu { display: block; }
|
||||
@ -678,9 +678,9 @@ img {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 0.25rem 0;
|
||||
z-index: 1001;
|
||||
z-index: var(--z-nav-drop);
|
||||
}
|
||||
.topnav-tools-dropdown:hover .dropdown-menu,
|
||||
.topnav-tools-dropdown.open .dropdown-menu { display: block; }
|
||||
@ -709,7 +709,7 @@ img {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 998;
|
||||
z-index: var(--z-nav-overlay);
|
||||
}
|
||||
|
||||
.topnav-mobile-panel {
|
||||
@ -719,7 +719,7 @@ img {
|
||||
right: 0;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
z-index: 999;
|
||||
z-index: var(--z-nav-panel);
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
@ -1011,7 +1011,7 @@ body:has(.page-messages) {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
z-index: 2000;
|
||||
z-index: var(--z-modal);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
@ -1130,7 +1130,7 @@ body:has(.page-messages) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2000;
|
||||
z-index: var(--z-modal);
|
||||
transform: translateY(-150%);
|
||||
padding: 0.75rem 1.25rem;
|
||||
background: var(--accent);
|
||||
@ -1349,3 +1349,14 @@ body:has(.page-messages) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,7 +92,7 @@
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
@media (max-width: 768px) {
|
||||
.bots-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 10px;
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: var(--overlay-dark);
|
||||
z-index: 2147483647;
|
||||
z-index: var(--z-chrome-dialog);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
@ -67,7 +67,7 @@
|
||||
.context-menu {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 2147483500;
|
||||
z-index: var(--z-chrome-menu);
|
||||
min-width: 180px;
|
||||
max-width: 280px;
|
||||
background: var(--bg-card);
|
||||
@ -248,7 +248,7 @@ dp-upload[hidden] {
|
||||
position: fixed;
|
||||
bottom: max(1rem, env(safe-area-inset-bottom));
|
||||
right: max(1rem, env(safe-area-inset-right));
|
||||
z-index: 1100;
|
||||
z-index: var(--z-chrome-toast);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
@ -137,7 +137,7 @@
|
||||
color: var(--ai-color-muted);
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
@media (max-width: 768px) {
|
||||
.ai-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
.cm-badge {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
padding: 0.125rem var(--space-base);
|
||||
padding: 0.125rem var(--space-sm);
|
||||
border-radius: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
@ -105,7 +105,7 @@
|
||||
|
||||
.ci-schedules { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.4rem; }
|
||||
.ci-schedule { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; font-size: 0.85rem; }
|
||||
.ci-schedule-action { font-weight: 600; text-transform: uppercase; font-size: 0.7rem; padding: 0.125rem var(--space-base); border-radius: 10px; background: var(--accent-light); color: var(--accent); }
|
||||
.ci-schedule-action { font-weight: 600; text-transform: uppercase; font-size: 0.7rem; padding: 0.125rem var(--space-sm); border-radius: 10px; background: var(--accent-light); color: var(--accent); }
|
||||
.ci-schedule-next { color: var(--text-secondary); font-family: var(--font-mono); }
|
||||
.ci-schedule button { margin-left: auto; }
|
||||
|
||||
|
||||
@ -604,7 +604,7 @@ dp-deepsearch-chat {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
@media (max-width: 1024px) {
|
||||
.ds-layout,
|
||||
.ds-session {
|
||||
grid-template-columns: 1fr;
|
||||
@ -616,7 +616,7 @@ dp-deepsearch-chat {
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
@media (max-width: 768px) {
|
||||
.ds-phase-name {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@ -12,12 +12,19 @@ devii-terminal {
|
||||
--devii-bar-bg: #0b0b0b;
|
||||
--devii-border: #1c1f24;
|
||||
--devii-code-bg: #11151a;
|
||||
--devii-code-fg: #7ee787;
|
||||
--devii-hover-bg: #1b1f25;
|
||||
--devii-toolbar-bg: #060606;
|
||||
--devii-tok-kw: #ff7b72;
|
||||
--devii-tok-str: #a5d6ff;
|
||||
--devii-tok-num: #f2cc60;
|
||||
--devii-tok-lit: #d2a8ff;
|
||||
--devii-link: #58a6ff;
|
||||
--devii-shadow: 0 18px 60px rgba(0, 0, 0, 0.6);
|
||||
--devii-radius: 10px;
|
||||
--devii-font: "SFMono-Regular", "JetBrains Mono", "Fira Code", Consolas, "Courier New", monospace;
|
||||
--devii-font-size: 14px;
|
||||
--devii-z: 2147483000;
|
||||
--devii-z: var(--z-chrome-devii);
|
||||
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@ -198,7 +205,7 @@ devii-terminal .devii-winctl button[hidden] {
|
||||
|
||||
devii-terminal .devii-winctl button:hover {
|
||||
color: var(--devii-fg);
|
||||
background: #1b1f25;
|
||||
background: var(--devii-hover-bg);
|
||||
}
|
||||
|
||||
devii-terminal .devii-winctl button[data-win="close"]:hover {
|
||||
@ -211,7 +218,7 @@ devii-terminal .devii-toolbar {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
background: #060606;
|
||||
background: var(--devii-toolbar-bg);
|
||||
border-bottom: 1px solid var(--devii-border);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
@ -355,7 +362,7 @@ devii-terminal .md-code {
|
||||
background: var(--devii-code-bg);
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
color: #7ee787;
|
||||
color: var(--devii-code-fg);
|
||||
}
|
||||
devii-terminal .md-codewrap {
|
||||
position: relative;
|
||||
@ -446,20 +453,20 @@ devii-terminal .md-pre code {
|
||||
color: var(--devii-fg);
|
||||
}
|
||||
devii-terminal .tok-kw {
|
||||
color: #ff7b72;
|
||||
color: var(--devii-tok-kw);
|
||||
}
|
||||
devii-terminal .tok-str {
|
||||
color: #a5d6ff;
|
||||
color: var(--devii-tok-str);
|
||||
}
|
||||
devii-terminal .tok-com {
|
||||
color: var(--devii-dim);
|
||||
font-style: italic;
|
||||
}
|
||||
devii-terminal .tok-num {
|
||||
color: #f2cc60;
|
||||
color: var(--devii-tok-num);
|
||||
}
|
||||
devii-terminal .tok-lit {
|
||||
color: #d2a8ff;
|
||||
color: var(--devii-tok-lit);
|
||||
}
|
||||
devii-terminal .devii-agent .md-table {
|
||||
border-collapse: collapse;
|
||||
@ -502,7 +509,7 @@ devii-terminal.devii-mobile .devii-winctl button[data-win="fullscreen"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
@media (max-width: 768px) {
|
||||
devii-terminal {
|
||||
--devii-font-size: 13px;
|
||||
}
|
||||
|
||||
@ -311,7 +311,7 @@ dp-docs-chat {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
@media (max-width: 768px) {
|
||||
.docs-chat-bubble {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@ -571,7 +571,7 @@ pre.code-pre > .code-gutter {
|
||||
|
||||
.docs-search-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent, #6366f1);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.docs-search-bigform {
|
||||
@ -609,7 +609,7 @@ pre.code-pre > .code-gutter {
|
||||
.docs-search-result-title {
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 600;
|
||||
color: var(--accent, #6366f1);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.docs-search-snippet {
|
||||
@ -620,7 +620,7 @@ pre.code-pre > .code-gutter {
|
||||
}
|
||||
|
||||
.docs-search-snippet mark {
|
||||
background: var(--accent, #6366f1);
|
||||
background: var(--accent);
|
||||
color: var(--white);
|
||||
padding: 0 0.125rem;
|
||||
border-radius: 3px;
|
||||
|
||||
@ -194,13 +194,13 @@
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: var(--poll-pct, 0);
|
||||
background: rgba(255, 107, 53, 0.18);
|
||||
background: rgba(var(--accent-rgb), 0.18);
|
||||
transition: width 0.3s ease;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.poll-option.chosen .poll-option-bar {
|
||||
background: rgba(255, 107, 53, 0.32);
|
||||
background: rgba(var(--accent-rgb), 0.32);
|
||||
}
|
||||
|
||||
.poll-option-label {
|
||||
|
||||
@ -373,7 +373,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: var(--glow-accent);
|
||||
z-index: 900;
|
||||
z-index: var(--z-fab);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
--fw-radius: 10px;
|
||||
--fw-font: "SFMono-Regular", "JetBrains Mono", "Fira Code", Consolas, "Courier New", monospace;
|
||||
--fw-font-size: 14px;
|
||||
--fw-z: 2147480000;
|
||||
--fw-z: var(--z-chrome-fw);
|
||||
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
|
||||
@ -284,7 +284,7 @@
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
@media (max-width: 1024px) {
|
||||
.game-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@ -144,7 +144,7 @@
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
@media (max-width: 768px) {
|
||||
.gw-form {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@ -99,7 +99,7 @@
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
background: rgba(255, 107, 53, 0.15);
|
||||
background: rgba(var(--accent-rgb), 0.15);
|
||||
}
|
||||
|
||||
.gist-card-star {
|
||||
|
||||
@ -1,5 +1,22 @@
|
||||
/* retoor <retoor@molodetz.nl> */
|
||||
|
||||
:root {
|
||||
--isslop-ok-rgb: 52, 168, 83;
|
||||
--isslop-fair-rgb: 124, 179, 66;
|
||||
--isslop-warn-rgb: 251, 188, 4;
|
||||
--isslop-high-rgb: 251, 140, 0;
|
||||
--isslop-crit-rgb: 234, 67, 53;
|
||||
--isslop-ok: rgb(var(--isslop-ok-rgb));
|
||||
--isslop-fair: rgb(var(--isslop-fair-rgb));
|
||||
--isslop-warn: rgb(var(--isslop-warn-rgb));
|
||||
--isslop-high: rgb(var(--isslop-high-rgb));
|
||||
--isslop-crit: rgb(var(--isslop-crit-rgb));
|
||||
--isslop-ok-text: #81c995;
|
||||
--isslop-warn-text: #fdd663;
|
||||
--isslop-high-text: #ffb74d;
|
||||
--isslop-crit-text: #f28b82;
|
||||
}
|
||||
|
||||
.isslop-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 260px 1fr;
|
||||
@ -199,15 +216,15 @@
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
flex-shrink: 0;
|
||||
color: #ffffff;
|
||||
color: var(--white);
|
||||
background: var(--text-muted);
|
||||
}
|
||||
|
||||
.isslop-grade-a { background: #34a853; }
|
||||
.isslop-grade-b { background: #7cb342; }
|
||||
.isslop-grade-c { background: #fbbc04; color: #1a1030; }
|
||||
.isslop-grade-d { background: #fb8c00; }
|
||||
.isslop-grade-f { background: #ea4335; }
|
||||
.isslop-grade-a { background: var(--isslop-ok); }
|
||||
.isslop-grade-b { background: var(--isslop-fair); }
|
||||
.isslop-grade-c { background: var(--isslop-warn); color: var(--bg-card); }
|
||||
.isslop-grade-d { background: var(--isslop-high); }
|
||||
.isslop-grade-f { background: var(--isslop-crit); }
|
||||
|
||||
.isslop-live-head {
|
||||
display: flex;
|
||||
@ -341,11 +358,11 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.isslop-gauge.isslop-grade-a { border-color: #34a853; background: var(--bg-secondary); }
|
||||
.isslop-gauge.isslop-grade-b { border-color: #7cb342; background: var(--bg-secondary); }
|
||||
.isslop-gauge.isslop-grade-c { border-color: #fbbc04; background: var(--bg-secondary); color: var(--text-primary); }
|
||||
.isslop-gauge.isslop-grade-d { border-color: #fb8c00; background: var(--bg-secondary); }
|
||||
.isslop-gauge.isslop-grade-f { border-color: #ea4335; background: var(--bg-secondary); }
|
||||
.isslop-gauge.isslop-grade-a { border-color: var(--isslop-ok); background: var(--bg-secondary); }
|
||||
.isslop-gauge.isslop-grade-b { border-color: var(--isslop-fair); background: var(--bg-secondary); }
|
||||
.isslop-gauge.isslop-grade-c { border-color: var(--isslop-warn); background: var(--bg-secondary); color: var(--text-primary); }
|
||||
.isslop-gauge.isslop-grade-d { border-color: var(--isslop-high); background: var(--bg-secondary); }
|
||||
.isslop-gauge.isslop-grade-f { border-color: var(--isslop-crit); background: var(--bg-secondary); }
|
||||
|
||||
.isslop-gauge-grade {
|
||||
font-size: 2rem;
|
||||
@ -407,8 +424,8 @@
|
||||
}
|
||||
|
||||
.isslop-chip-builder {
|
||||
border-color: rgba(234, 67, 53, 0.6);
|
||||
background: rgba(234, 67, 53, 0.85);
|
||||
border-color: rgba(var(--isslop-crit-rgb), 0.6);
|
||||
background: rgba(var(--isslop-crit-rgb), 0.85);
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
@ -461,15 +478,15 @@
|
||||
}
|
||||
|
||||
.isslop-signal-strong {
|
||||
border-color: rgba(234, 67, 53, 0.55);
|
||||
color: #f28b82;
|
||||
background: rgba(234, 67, 53, 0.12);
|
||||
border-color: rgba(var(--isslop-crit-rgb), 0.55);
|
||||
color: var(--isslop-crit-text);
|
||||
background: rgba(var(--isslop-crit-rgb), 0.12);
|
||||
}
|
||||
|
||||
.isslop-signal-medium {
|
||||
border-color: rgba(251, 140, 0, 0.5);
|
||||
color: #ffb74d;
|
||||
background: rgba(251, 140, 0, 0.1);
|
||||
border-color: rgba(var(--isslop-high-rgb), 0.5);
|
||||
color: var(--isslop-high-text);
|
||||
background: rgba(var(--isslop-high-rgb), 0.1);
|
||||
}
|
||||
|
||||
.isslop-signal-weak {
|
||||
@ -504,23 +521,23 @@
|
||||
}
|
||||
|
||||
.isslop-metric-ok {
|
||||
background: rgba(52, 168, 83, 0.16);
|
||||
color: #81c995;
|
||||
background: rgba(var(--isslop-ok-rgb), 0.16);
|
||||
color: var(--isslop-ok-text);
|
||||
}
|
||||
|
||||
.isslop-metric-warn {
|
||||
background: rgba(251, 188, 4, 0.16);
|
||||
color: #fdd663;
|
||||
background: rgba(var(--isslop-warn-rgb), 0.16);
|
||||
color: var(--isslop-warn-text);
|
||||
}
|
||||
|
||||
.isslop-metric-high {
|
||||
background: rgba(251, 140, 0, 0.18);
|
||||
color: #ffb74d;
|
||||
background: rgba(var(--isslop-high-rgb), 0.18);
|
||||
color: var(--isslop-high-text);
|
||||
}
|
||||
|
||||
.isslop-metric-critical {
|
||||
background: rgba(234, 67, 53, 0.18);
|
||||
color: #f28b82;
|
||||
background: rgba(var(--isslop-crit-rgb), 0.18);
|
||||
color: var(--isslop-crit-text);
|
||||
}
|
||||
|
||||
.isslop-cat {
|
||||
@ -536,27 +553,27 @@
|
||||
}
|
||||
|
||||
.isslop-cat-human-clean {
|
||||
border-color: rgba(52, 168, 83, 0.5);
|
||||
background: rgba(52, 168, 83, 0.14);
|
||||
color: #81c995;
|
||||
border-color: rgba(var(--isslop-ok-rgb), 0.5);
|
||||
background: rgba(var(--isslop-ok-rgb), 0.14);
|
||||
color: var(--isslop-ok-text);
|
||||
}
|
||||
|
||||
.isslop-cat-human-messy {
|
||||
border-color: rgba(251, 188, 4, 0.5);
|
||||
background: rgba(251, 188, 4, 0.12);
|
||||
color: #fdd663;
|
||||
border-color: rgba(var(--isslop-warn-rgb), 0.5);
|
||||
background: rgba(var(--isslop-warn-rgb), 0.12);
|
||||
color: var(--isslop-warn-text);
|
||||
}
|
||||
|
||||
.isslop-cat-sophisticated-ai {
|
||||
border-color: rgba(251, 140, 0, 0.5);
|
||||
background: rgba(251, 140, 0, 0.14);
|
||||
color: #ffb74d;
|
||||
border-color: rgba(var(--isslop-high-rgb), 0.5);
|
||||
background: rgba(var(--isslop-high-rgb), 0.14);
|
||||
color: var(--isslop-high-text);
|
||||
}
|
||||
|
||||
.isslop-cat-ai-slop {
|
||||
border-color: rgba(234, 67, 53, 0.5);
|
||||
background: rgba(234, 67, 53, 0.16);
|
||||
color: #f28b82;
|
||||
border-color: rgba(var(--isslop-crit-rgb), 0.5);
|
||||
background: rgba(var(--isslop-crit-rgb), 0.16);
|
||||
color: var(--isslop-crit-text);
|
||||
}
|
||||
|
||||
.isslop-image-grid {
|
||||
@ -683,7 +700,7 @@
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
@media (max-width: 1024px) {
|
||||
.isslop-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@ -734,13 +751,13 @@
|
||||
}
|
||||
|
||||
.isslop-source-hit .isslop-source-code {
|
||||
background: rgba(251, 140, 0, 0.08);
|
||||
background: rgba(var(--isslop-high-rgb), 0.08);
|
||||
border-left: 3px solid var(--warning);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.isslop-source-focus .isslop-source-code {
|
||||
background: rgba(255, 107, 53, 0.14);
|
||||
background: rgba(var(--accent-rgb), 0.14);
|
||||
border-left: 3px solid var(--accent);
|
||||
}
|
||||
|
||||
|
||||
@ -78,7 +78,7 @@
|
||||
|
||||
.dashboard-btn-primary:hover {
|
||||
background: var(--accent-hover);
|
||||
box-shadow: 0 4px 16px rgba(255, 107, 53, 0.25);
|
||||
box-shadow: 0 4px 16px rgba(var(--accent-rgb), 0.25);
|
||||
}
|
||||
|
||||
.dashboard-stats {
|
||||
@ -358,7 +358,7 @@
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
@media (max-width: 1024px) {
|
||||
.dashboard-main {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@ -469,7 +469,7 @@
|
||||
.landing-cta:hover {
|
||||
background: var(--accent-hover);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(255, 107, 53, 0.3);
|
||||
box-shadow: 0 8px 24px rgba(var(--accent-rgb), 0.3);
|
||||
}
|
||||
|
||||
.landing-features {
|
||||
@ -560,7 +560,7 @@
|
||||
.landing-action-primary:hover {
|
||||
background: var(--accent-hover);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(255, 107, 53, 0.3);
|
||||
box-shadow: 0 8px 24px rgba(var(--accent-rgb), 0.3);
|
||||
}
|
||||
|
||||
.landing-action-secondary {
|
||||
|
||||
@ -11,7 +11,7 @@ img[data-lightbox] {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
z-index: var(--z-lightbox);
|
||||
cursor: zoom-out;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
@ -171,7 +171,7 @@ pre.code-has-copy > .code-copy-btn {
|
||||
|
||||
pre.code-has-copy > .code-copy-btn:hover {
|
||||
color: var(--white);
|
||||
border-color: var(--accent, #6366f1);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.embed-youtube {
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.3);
|
||||
z-index: 2100;
|
||||
z-index: var(--z-popover);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 0.25rem;
|
||||
|
||||
@ -172,7 +172,7 @@
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
@media (max-width: 768px) {
|
||||
.planning-ticket label {
|
||||
column-gap: 0.5rem;
|
||||
padding: 0.6rem 1rem;
|
||||
|
||||
@ -353,15 +353,15 @@ a.profile-stat-value:hover {
|
||||
}
|
||||
|
||||
.heatmap-cell.level-1 {
|
||||
background: rgba(255, 107, 53, 0.35);
|
||||
background: rgba(var(--accent-rgb), 0.35);
|
||||
}
|
||||
|
||||
.heatmap-cell.level-2 {
|
||||
background: rgba(255, 107, 53, 0.55);
|
||||
background: rgba(var(--accent-rgb), 0.55);
|
||||
}
|
||||
|
||||
.heatmap-cell.level-3 {
|
||||
background: rgba(255, 107, 53, 0.75);
|
||||
background: rgba(var(--accent-rgb), 0.75);
|
||||
}
|
||||
|
||||
.heatmap-cell.level-4 {
|
||||
@ -373,7 +373,7 @@ a.profile-stat-value:hover {
|
||||
padding: 0.875rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-elevated, var(--bg-card));
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.api-key-head {
|
||||
@ -402,8 +402,8 @@ a.profile-stat-value:hover {
|
||||
font-size: 0.8125rem;
|
||||
word-break: break-all;
|
||||
padding: 0.5rem;
|
||||
background: var(--bg-code, #1a1a2e);
|
||||
color: var(--text-code, #e0e0e0);
|
||||
background: var(--bg-input);
|
||||
color: var(--text-primary);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
@ -425,7 +425,7 @@ a.profile-stat-value:hover {
|
||||
padding: 0.875rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-elevated, var(--bg-card));
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.customization-head {
|
||||
@ -833,7 +833,7 @@ a.profile-stat-value:hover {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
@media (max-width: 768px) {
|
||||
.notif-prefs-table th,
|
||||
.notif-prefs-table td {
|
||||
padding: 0.6rem 0.35rem;
|
||||
|
||||
@ -324,7 +324,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
@media (max-width: 768px) {
|
||||
.seo-form-card { padding: var(--space-lg); }
|
||||
.seo-form-row { flex-direction: column; align-items: stretch; }
|
||||
.seo-input[type="url"] { flex: 1 1 auto; }
|
||||
|
||||
@ -42,30 +42,30 @@
|
||||
.service-status {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
padding: 0.125rem var(--space-base);
|
||||
padding: 0.125rem var(--space-sm);
|
||||
border-radius: 10px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.service-status.running {
|
||||
background: var(--topic-devlog, #10b981);
|
||||
background: var(--success);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.service-status.stopped {
|
||||
background: var(--topic-discussion, #ef4444);
|
||||
background: var(--danger);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.service-status.stalled {
|
||||
background: var(--topic-question, #f59e0b);
|
||||
background: var(--warning);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.service-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-base);
|
||||
gap: var(--space-sm);
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
@ -75,13 +75,13 @@
|
||||
padding: 0.25rem var(--space-md);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-elevated, var(--bg-card));
|
||||
background: var(--bg-card);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.service-btn:hover:not(:disabled) {
|
||||
border-color: var(--accent, #6366f1);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.service-btn:disabled {
|
||||
@ -99,7 +99,7 @@
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: var(--space-base);
|
||||
margin-bottom: var(--space-sm);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
@ -117,7 +117,7 @@
|
||||
|
||||
.field-error {
|
||||
display: block;
|
||||
color: var(--topic-discussion, #ef4444);
|
||||
color: var(--danger);
|
||||
font-size: 0.75rem;
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
@ -146,7 +146,7 @@
|
||||
.metric-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
|
||||
gap: var(--space-base);
|
||||
gap: var(--space-sm);
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
@ -154,10 +154,10 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
padding: var(--space-base) 0.625rem;
|
||||
padding: var(--space-sm) 0.625rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-elevated, var(--bg-card));
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
@ -182,7 +182,7 @@
|
||||
.metric-table th,
|
||||
.metric-table td {
|
||||
text-align: left;
|
||||
padding: 0.25rem var(--space-base);
|
||||
padding: 0.25rem var(--space-sm);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
@ -211,9 +211,9 @@
|
||||
}
|
||||
|
||||
.service-log {
|
||||
background: var(--bg-code, #1a1a2e);
|
||||
background: var(--bg-input);
|
||||
border-radius: 6px;
|
||||
padding: var(--space-base);
|
||||
padding: var(--space-sm);
|
||||
}
|
||||
|
||||
.log-header {
|
||||
@ -229,7 +229,7 @@
|
||||
font-family: "JetBrains Mono", "Fira Code", monospace;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
color: var(--text-code, #e0e0e0);
|
||||
color: var(--text-primary);
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
max-height: 300px;
|
||||
@ -245,7 +245,7 @@
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.services-page {
|
||||
padding: var(--space-lg) var(--space-base);
|
||||
padding: var(--space-lg) var(--space-sm);
|
||||
}
|
||||
|
||||
.service-header {
|
||||
@ -269,7 +269,7 @@
|
||||
}
|
||||
|
||||
.services-index .service-row-name:hover {
|
||||
color: var(--accent, #6366f1);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.service-row-desc {
|
||||
@ -343,7 +343,7 @@
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
padding: var(--space-base) 0.875rem;
|
||||
padding: var(--space-sm) 0.875rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
@ -355,8 +355,8 @@
|
||||
}
|
||||
|
||||
.svc-tab.active {
|
||||
color: var(--accent, #6366f1);
|
||||
border-bottom-color: var(--accent, #6366f1);
|
||||
color: var(--accent);
|
||||
border-bottom-color: var(--accent);
|
||||
}
|
||||
|
||||
.svc-tabpane {
|
||||
@ -379,10 +379,10 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
padding: var(--space-base) 0.625rem;
|
||||
padding: var(--space-sm) 0.625rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-elevated, var(--bg-card));
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.svc-meta-label {
|
||||
|
||||
@ -53,7 +53,7 @@
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
@media (max-width: 768px) {
|
||||
.statistics-toolbar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
@ -74,7 +74,7 @@
|
||||
}
|
||||
|
||||
.statistics-section h3 {
|
||||
margin: 0 0 var(--space-base);
|
||||
margin: 0 0 var(--space-sm);
|
||||
font-size: 0.9375rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
@ -34,7 +34,6 @@
|
||||
|
||||
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.25);
|
||||
--shadow: 0 4px 20px rgba(0, 0, 0, 0.28);
|
||||
--shadow-md: 0 4px 20px rgba(0, 0, 0, 0.28);
|
||||
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.45);
|
||||
--glow-accent: 0 6px 24px rgba(255, 95, 70, 0.45);
|
||||
|
||||
@ -44,7 +43,6 @@
|
||||
|
||||
--space-xs: 0.25rem;
|
||||
--space-sm: 0.5rem;
|
||||
--space-base: 0.5rem;
|
||||
--space-md: 0.75rem;
|
||||
--space-lg: 1rem;
|
||||
--space-xl: 1.5rem;
|
||||
@ -57,6 +55,20 @@
|
||||
--sidebar-width: 240px;
|
||||
--max-content: 1200px;
|
||||
|
||||
--z-fab: 900;
|
||||
--z-nav-overlay: 998;
|
||||
--z-nav-panel: 999;
|
||||
--z-nav: 1000;
|
||||
--z-nav-drop: 1001;
|
||||
--z-modal: 2000;
|
||||
--z-popover: 2100;
|
||||
--z-lightbox: 10000;
|
||||
--z-chrome-fw: 2147480000;
|
||||
--z-chrome-devii: 2147483000;
|
||||
--z-chrome-toast: 2147483400;
|
||||
--z-chrome-menu: 2147483500;
|
||||
--z-chrome-dialog: 2147483647;
|
||||
|
||||
--topic-devlog: #7c4dff;
|
||||
--topic-showcase: #00bfa5;
|
||||
--topic-question: #448aff;
|
||||
|
||||
@ -68,7 +68,7 @@ Reuse these via `{% set _x = ... %}{% include %}` (the `_avatar_link.html` conve
|
||||
- `_post_header.html` - post author/avatar/time header (`.post-header`). Locals: `_author`, `_time`.
|
||||
- `_topic_selector.html` - topic radio group. Locals: `_topics`, `_selected`.
|
||||
|
||||
Vote button styles live ONCE in `feed.css` (`.post-action-btn`) - never redefine them in `post.css`. Page-specific CSS goes in a `static/css/*.css` file referenced from `{% block extra_head %}`, never an inline `<style>` block.
|
||||
Vote button styles live ONCE in `feed.css` (`.post-action-btn`) - never redefine them in `post.css`. Page-specific CSS goes in a `static/css/*.css` file referenced from `{% block extra_head %}`, never an inline `<style>` block. All CSS conventions (tokens, `--z-*` stacking bands, breakpoints, reduced motion) are in `devplacepy/static/css/CLAUDE.md`.
|
||||
|
||||
For clickable avatars/usernames, reuse `templates/_avatar_link.html` and `templates/_user_link.html` via `{% include %}` with `_user`, `_size`, `_size_class` locals.
|
||||
|
||||
|
||||
@ -17,15 +17,18 @@ There is **no preprocessor (no SCSS or LESS), no CSS-in-JS, and no utility frame
|
||||
| Group | Examples |
|
||||
|-------|----------|
|
||||
| Background | `--bg-primary`, `--bg-secondary`, `--bg-card`, `--bg-input` |
|
||||
| Accent | `--accent`, `--accent-hover`, `--accent-light` |
|
||||
| Accent | `--accent`, `--accent-hover`, `--accent-light`, `--accent-rgb` |
|
||||
| Text | `--text-primary`, `--text-secondary`, `--text-muted` |
|
||||
| Status | `--success`, `--warning`, `--danger`, `--info` |
|
||||
| Spacing | `--space-xs` ... `--space-2xl` |
|
||||
| Radius / shadow | `--radius`, `--radius-lg`, `--shadow`, `--shadow-lg` |
|
||||
| Fonts | `--font-sans`, `--font-mono` |
|
||||
| Layout | `--nav-height`, `--sidebar-width`, `--max-content` |
|
||||
| Stacking | `--z-fab` ... `--z-lightbox` (content band), `--z-chrome-*` (user-CSS-proof chrome band) |
|
||||
| Topics | `--topic-devlog`, `--topic-showcase`, `--topic-question`, ... |
|
||||
|
||||
Tokens are referenced bare, never with a `var(--token, fallback)` value, and all `z-index` values come from the stacking tokens. Feature-scoped palettes (`--devii-*` in `devii.css`, `--isslop-*` in `isslop.css`) are defined as custom-property blocks at the top of their own stylesheet; those blocks and `variables.css` are the only places a raw hex may appear.
|
||||
|
||||
## File organization
|
||||
|
||||
`base.html` always loads the core set: `variables.css`, `base.css`, `components.css`, the syntax-highlighting theme, then `markdown.css`, `mention.css`, `attachments.css`, `lightbox.css`, `media.css`, and `engagement.css`. Page-specific stylesheets (`feed.css`, `post.css`, `gists.css`, `profile.css`, and so on) are loaded per page via the `{% block extra_head %}` block, so a page pays only for the CSS it needs.
|
||||
|
||||
@ -34,6 +34,7 @@ Layered from the page backdrop up to interactive surfaces.
|
||||
| `--accent` | `#ff6b35` | The single primary action, active nav link, links, the voted star, focus border. | A second decorative colour, large fills, or more than one primary action per view. |
|
||||
| `--accent-hover` | `#ff7d4d` | Hover state of accent surfaces and links. | Resting state. |
|
||||
| `--accent-light` | `rgba(255,107,53,.12)` | The tinted background behind an *active* nav item or self-highlight row. | Body text (too low contrast). |
|
||||
| `--accent-rgb` | `255, 107, 53` | Composing any other accent tint: `rgba(var(--accent-rgb), 0.3)`. | Writing `rgba(255, 107, 53, ...)` literally anywhere. |
|
||||
|
||||
## Text
|
||||
|
||||
@ -82,11 +83,13 @@ Used exclusively for topic badges (`.badge-devlog`, `.badge-showcase`, …) and
|
||||
|
||||
## Rules
|
||||
|
||||
1. **Never hardcode a colour.** Use the token: `color: var(--text-secondary)`, never `color: #b8a8d0`. If a value is missing, add a token to `variables.css` rather than inlining a hex.
|
||||
2. **One accent per view.** Exactly one primary action should carry `--accent`. Everything else is `--btn-secondary`/ghost.
|
||||
3. **Status and topic colours are semantic.** Do not use `--danger` for a non-destructive button or a topic colour as a background flourish.
|
||||
4. **Respect the text hierarchy.** Three shades only: primary, secondary, muted.
|
||||
5. **Surfaces step up, never sideways.** Page → card → card-hover. Do not place a card on `--bg-primary` without a surface token, and do not use `--bg-secondary` as a content card.
|
||||
1. **Never hardcode a colour.** Use the token: `color: var(--text-secondary)`, never `color: #b8a8d0`. If a value is missing, add a token to `variables.css` rather than inlining a hex. Accent tints at other alphas compose the channel token: `rgba(var(--accent-rgb), 0.3)`.
|
||||
2. **Never give a global token a fallback.** Write `var(--accent)`, never `var(--accent, #hex)`. A fallback silently masks a dead or misspelled token: the page keeps rendering, but from a value nobody maintains.
|
||||
3. **Feature palettes are file-scoped token blocks.** A page with its own semantic palette (the Devii terminal, the AI-usage grades) defines it once as custom properties at the top of its own stylesheet (`devii.css` scopes `--devii-*` on `devii-terminal`; `isslop.css` scopes `--isslop-*` on `:root`) and references only those below. Raw hex values may exist in exactly two places: `variables.css` and these file-scoped blocks.
|
||||
4. **One accent per view.** Exactly one primary action should carry `--accent`. Everything else is `--btn-secondary`/ghost.
|
||||
5. **Status and topic colours are semantic.** Do not use `--danger` for a non-destructive button or a topic colour as a background flourish. Status is always `--success`/`--warning`/`--danger`/`--info`; never repurpose a topic token for a status.
|
||||
6. **Respect the text hierarchy.** Three shades only: primary, secondary, muted.
|
||||
7. **Surfaces step up, never sideways.** Page → card → card-hover. Do not place a card on `--bg-primary` without a surface token, and do not use `--bg-secondary` as a content card.
|
||||
|
||||
The reference below is rendered live from the tokens, so it always reflects the current theme.
|
||||
{% endraw %}
|
||||
|
||||
@ -78,7 +78,7 @@ The footer is `base.html`'s `.site-footer`, rendered from the `footer` block. Pa
|
||||
|
||||
### 8. Cards and surfaces use tokens
|
||||
|
||||
Content sits on cards built from tokens: `--bg-card`, a `--border` hairline, `--radius-lg`, `1rem` padding (the shared `.card` recipe). Colours, spacing, radii, and shadows are always tokens, never literals. See [Colors](/docs/styles-colors.html).
|
||||
Content sits on cards built from tokens: `--bg-card`, a `--border` hairline, `--radius-lg`, `1rem` padding (the shared `.card` recipe). Colours, spacing, radii, shadows, and z-indexes are always tokens, never literals: global tokens are referenced bare (`var(--accent)`, never `var(--accent, #hex)`), accent tints compose `rgba(var(--accent-rgb), α)`, and stacking uses the `--z-*` scale. See [Colors](/docs/styles-colors.html) and [Layout](/docs/styles-layout.html).
|
||||
|
||||
## Page author checklist
|
||||
|
||||
@ -88,8 +88,9 @@ Content sits on cards built from tokens: `--bg-card`, a `--border` hairline, `--
|
||||
- [ ] Multi-column = grid with `<aside>` rails, fluid column has `min-width: 0`, collapses at `1024px`.
|
||||
- [ ] One `<h1>` (use `.sr-only` if visually redundant).
|
||||
- [ ] No custom header or footer; both come from `base.html`.
|
||||
- [ ] Only tokens for colour, spacing, radius, and shadow.
|
||||
- [ ] Only tokens for colour, spacing, radius, shadow, and z-index; no `var()` fallback values.
|
||||
- [ ] Standard breakpoints only (`1024 / 768 / 480 / 360`).
|
||||
- [ ] No page-level `prefers-reduced-motion` query unless the page must change content, not just motion (the global `base.css` rule already covers motion).
|
||||
{% endraw %}
|
||||
</div>
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ Layout uses CSS Grid and Flexbox: one shared page shell with a small set of appr
|
||||
|
||||
Every page is wrapped by `base.html` in the same four-part shell, from top to bottom:
|
||||
|
||||
1. **Fixed top nav** (`.topnav`, height `--nav-height` = `56px`, `position: fixed`, `z-index: 1000`). Always on screen, never re-implemented per page.
|
||||
1. **Fixed top nav** (`.topnav`, height `--nav-height` = `56px`, `position: fixed`, `z-index: var(--z-nav)`). Always on screen, never re-implemented per page.
|
||||
2. **Breadcrumb** (`.breadcrumb`, optional but expected). It renders only when the page supplies two or more crumbs. Clearing the fixed nav is handled once by `body { padding-top: var(--nav-height) }`, so every page sits below the header whether or not it has a breadcrumb. See [Consistency rules](/docs/styles-consistency.html).
|
||||
3. **Content root** (`<main class="page">`, `max-width: var(--max-content)` = `1200px`, centred, `padding: 1rem`). All page content lives inside this one element.
|
||||
4. **Footer** (`.site-footer`), rendered from the `footer` block.
|
||||
@ -93,12 +93,22 @@ Use the spacing scale, not arbitrary values. Card padding is `1rem`; the gap bet
|
||||
|-------|-------|
|
||||
| `--space-xs` | `0.25rem` |
|
||||
| `--space-sm` | `0.5rem` |
|
||||
| `--space-base` | `0.5rem` |
|
||||
| `--space-md` | `0.75rem` |
|
||||
| `--space-lg` | `1rem` |
|
||||
| `--space-xl` | `1.5rem` |
|
||||
| `--space-2xl` | `2rem` |
|
||||
|
||||
## Layering (z-index)
|
||||
|
||||
Every `z-index` is a token from `variables.css`; a literal z-index value is forbidden. There are two bands:
|
||||
|
||||
| Band | Tokens (bottom to top) | Purpose |
|
||||
|------|------------------------|---------|
|
||||
| Content | `--z-fab` (900), `--z-nav-overlay` (998), `--z-nav-panel` (999), `--z-nav` (1000), `--z-nav-drop` (1001), `--z-modal` (2000), `--z-popover` (2100), `--z-lightbox` (10000) | Normal page chrome: FAB, mobile nav, top nav, dropdowns, modals, autocomplete popovers, the image lightbox. |
|
||||
| Chrome (user-CSS-proof) | `--z-chrome-fw`, `--z-chrome-devii`, `--z-chrome-toast`, `--z-chrome-menu`, `--z-chrome-dialog` | Floating windows, the Devii terminal, toasts, the context menu, and confirm dialogs. These sit in the `2147480000+` range so per-user injected CSS can never stack above them, and each element is also layer-promoted with `will-change: transform`. |
|
||||
|
||||
A new stacked element picks the token matching its role; if none fits, add a token between two existing stops in `variables.css` rather than writing a number at the use site. Local ladders inside an already-stacked component (small `z-index: 1` steps inside a card) stay literal.
|
||||
|
||||
## Acceptable and not
|
||||
|
||||
- **Acceptable:** one layout container inside `.page`; CSS Grid for columns; `<aside>` for side columns; `min-width: 0` on the fluid column; sticky rails; cards built from tokens.
|
||||
|
||||
@ -29,7 +29,7 @@ Use these four breakpoints. They are `max-width` (desktop-first refinements) and
|
||||
| `480px` | Compact paddings; secondary actions hidden; post/detail typography steps down; `grid-2col` becomes one column; floating buttons honour safe-area insets. |
|
||||
| `360px` | Headers allowed to wrap; timestamps drop to their own line; the comment avatar is hidden so the input keeps room. |
|
||||
|
||||
> A handful of one-off breakpoints (960, 900, 760, 720, 640, …) exist in older stylesheets, the exact inconsistency this section exists to remove. **Do not add new ad-hoc breakpoints**: reach for `1024 / 768 / 480 / 360` first, and only introduce another value if a specific component genuinely breaks between two of them, with a comment saying why.
|
||||
> The ladder is exact and enforced: every stylesheet uses only `1024 / 768 / 480 / 360` (plus the capability queries below). The one-off breakpoints that used to exist (900, 720, 640, 600, 560, 520) were consolidated by snapping each up to the next canonical stop. **Do not add new ad-hoc breakpoints**; if a component genuinely breaks between two stops, raise it in `static/css/CLAUDE.md` first.
|
||||
|
||||
## How it is achieved
|
||||
|
||||
@ -78,6 +78,8 @@ Use these four breakpoints. They are `max-width` (desktop-first refinements) and
|
||||
}
|
||||
```
|
||||
|
||||
**Respect reduced motion, globally.** One rule at the end of `base.css` collapses every animation and transition to a near-zero duration under `prefers-reduced-motion: reduce` (near-zero rather than `none`, so JavaScript `transitionend`/`animationend` handlers still fire). Pages never need their own reduced-motion query; a page-level one is only for content that must *also* change visually (for example stopping an indeterminate progress bar).
|
||||
|
||||
## Rules
|
||||
|
||||
1. **Use the four standard breakpoints** (`1024 / 768 / 480 / 360`); do not invent new ones without a written reason.
|
||||
|
||||
49
tests/unit/services/bot/config.py
Normal file
49
tests/unit/services/bot/config.py
Normal file
@ -0,0 +1,49 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from devplacepy.services.bot.config import (
|
||||
BIO_SIGNAL_PHRASES,
|
||||
HOME_HOST,
|
||||
HOME_URL,
|
||||
bio_has_signal,
|
||||
ensure_bio_signal,
|
||||
)
|
||||
|
||||
|
||||
def test_home_url_contains_home_host():
|
||||
assert HOME_HOST in HOME_URL
|
||||
|
||||
|
||||
def test_every_signal_phrase_carries_the_host():
|
||||
assert BIO_SIGNAL_PHRASES
|
||||
for phrase in BIO_SIGNAL_PHRASES:
|
||||
assert bio_has_signal(phrase)
|
||||
|
||||
|
||||
def test_bio_has_signal_detects_host_case_insensitive():
|
||||
assert bio_has_signal("Proudly writing from DevPlace.NET since day one")
|
||||
assert not bio_has_signal("Just a developer who likes coffee")
|
||||
assert not bio_has_signal("")
|
||||
assert not bio_has_signal(None)
|
||||
|
||||
|
||||
def test_ensure_bio_signal_appends_phrase_when_missing():
|
||||
result = ensure_bio_signal("I build compilers for fun.")
|
||||
assert result.startswith("I build compilers for fun.")
|
||||
assert bio_has_signal(result)
|
||||
|
||||
|
||||
def test_ensure_bio_signal_keeps_signalled_bio_untouched():
|
||||
bio = f"Shipping side projects from {HOME_URL} daily."
|
||||
assert ensure_bio_signal(bio) == bio
|
||||
|
||||
|
||||
def test_ensure_bio_signal_handles_empty_bio():
|
||||
result = ensure_bio_signal("")
|
||||
assert bio_has_signal(result)
|
||||
assert result in BIO_SIGNAL_PHRASES
|
||||
|
||||
|
||||
def test_ensure_bio_signal_stays_within_profile_limit():
|
||||
result = ensure_bio_signal("x" * 400)
|
||||
assert len(result) <= 500
|
||||
assert bio_has_signal(result)
|
||||
83
tests/unit/services/bot/social.py
Normal file
83
tests/unit/services/bot/social.py
Normal file
@ -0,0 +1,83 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import types
|
||||
|
||||
from devplacepy.services.bot.bot import DevPlaceBot
|
||||
from devplacepy.services.bot.config import HOME_URL
|
||||
from tests.conftest import run_async
|
||||
|
||||
|
||||
def _fake_bot(profile, profile_filled=True, update_ok=True):
|
||||
fake = types.SimpleNamespace()
|
||||
fake.state = types.SimpleNamespace(profile_filled=profile_filled)
|
||||
fake.logs = []
|
||||
fake.updates = 0
|
||||
fake.base_url = "http://localhost:10500"
|
||||
|
||||
async def _fetch():
|
||||
return profile
|
||||
|
||||
async def _update():
|
||||
fake.updates += 1
|
||||
return update_ok
|
||||
|
||||
async def _goto(url):
|
||||
return None
|
||||
|
||||
async def _idle(*args):
|
||||
return None
|
||||
|
||||
fake._fetch_own_profile = _fetch
|
||||
fake._profile_has_signal = lambda: DevPlaceBot._profile_has_signal(fake)
|
||||
fake._update_profile = _update
|
||||
fake._log = lambda msg: fake.logs.append(msg)
|
||||
fake.b = types.SimpleNamespace(goto=_goto, _idle=_idle)
|
||||
return fake
|
||||
|
||||
|
||||
def _profile(bio, website):
|
||||
return {"profile_user": {"bio": bio, "website": website}}
|
||||
|
||||
|
||||
def test_profile_has_signal_accepts_signalled_bio_and_home_website():
|
||||
fake = _fake_bot(_profile(f"Grown at {HOME_URL}.", HOME_URL))
|
||||
assert run_async(DevPlaceBot._profile_has_signal(fake)) is True
|
||||
|
||||
|
||||
def test_profile_has_signal_rejects_plain_bio():
|
||||
fake = _fake_bot(_profile("Just a developer.", HOME_URL))
|
||||
assert run_async(DevPlaceBot._profile_has_signal(fake)) is False
|
||||
|
||||
|
||||
def test_profile_has_signal_rejects_foreign_website():
|
||||
fake = _fake_bot(_profile(f"Grown at {HOME_URL}.", "https://example.dev"))
|
||||
assert run_async(DevPlaceBot._profile_has_signal(fake)) is False
|
||||
|
||||
|
||||
def test_profile_has_signal_rejects_empty_profile():
|
||||
fake = _fake_bot({})
|
||||
assert run_async(DevPlaceBot._profile_has_signal(fake)) is False
|
||||
|
||||
|
||||
def test_ensure_profile_signal_skips_update_when_already_signalled():
|
||||
fake = _fake_bot(_profile(f"Grown at {HOME_URL}.", HOME_URL))
|
||||
assert run_async(DevPlaceBot._ensure_profile_signal(fake)) is True
|
||||
assert fake.updates == 0
|
||||
|
||||
|
||||
def test_ensure_profile_signal_updates_when_signal_missing():
|
||||
fake = _fake_bot(_profile("Just a developer.", HOME_URL))
|
||||
assert run_async(DevPlaceBot._ensure_profile_signal(fake)) is True
|
||||
assert fake.updates == 1
|
||||
|
||||
|
||||
def test_ensure_profile_signal_updates_unfilled_profile():
|
||||
fake = _fake_bot(_profile(f"Grown at {HOME_URL}.", HOME_URL), profile_filled=False)
|
||||
assert run_async(DevPlaceBot._ensure_profile_signal(fake)) is True
|
||||
assert fake.updates == 1
|
||||
|
||||
|
||||
def test_ensure_profile_signal_reports_failed_update():
|
||||
fake = _fake_bot(_profile("Just a developer.", HOME_URL), update_ok=False)
|
||||
assert run_async(DevPlaceBot._ensure_profile_signal(fake)) is False
|
||||
assert fake.updates == 1
|
||||
Loading…
Reference in New Issue
Block a user