updates 33.1 139
This commit is contained in:
@@ -374,6 +374,21 @@
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
<!-- theme:baked-defaults — generated by common/theme/bake.py; do not edit -->
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0d0d0f;
|
||||
--border: #2e2e38;
|
||||
--border-strong: #3d3d4a;
|
||||
--dim: #555568;
|
||||
--muted: #8888a0;
|
||||
--surface: #16161a;
|
||||
--system-accent: #d4a574;
|
||||
--system-accent-text: #e0b98d;
|
||||
--text: #e8e8f0;
|
||||
}
|
||||
</style>
|
||||
<!-- /theme:baked-defaults -->
|
||||
<link rel="stylesheet" href="/theme.css">
|
||||
<style>
|
||||
/* Artery keeps its own colour under every theme. */
|
||||
@@ -398,8 +413,8 @@
|
||||
<circle cx="24" cy="20" r="2" fill="currentColor" />
|
||||
</svg>
|
||||
<h1>Artery</h1>
|
||||
{% if pawprint_url %}<a
|
||||
href="{{ pawprint_url }}"
|
||||
{% if soleprint_url %}<a
|
||||
href="{{ soleprint_url }}"
|
||||
style="
|
||||
position: absolute;
|
||||
right: 0;
|
||||
@@ -918,7 +933,7 @@
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
{% if pawprint_url %}<a href="{{ pawprint_url }}">← Soleprint</a>{%
|
||||
{% if soleprint_url %}<a href="{{ soleprint_url }}">← Soleprint</a>{%
|
||||
else %}<span class="disabled">← Soleprint</span>{% endif %}
|
||||
</footer>
|
||||
|
||||
|
||||
43
soleprint/artery/plexuses/bundle/plexus.json
Normal file
43
soleprint/artery/plexuses/bundle/plexus.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "bundle",
|
||||
"title": "Soleprint Bundle",
|
||||
"description": "What a rig installation has at its disposal. Exports to a single file that opens with no server.",
|
||||
"theme": "lucid",
|
||||
"graph": "system_overview",
|
||||
"_comment": "A plexus is exported, not served. build.py inlines the theme, the data and the diagram into one index.html so it survives a locked-down Windows box, a zip attachment and a double-click. Nothing here is fetched at runtime.",
|
||||
|
||||
"tools": [
|
||||
{"name": "modelgen", "summary": "Generate models from 6 sources to 9 targets", "standalone": true},
|
||||
{"name": "datagen", "summary": "Serve rig-owned generators; seed from real rows", "standalone": true},
|
||||
{"name": "graphgen", "summary": "Generate navigable model graphs", "standalone": true},
|
||||
{"name": "shuntgen", "summary": "OpenAPI spec or CSV/ODS folder to a running fake service", "standalone": true},
|
||||
{"name": "tester", "summary": "HTTP contract test runner — one suite, any environment", "standalone": true},
|
||||
{"name": "databrowse", "summary": "SQL data browser", "standalone": true},
|
||||
{"name": "sbwrapper", "summary": "Sandbox wrapper", "standalone": true}
|
||||
],
|
||||
|
||||
"cabinets": [
|
||||
{"name": "postgres", "summary": "Relational database", "rig_addon": "postgres"},
|
||||
{"name": "redis", "summary": "Cache and broker", "rig_addon": "redis"},
|
||||
{"name": "airflow", "summary": "Scheduled pipelines", "rig_addon": "airflow", "needs": ["postgres", "redis"]}
|
||||
],
|
||||
|
||||
"veins": [
|
||||
{"name": "google", "summary": "Sheets and Drive, over OAuth2"},
|
||||
{"name": "jira", "summary": "Issues and boards"},
|
||||
{"name": "slack", "summary": "Messages and channels"},
|
||||
{"name": "ia", "summary": "Model connector"}
|
||||
],
|
||||
|
||||
"themes": [
|
||||
{"name": "lucid", "summary": "Light, print-ready, shaped after lucid.app", "active": true},
|
||||
{"name": "soleprint", "summary": "Dark, rounded, amber — the default"},
|
||||
{"name": "mcrn", "summary": "Dark, square, monospace"}
|
||||
],
|
||||
|
||||
"next": [
|
||||
"Every tool above is standalone: it runs without the rest of soleprint.",
|
||||
"Cabinets install as compose services on a laptop, or as rig addons of the same name in a cluster.",
|
||||
"The diagram below is generated from docs/graphs/*.dot and themed by the same palette as this page."
|
||||
]
|
||||
}
|
||||
@@ -138,6 +138,16 @@
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
<!-- theme:baked-defaults — generated by common/theme/bake.py; do not edit -->
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0d0d0f;
|
||||
--muted: #8888a0;
|
||||
--system-accent-text: #e0b98d;
|
||||
--text: #e8e8f0;
|
||||
}
|
||||
</style>
|
||||
<!-- /theme:baked-defaults -->
|
||||
<link rel="stylesheet" href="/theme.css">
|
||||
<style>
|
||||
/* Atlas keeps its own colour under every theme. */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Album - Documentation system.
|
||||
Atlas - Documentation system.
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -11,7 +11,7 @@ from fastapi.responses import HTMLResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
app = FastAPI(title="Album", version="0.1.0")
|
||||
app = FastAPI(title="Atlas", version="0.1.0")
|
||||
|
||||
BASE_DIR = Path(__file__).parent.resolve()
|
||||
BOOK_DIR = BASE_DIR / "book"
|
||||
@@ -25,24 +25,24 @@ templates = Jinja2Templates(directory=str(BASE_DIR))
|
||||
# Serve static files
|
||||
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
||||
|
||||
# Pawprint URL for data fetching
|
||||
PAWPRINT_URL = os.getenv("PAWPRINT_URL", "http://localhost:12000")
|
||||
# The soleprint hub this atlas reads its data from.
|
||||
SOLEPRINT_URL = os.getenv("SOLEPRINT_URL", "http://localhost:12000")
|
||||
|
||||
|
||||
def get_data():
|
||||
"""Fetch data from pawprint hub."""
|
||||
"""Fetch data from the soleprint hub."""
|
||||
try:
|
||||
resp = httpx.get(f"{PAWPRINT_URL}/api/data/album", timeout=5.0)
|
||||
resp = httpx.get(f"{SOLEPRINT_URL}/api/data/atlas", timeout=5.0)
|
||||
if resp.status_code == 200:
|
||||
return resp.json()
|
||||
except Exception as e:
|
||||
print(f"Failed to fetch data from pawprint: {e}")
|
||||
return {"templates": [], "larders": [], "books": []}
|
||||
print(f"Failed to fetch data from soleprint: {e}")
|
||||
return {"templates": [], "depots": [], "books": []}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
return {"status": "ok", "service": "album"}
|
||||
return {"status": "ok", "service": "atlas"}
|
||||
|
||||
|
||||
@app.get("/")
|
||||
@@ -52,7 +52,7 @@ def index(request: Request):
|
||||
"index.html",
|
||||
{
|
||||
"request": request,
|
||||
"pawprint_url": os.getenv("PAWPRINT_EXTERNAL_URL", PAWPRINT_URL),
|
||||
"soleprint_url": os.getenv("SOLEPRINT_EXTERNAL_URL", SOLEPRINT_URL),
|
||||
**data,
|
||||
},
|
||||
)
|
||||
@@ -60,7 +60,7 @@ def index(request: Request):
|
||||
|
||||
@app.get("/api/data")
|
||||
def api_data():
|
||||
"""API endpoint for frontend data (proxied from pawprint)."""
|
||||
"""API endpoint for frontend data (proxied from soleprint)."""
|
||||
return get_data()
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ def feature_form_samples_template():
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Feature Form Template · Album</title>
|
||||
<title>Feature Form Template · Atlas</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
@@ -229,7 +229,7 @@ def feature_form_samples_template():
|
||||
<div class="container">
|
||||
<header>
|
||||
<div class="breadcrumb">
|
||||
<a href="/">Album</a> / <a href="/book/feature-form-samples/">Feature Form Samples</a> / Template
|
||||
<a href="/">Atlas</a> / <a href="/book/feature-form-samples/">Feature Form Samples</a> / Template
|
||||
</div>
|
||||
<h1>Feature Form Template</h1>
|
||||
<div class="meta">
|
||||
@@ -244,7 +244,7 @@ def feature_form_samples_template():
|
||||
<div class="form-body">
|
||||
<div class="field">
|
||||
<label class="field-label">Tipo de Usuario</label>
|
||||
<div class="field-value">[Dueno de mascota / Veterinario / Admin]</div>
|
||||
<div class="field-value">[Tipo A / Tipo B / Admin]</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label">Punto de Entrada</label>
|
||||
@@ -296,25 +296,25 @@ def feature_form_samples_template():
|
||||
return HTMLResponse(html)
|
||||
|
||||
|
||||
@app.get("/book/feature-form-samples/larder/", response_class=HTMLResponse)
|
||||
@app.get("/book/feature-form-samples/larder", response_class=HTMLResponse)
|
||||
def feature_form_samples_larder():
|
||||
"""Browse the larder (actual data)"""
|
||||
@app.get("/book/feature-form-samples/depot/", response_class=HTMLResponse)
|
||||
@app.get("/book/feature-form-samples/depot", response_class=HTMLResponse)
|
||||
def feature_form_samples_depot():
|
||||
"""Browse the depot (actual data)"""
|
||||
html_file = BOOK_DIR / "feature-form-samples" / "index.html"
|
||||
if html_file.exists():
|
||||
return HTMLResponse(html_file.read_text())
|
||||
return HTMLResponse("<h1>Larder index not found</h1>", status_code=404)
|
||||
return HTMLResponse("<h1>Depot index not found</h1>", status_code=404)
|
||||
|
||||
|
||||
@app.get(
|
||||
"/book/feature-form-samples/larder/{user_type}/{filename}",
|
||||
"/book/feature-form-samples/depot/{user_type}/{filename}",
|
||||
response_class=HTMLResponse,
|
||||
)
|
||||
def feature_form_samples_detail(request: Request, user_type: str, filename: str):
|
||||
"""View a specific feature form"""
|
||||
# Look in the larder subfolder (feature-form)
|
||||
larder_dir = BOOK_DIR / "feature-form-samples" / "feature-form"
|
||||
file_path = larder_dir / user_type / filename
|
||||
# Look in the depot subfolder (feature-form)
|
||||
depot_dir = BOOK_DIR / "feature-form-samples" / "feature-form"
|
||||
file_path = depot_dir / user_type / filename
|
||||
if not file_path.exists():
|
||||
return HTMLResponse("<h1>Not found</h1>", status_code=404)
|
||||
|
||||
|
||||
177
soleprint/common/theme/bake.py
Normal file
177
soleprint/common/theme/bake.py
Normal file
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Bake the default palette into the pages that use it.
|
||||
|
||||
python3 common/theme/bake.py # rewrite the baked blocks
|
||||
python3 common/theme/bake.py --check # fail if any page is stale
|
||||
|
||||
A page that says `background: var(--bg)` and never gets `--bg` does not fall
|
||||
back to something plainer — the declaration is invalid at computed-value time,
|
||||
so the background goes transparent and the text goes initial-black on a design
|
||||
that assumed dark. Unstyled, not merely unbranded.
|
||||
|
||||
That matters because `/theme.css` is an absolute path and soleprint is not
|
||||
always at the root. In the sample room's nginx, soleprint sits under `/spr/`
|
||||
while `location /` proxies to the frontend, so `/theme.css` reaches the wrong
|
||||
service. Same story for a page opened over file://.
|
||||
|
||||
So every page carries a baked default: a `:root` block with literal values,
|
||||
emitted BEFORE the `<link>`. Both are `:root`, so document order decides — the
|
||||
served stylesheet wins whenever it loads, and the baked block is what is left
|
||||
when it does not. Nothing is given up in either direction.
|
||||
|
||||
The block is generated rather than hand-written, which is the point: the values
|
||||
come from tokens.css and the default theme, so there is still one source. Only
|
||||
the variables a page actually uses are emitted, so the blocks stay small.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
SPR_ROOT = HERE.parent.parent # soleprint/
|
||||
|
||||
TOKENS = HERE / "tokens.css"
|
||||
DEFAULT_THEME = HERE / "themes" / "soleprint.css"
|
||||
|
||||
BEGIN = "<!-- theme:baked-defaults — generated by common/theme/bake.py; do not edit -->"
|
||||
END = "<!-- /theme:baked-defaults -->"
|
||||
|
||||
# Pages that link /theme.css and therefore need a default to fall back on.
|
||||
PAGES = [
|
||||
"index.html",
|
||||
"artery/index.html",
|
||||
"atlas/index.html",
|
||||
"station/index.html",
|
||||
"station/tools/datagen/templates/index.html",
|
||||
"station/tools/graphgen/templates/index.html",
|
||||
"station/tools/shuntgen/templates/index.html",
|
||||
]
|
||||
|
||||
|
||||
def declarations(css: str, selector: str) -> dict[str, str]:
|
||||
"""Pull `--name: value;` pairs out of one rule."""
|
||||
match = re.search(re.escape(selector) + r"\s*\{(.*?)\n\}", css, re.S)
|
||||
if not match:
|
||||
return {}
|
||||
out = {}
|
||||
for name, value in re.findall(r"(--[\w-]+)\s*:\s*([^;]+);", match.group(1)):
|
||||
out[name] = value.strip()
|
||||
return out
|
||||
|
||||
|
||||
def resolve(name: str, table: dict[str, str], seen: frozenset = frozenset()) -> str | None:
|
||||
"""Flatten a value to literals, following var() chains and honouring fallbacks."""
|
||||
if name in seen or name not in table:
|
||||
return None
|
||||
value = table[name]
|
||||
|
||||
def swap(match: re.Match) -> str:
|
||||
inner = match.group(1)
|
||||
# var(--x, fallback) — the fallback may itself contain commas.
|
||||
if "," in inner:
|
||||
ref, fallback = inner.split(",", 1)
|
||||
ref, fallback = ref.strip(), fallback.strip()
|
||||
else:
|
||||
ref, fallback = inner.strip(), None
|
||||
resolved = resolve(ref, table, seen | {name})
|
||||
if resolved is not None:
|
||||
return resolved
|
||||
return fallback if fallback is not None else ""
|
||||
|
||||
for _ in range(10): # chains are shallow; the bound just stops a cycle
|
||||
new = re.sub(r"var\(\s*([^()]*(?:\([^()]*\)[^()]*)*)\)", swap, value)
|
||||
if new == value:
|
||||
break
|
||||
value = new
|
||||
return value.strip() or None
|
||||
|
||||
|
||||
def palette() -> dict[str, str]:
|
||||
"""Every token, flattened to literals, with the default theme applied."""
|
||||
tokens = declarations(TOKENS.read_text(), ":root")
|
||||
tokens.update(declarations(DEFAULT_THEME.read_text(), '[data-theme="soleprint"]'))
|
||||
return {name: resolve(name, tokens) for name in tokens}
|
||||
|
||||
|
||||
def used(html: str) -> set[str]:
|
||||
"""Variables a page references, ignoring the baked block itself."""
|
||||
body = re.sub(re.escape(BEGIN) + r".*?" + re.escape(END), "", html, flags=re.S)
|
||||
return set(re.findall(r"var\(\s*(--[\w-]+)", body))
|
||||
|
||||
|
||||
def block(names: set[str], values: dict[str, str], indent: str) -> str:
|
||||
"""The baked <style>, wrapped in markers so it can be replaced next time."""
|
||||
lines = [f"{indent}{BEGIN}", f"{indent}<style>", f"{indent} :root {{"]
|
||||
for name in sorted(names):
|
||||
value = values.get(name)
|
||||
if value:
|
||||
lines.append(f"{indent} {name}: {value};")
|
||||
lines += [f"{indent} }}", f"{indent}</style>", f"{indent}{END}"]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def bake(path: Path, values: dict[str, str]) -> tuple[bool, str]:
|
||||
"""Return (changed, note) for one page."""
|
||||
html = path.read_text()
|
||||
|
||||
link = re.search(r'([ \t]*)<link rel="stylesheet" href="/theme.css">', html)
|
||||
if not link:
|
||||
return False, "no /theme.css link — skipped"
|
||||
|
||||
indent = link.group(1)
|
||||
names = used(html)
|
||||
if not names:
|
||||
return False, "uses no theme variables — skipped"
|
||||
|
||||
fresh = block(names, values, indent)
|
||||
|
||||
existing = re.search(re.escape(BEGIN) + r".*?" + re.escape(END), html, re.S)
|
||||
if existing:
|
||||
updated = html[: existing.start()] + fresh.lstrip() + html[existing.end() :]
|
||||
else:
|
||||
# Before the link, never after: document order is what makes the served
|
||||
# stylesheet win over the baked one.
|
||||
updated = html[: link.start()] + fresh + "\n" + html[link.start() :]
|
||||
|
||||
if updated == html:
|
||||
return False, f"up to date ({len(names)} variables)"
|
||||
path.write_text(updated)
|
||||
return True, f"baked {len(names)} variables"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
check = "--check" in sys.argv
|
||||
values = palette()
|
||||
missing = [n for n, v in values.items() if not v]
|
||||
if missing:
|
||||
print(f"warning: unresolved tokens: {', '.join(sorted(missing))}", file=sys.stderr)
|
||||
|
||||
stale = []
|
||||
for rel in PAGES:
|
||||
path = SPR_ROOT / rel
|
||||
if not path.exists():
|
||||
print(f" {rel}: not found")
|
||||
continue
|
||||
if check:
|
||||
before = path.read_text()
|
||||
changed, note = bake(path, values)
|
||||
if changed:
|
||||
path.write_text(before)
|
||||
stale.append(rel)
|
||||
print(f" {rel}: STALE")
|
||||
else:
|
||||
print(f" {rel}: {note}")
|
||||
else:
|
||||
_, note = bake(path, values)
|
||||
print(f" {rel}: {note}")
|
||||
|
||||
if check and stale:
|
||||
print(f"\n{len(stale)} page(s) stale — run: python3 common/theme/bake.py", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -19,7 +19,11 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var THEMES = ["soleprint", "mcrn"];
|
||||
// Order is the toggle order. soleprint stays first because it is the
|
||||
// default; lucid is last because it is the one you switch to on purpose,
|
||||
// to show someone something.
|
||||
var THEMES = ["soleprint", "mcrn", "lucid"];
|
||||
var LABELS = { soleprint: "SPR", mcrn: "MCRN", lucid: "LUCID" };
|
||||
var KEY = "spr-theme";
|
||||
var root = document.documentElement;
|
||||
|
||||
@@ -82,7 +86,7 @@
|
||||
var button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.dataset.theme = theme;
|
||||
button.textContent = theme === "mcrn" ? "MCRN" : "SPR";
|
||||
button.textContent = LABELS[theme] || theme.toUpperCase();
|
||||
button.title = "Switch to the " + theme + " theme";
|
||||
button.addEventListener("click", function () {
|
||||
apply(theme, true);
|
||||
|
||||
150
soleprint/common/theme/themes/lucid.css
Normal file
150
soleprint/common/theme/themes/lucid.css
Normal file
@@ -0,0 +1,150 @@
|
||||
/* Lucid — the regulated-document look, shaped after lucid.app exports.
|
||||
*
|
||||
* Why it exists: the deliverable gets shown on Windows, printed, and pasted next
|
||||
* to real Lucidchart diagrams. Dark developer chrome cannot go in that room. The
|
||||
* target is that a generated page and a genuine Lucid export sit side by side
|
||||
* without announcing which is which.
|
||||
*
|
||||
* This is the first LIGHT theme here, and that is the part that needed care —
|
||||
* tokens.css and both sibling themes were written assuming near-black. Two
|
||||
* things do not survive the inversion and are overridden below rather than
|
||||
* inherited:
|
||||
*
|
||||
* - the glow. A coloured halo means "lit" against black; against white it just
|
||||
* looks like a rendering fault. Replaced with a hairline drop shadow.
|
||||
* - --dim as body-adjacent text. At #555568 on white it fails contrast, so the
|
||||
* ramp is rebuilt from the light end rather than reused.
|
||||
*
|
||||
* Fonts are stacks, never a webfont: this has to render with no egress. Arial is
|
||||
* last because it is the one face guaranteed on Windows and aliased on Linux —
|
||||
* the same reason the graphviz themes name it (see docs/graphs/themes/).
|
||||
*/
|
||||
|
||||
[data-theme="lucid"] {
|
||||
color-scheme: light;
|
||||
|
||||
--bg: #ffffff;
|
||||
--bg-2: #f5f7fa;
|
||||
--surface: #f5f7fa;
|
||||
--surface-raised: #e4e7eb;
|
||||
--border: #cbd2d9;
|
||||
--border-strong: #9aa5b1;
|
||||
|
||||
/* Measured against both #ffffff and the #f5f7fa panel, because --dim is used
|
||||
* for 11px notes and --status-warn for 10px labels — sizes where AA wants
|
||||
* 4.5:1, not the 3:1 that large text gets away with. The obvious lighter
|
||||
* greys (#7b8794, #73808d) come in at 3.4–3.8 on the panel and were dropped
|
||||
* for that reason. */
|
||||
--text: #1f2933; /* 14.76 on white — near-black; pure #000 reads harsh in print */
|
||||
--muted: #616e7c; /* 5.21 / 4.86 */
|
||||
--dim: #66717d; /* 4.97 / 4.63 */
|
||||
|
||||
--accent: #3a7dff;
|
||||
--accent-dim: #2f6ae0;
|
||||
--accent-text: #1c5bd9; /* darkened: the fill blue is too light for small text */
|
||||
--glow: rgba(58, 125, 255, 0.18);
|
||||
|
||||
--status-ok: #0b875b; /* 4.53 / 4.23 */
|
||||
--status-info: #1c5bd9; /* 5.92 / 5.52 */
|
||||
--status-warn: #a35f00; /* 5.01 / 4.67 — #b06a00 was 3.99 on the panel */
|
||||
--status-error: #cf2e2e; /* 5.14 / 4.79 */
|
||||
--status-idle: #9aa5b1; /* dots and rules only, never text */
|
||||
|
||||
--radius-sm: 4px;
|
||||
--radius: 6px;
|
||||
--radius-lg: 8px;
|
||||
--radius-xl: 8px;
|
||||
|
||||
--font-ui: "Segoe UI", Inter, system-ui, -apple-system, Arial, sans-serif;
|
||||
--font-mono: "Cascadia Mono", Consolas, "JetBrains Mono", monospace;
|
||||
--font-heading: "Segoe UI", Inter, system-ui, Arial, sans-serif;
|
||||
--heading-transform: none;
|
||||
--heading-spacing: 0;
|
||||
--heading-weight: 600;
|
||||
--label-spacing: 0.02em;
|
||||
|
||||
--speed-fast: 0.12s;
|
||||
--speed: 0.18s;
|
||||
|
||||
/* Elevation, not luminosity. */
|
||||
--hover-shadow: 0 1px 3px rgba(16, 24, 40, 0.1), 0 1px 2px rgba(16, 24, 40, 0.06);
|
||||
--hover-lift: none;
|
||||
--focus-shadow: 0 0 0 2px rgba(58, 125, 255, 0.35);
|
||||
}
|
||||
|
||||
/* Panels are white cards on a pale canvas — the inverse of the dark themes,
|
||||
* where the panel is lighter than the page. */
|
||||
[data-theme="lucid"] .panel,
|
||||
[data-theme="lucid"] .card,
|
||||
[data-theme="lucid"] .model-card {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
[data-theme="lucid"] .card:hover,
|
||||
[data-theme="lucid"] .panel:hover,
|
||||
[data-theme="lucid"] .system-card:hover,
|
||||
[data-theme="lucid"] .tool-card:hover,
|
||||
[data-theme="lucid"] .model-card:hover {
|
||||
border-color: var(--system-accent, var(--accent));
|
||||
box-shadow: var(--hover-shadow);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* A solid fill with white knocked out, the way a Lucid toolbar reads. No
|
||||
* gradient: gradients are the first thing that looks wrong in print. */
|
||||
[data-theme="lucid"] button[aria-pressed="true"],
|
||||
[data-theme="lucid"] .active,
|
||||
[data-theme="lucid"] .selected {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
[data-theme="lucid"] .label,
|
||||
[data-theme="lucid"] .panel-title {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* Pale-fill chips, the shape Lucid uses for tags on a shape. */
|
||||
[data-theme="lucid"] .badge {
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 1px 6px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
background: #eaf0ff;
|
||||
border: 1px solid #c3d4ff;
|
||||
color: var(--accent-text);
|
||||
}
|
||||
|
||||
[data-theme="lucid"] code,
|
||||
[data-theme="lucid"] pre {
|
||||
background: #f5f7fa;
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
/* Diagrams are the point of this theme, and they are rendered as <img>, so the
|
||||
* page cannot colour them — it can only stop fighting them. A white-canvas SVG
|
||||
* needs a frame to read as a figure rather than as a hole in the page. */
|
||||
[data-theme="lucid"] img[src$=".svg"] {
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
/* Printing is a first-class output here: this theme exists to end up in a
|
||||
* document. Drop the chrome that has no meaning on paper. */
|
||||
@media print {
|
||||
[data-theme="lucid"] #spr-theme-toggle,
|
||||
[data-theme="lucid"] #spr-sidebar {
|
||||
display: none !important;
|
||||
}
|
||||
[data-theme="lucid"] .panel,
|
||||
[data-theme="lucid"] .card {
|
||||
box-shadow: none;
|
||||
break-inside: avoid;
|
||||
}
|
||||
}
|
||||
@@ -42,9 +42,9 @@
|
||||
--radius-lg: 0;
|
||||
--radius-xl: 0;
|
||||
|
||||
--font-ui: "JetBrains Mono", "Fira Code", "SF Mono", monospace;
|
||||
--font-mono: "JetBrains Mono", "Fira Code", "SF Mono", monospace;
|
||||
--font-heading: "JetBrains Mono", "Fira Code", "SF Mono", monospace;
|
||||
--font-ui: "JetBrains Mono", "Cascadia Mono", Consolas, "SF Mono", monospace;
|
||||
--font-mono: "JetBrains Mono", "Cascadia Mono", Consolas, "SF Mono", monospace;
|
||||
--font-heading: "JetBrains Mono", "Cascadia Mono", Consolas, "SF Mono", monospace;
|
||||
--heading-transform: uppercase;
|
||||
--heading-spacing: 0.1em;
|
||||
--heading-weight: 400;
|
||||
|
||||
@@ -41,9 +41,9 @@
|
||||
--radius-lg: 8px;
|
||||
--radius-xl: 12px;
|
||||
|
||||
--font-ui: "Inter", system-ui, -apple-system, sans-serif;
|
||||
--font-mono: "JetBrains Mono", "Fira Code", monospace;
|
||||
--font-heading: "Inter", system-ui, sans-serif;
|
||||
--font-ui: Inter, "Segoe UI", system-ui, -apple-system, Arial, sans-serif;
|
||||
--font-mono: "JetBrains Mono", "Cascadia Mono", Consolas, monospace;
|
||||
--font-heading: Inter, "Segoe UI", system-ui, Arial, sans-serif;
|
||||
--heading-transform: none;
|
||||
--heading-spacing: 0.02em;
|
||||
--heading-weight: 600;
|
||||
|
||||
@@ -20,7 +20,11 @@
|
||||
* handler beside /sidebar.css.
|
||||
*/
|
||||
|
||||
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;600&display=swap");
|
||||
/* No webfont import. This has to render on a locked-down Windows box with no
|
||||
* egress and from a double-clicked file, and a blocked stylesheet there is a
|
||||
* blank page or a stall, not a fallback. The stacks below resolve to something
|
||||
* deliberate on every target: Segoe UI and Consolas ship with Windows, Inter and
|
||||
* JetBrains Mono are picked up where they happen to be installed. */
|
||||
|
||||
:root {
|
||||
/* ── surfaces ─────────────────────────────────────────────────────── */
|
||||
@@ -63,8 +67,8 @@
|
||||
--hairline: 1px;
|
||||
|
||||
/* ── type ─────────────────────────────────────────────────────────── */
|
||||
--font-ui: "Inter", system-ui, -apple-system, sans-serif;
|
||||
--font-mono: "JetBrains Mono", "Fira Code", "SF Mono", monospace;
|
||||
--font-ui: "Segoe UI", Inter, system-ui, -apple-system, Arial, sans-serif;
|
||||
--font-mono: "Cascadia Mono", "JetBrains Mono", Consolas, "SF Mono", monospace;
|
||||
--font-heading: var(--font-ui);
|
||||
--font-size-sm: 11px;
|
||||
--font-size-base: 13px;
|
||||
|
||||
@@ -249,6 +249,20 @@
|
||||
<link rel="stylesheet" href="/sidebar.css">
|
||||
<script src="/sidebar.js"></script>
|
||||
{% endif %}
|
||||
<!-- theme:baked-defaults — generated by common/theme/bake.py; do not edit -->
|
||||
<style>
|
||||
:root {
|
||||
--accent: #d4a574;
|
||||
--accent-dim: #b8956a;
|
||||
--bg: #0d0d0f;
|
||||
--border: #2e2e38;
|
||||
--dim: #555568;
|
||||
--muted: #8888a0;
|
||||
--surface: #16161a;
|
||||
--text: #e8e8f0;
|
||||
}
|
||||
</style>
|
||||
<!-- /theme:baked-defaults -->
|
||||
<link rel="stylesheet" href="/theme.css">
|
||||
</head>
|
||||
<body{% if managed %} class="has-sidebar"{% endif %}>
|
||||
|
||||
@@ -601,11 +601,21 @@ def station_route(path: str):
|
||||
# page can switch themes without a second request and without FOUC.
|
||||
|
||||
|
||||
def available_themes() -> list[str]:
|
||||
"""Theme names, from the files themselves — adding one is adding a file."""
|
||||
theme_dir = SPR_ROOT / "common" / "theme" / "themes"
|
||||
if not theme_dir.exists():
|
||||
return ["soleprint"]
|
||||
names = sorted(p.stem for p in theme_dir.glob("*.css"))
|
||||
# Default first, so a consumer taking names[0] gets the sensible one.
|
||||
return sorted(names, key=lambda n: (n != "soleprint", n))
|
||||
|
||||
|
||||
def get_default_theme() -> str:
|
||||
"""The theme a page is served in, before the browser has an opinion."""
|
||||
framework = load_config().get("framework", {})
|
||||
theme = framework.get("theme", "soleprint")
|
||||
return theme if theme in ("soleprint", "mcrn") else "soleprint"
|
||||
return theme if theme in available_themes() else "soleprint"
|
||||
|
||||
|
||||
@app.get("/theme.css")
|
||||
@@ -652,7 +662,7 @@ def theme_js():
|
||||
@app.get("/api/theme")
|
||||
def theme_config():
|
||||
"""The server-side default, for pages that render their own <html> tag."""
|
||||
return {"theme": get_default_theme(), "themes": ["soleprint", "mcrn"]}
|
||||
return {"theme": get_default_theme(), "themes": available_themes()}
|
||||
|
||||
|
||||
@app.get("/sidebar.css")
|
||||
|
||||
@@ -157,6 +157,20 @@
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
<!-- theme:baked-defaults — generated by common/theme/bake.py; do not edit -->
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0d0d0f;
|
||||
--border: #2e2e38;
|
||||
--border-strong: #3d3d4a;
|
||||
--muted: #8888a0;
|
||||
--surface: #16161a;
|
||||
--system-accent: #d4a574;
|
||||
--system-accent-text: #e0b98d;
|
||||
--text: #e8e8f0;
|
||||
}
|
||||
</style>
|
||||
<!-- /theme:baked-defaults -->
|
||||
<link rel="stylesheet" href="/theme.css">
|
||||
<style>
|
||||
/* Station keeps its own colour under every theme. */
|
||||
|
||||
@@ -15,26 +15,26 @@ This monitor provides at-a-glance views of the database grouped by test-relevant
|
||||
|
||||
## Architecture
|
||||
|
||||
Follows pawprint **book/larder pattern**:
|
||||
- **larder/** contains all data files (schema, views, scenarios)
|
||||
Follows the book/depot pattern:
|
||||
- **depot/** contains all data files (schema, views, scenarios)
|
||||
- **main.py** generates SQL queries from view definitions
|
||||
- Two modes: **SQL** (direct queries) and **API** (Django backend, placeholder)
|
||||
|
||||
### Key Concepts
|
||||
|
||||
**Schema** (`larder/schema.json`)
|
||||
**Schema** (`depot/schema.json`)
|
||||
- AMAR data model with SQL table mappings
|
||||
- Regular fields (from database columns)
|
||||
- Computed fields (SQL expressions)
|
||||
- Support for multiple graph generators
|
||||
|
||||
**Views** (`larder/views.json`)
|
||||
**Views** (`depot/views.json`)
|
||||
- Define what to display and how to group it
|
||||
- Each view targets an entity (User, PetOwner, Veterinarian, etc.)
|
||||
- Can group results (e.g., by role, by data state, by availability)
|
||||
- SQL is generated automatically from view configuration
|
||||
|
||||
**Scenarios** (`larder/scenarios.json`)
|
||||
**Scenarios** (`depot/scenarios.json`)
|
||||
- Test scenarios emerge from actual usage
|
||||
- Format defined, real scenarios added as needed
|
||||
- Links scenarios to specific views with filters
|
||||
@@ -49,21 +49,21 @@ Follows pawprint **book/larder pattern**:
|
||||
## Running Locally
|
||||
|
||||
```bash
|
||||
cd /home/mariano/wdir/ama/pawprint/ward/monitor/data_browse
|
||||
cd soleprint/station/monitors/databrowse
|
||||
python main.py
|
||||
# Opens on http://localhost:12020
|
||||
```
|
||||
|
||||
Or with uvicorn:
|
||||
```bash
|
||||
uvicorn ward.monitor.data_browse.main:app --port 12020 --reload
|
||||
uvicorn station.monitors.databrowse.main:app --port 12020 --reload
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
# Database connection (defaults to local dev)
|
||||
export NEST_NAME=local
|
||||
export ROOM_NAME=local
|
||||
export DB_HOST=localhost
|
||||
export DB_PORT=5433
|
||||
export DB_NAME=amarback
|
||||
@@ -85,7 +85,7 @@ GET /api/scenarios # Test scenarios (JSON)
|
||||
|
||||
## Adding New Views
|
||||
|
||||
Edit `larder/views.json`:
|
||||
Edit `depot/views.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -112,7 +112,7 @@ The SQL query is automatically generated from:
|
||||
|
||||
## Adding Computed Fields
|
||||
|
||||
Edit `larder/schema.json` in the entity definition:
|
||||
Edit `depot/schema.json` in the entity definition:
|
||||
|
||||
```json
|
||||
"computed": {
|
||||
@@ -127,7 +127,7 @@ Computed fields can be used in views just like regular fields.
|
||||
|
||||
## Adding Test Scenarios
|
||||
|
||||
As you identify test patterns, add them to `larder/scenarios.json`:
|
||||
As you identify test patterns, add them to `depot/scenarios.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -153,8 +153,8 @@ As you identify test patterns, add them to `larder/scenarios.json`:
|
||||
|
||||
```
|
||||
data_browse/
|
||||
├── larder/
|
||||
│ ├── .larder # Larder marker (book pattern)
|
||||
├── depot/
|
||||
│ ├── .depot # Depot marker (book pattern)
|
||||
│ ├── schema.json # AMAR data model with SQL mappings
|
||||
│ ├── views.json # View configurations
|
||||
│ └── scenarios.json # Test scenarios
|
||||
|
||||
@@ -245,7 +245,7 @@
|
||||
|
||||
{% if views|length == 0 %}
|
||||
<div class="empty">
|
||||
No views configured. Add views to larder/views.json
|
||||
No views configured. Add views to depot/views.json
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card-grid">
|
||||
@@ -289,7 +289,7 @@
|
||||
<div class="empty">
|
||||
No scenarios defined yet. Scenarios emerge from usage and
|
||||
conversations.
|
||||
<br />Add them to larder/scenarios.json as you identify test
|
||||
<br />Add them to depot/scenarios.json as you identify test
|
||||
patterns.
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
@@ -6,6 +6,21 @@
|
||||
<title>datagen — Test Data Generator</title>
|
||||
<!-- Palette, fonts and the theme switcher. The :root block that used to sit
|
||||
here was one of eight copies that had already drifted apart. -->
|
||||
<!-- theme:baked-defaults — generated by common/theme/bake.py; do not edit -->
|
||||
<style>
|
||||
:root {
|
||||
--amber: #d4a574;
|
||||
--amber-dim: #b8956a;
|
||||
--bg: #0d0d0f;
|
||||
--border: #2e2e38;
|
||||
--dim: #555568;
|
||||
--font-ui: Inter, "Segoe UI", system-ui, -apple-system, Arial, sans-serif;
|
||||
--muted: #8888a0;
|
||||
--surface: #16161a;
|
||||
--text: #e8e8f0;
|
||||
}
|
||||
</style>
|
||||
<!-- /theme:baked-defaults -->
|
||||
<link rel="stylesheet" href="/theme.css">
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
@@ -5,6 +5,21 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>graphgen — Schema Explorer</title>
|
||||
<!-- Palette, fonts and the theme switcher — see common/theme/. -->
|
||||
<!-- theme:baked-defaults — generated by common/theme/bake.py; do not edit -->
|
||||
<style>
|
||||
:root {
|
||||
--amber: #d4a574;
|
||||
--amber-dim: #b8956a;
|
||||
--bg: #0d0d0f;
|
||||
--border: #2e2e38;
|
||||
--dim: #555568;
|
||||
--font-ui: Inter, "Segoe UI", system-ui, -apple-system, Arial, sans-serif;
|
||||
--muted: #8888a0;
|
||||
--surface: #16161a;
|
||||
--text: #e8e8f0;
|
||||
}
|
||||
</style>
|
||||
<!-- /theme:baked-defaults -->
|
||||
<link rel="stylesheet" href="/theme.css">
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Pawprint Wrapper - Demo</title>
|
||||
<title>Sidebar Wrapper - Demo</title>
|
||||
<link rel="stylesheet" href="sidebar.css">
|
||||
<style>
|
||||
/* Demo page styles */
|
||||
@@ -19,7 +19,7 @@
|
||||
transition: margin-right 0.3s ease;
|
||||
}
|
||||
|
||||
#pawprint-sidebar.expanded ~ #demo-content {
|
||||
#spr-sidebar.expanded ~ #demo-content {
|
||||
margin-right: var(--sidebar-width);
|
||||
}
|
||||
|
||||
@@ -105,14 +105,14 @@
|
||||
|
||||
<div id="demo-content">
|
||||
<div class="demo-header">
|
||||
<h1>🐾 Pawprint Wrapper</h1>
|
||||
<p>Development tools sidebar for any pawprint-managed nest</p>
|
||||
<h1>Sidebar Wrapper</h1>
|
||||
<p>Development tools sidebar for any soleprint-managed room</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2>👋 Quick Start</h2>
|
||||
<p>
|
||||
This is a standalone demo of the Pawprint Wrapper sidebar.
|
||||
This is a standalone demo of the sidebar wrapper.
|
||||
Click the toggle button on the right edge of the screen, or press
|
||||
<span class="kbd">Ctrl</span> + <span class="kbd">Shift</span> + <span class="kbd">P</span>
|
||||
to open the sidebar.
|
||||
|
||||
@@ -4,6 +4,37 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>shuntgen</title>
|
||||
<!-- theme:baked-defaults — generated by common/theme/bake.py; do not edit -->
|
||||
<style>
|
||||
:root {
|
||||
--accent: #d4a574;
|
||||
--accent-dim: #b8956a;
|
||||
--accent-text: #e0b98d;
|
||||
--bg: #0d0d0f;
|
||||
--bg-2: #16161a;
|
||||
--border: #2e2e38;
|
||||
--dim: #555568;
|
||||
--font-mono: "JetBrains Mono", "Cascadia Mono", Consolas, monospace;
|
||||
--font-ui: Inter, "Segoe UI", system-ui, -apple-system, Arial, sans-serif;
|
||||
--hairline: 1px;
|
||||
--label-spacing: 0.04em;
|
||||
--muted: #8888a0;
|
||||
--radius: 6px;
|
||||
--radius-lg: 8px;
|
||||
--radius-sm: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-6: 24px;
|
||||
--status-error: #f06565;
|
||||
--status-info: #4f9cf9;
|
||||
--status-ok: #3ecf8e;
|
||||
--status-warn: #f5a623;
|
||||
--surface: #16161a;
|
||||
--text: #e8e8f0;
|
||||
}
|
||||
</style>
|
||||
<!-- /theme:baked-defaults -->
|
||||
<link rel="stylesheet" href="/theme.css">
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
# Contract HTTP Tests - Environment Configuration
|
||||
#
|
||||
# Get API key: ./get-api-key.sh --docker core_nest_db
|
||||
|
||||
CONTRACT_TEST_URL=http://backend:8000
|
||||
CONTRACT_TEST_API_KEY=118b1fcca089496919f0d82df2c4c89d35126793dfc3ea645366ae09d931f49f
|
||||
19
soleprint/station/tools/tester/.env.example
Normal file
19
soleprint/station/tools/tester/.env.example
Normal file
@@ -0,0 +1,19 @@
|
||||
# Contract test target. Copy to .env and fill in — .env is gitignored.
|
||||
#
|
||||
# cp .env.example .env
|
||||
#
|
||||
# Environment variables override this file (see config.py), so CI can set these
|
||||
# without a file at all:
|
||||
#
|
||||
# CONTRACT_TEST_URL=https://staging.example.com python -m tester run
|
||||
#
|
||||
# A real key was committed here once. Keep credentials in .env or the
|
||||
# environment; environments.json is tracked and must stay empty of them.
|
||||
|
||||
CONTRACT_TEST_URL=http://localhost:8000
|
||||
|
||||
# One of: bearer (default) | api-key | none
|
||||
CONTRACT_TEST_AUTH_TYPE=none
|
||||
|
||||
# CONTRACT_TEST_API_KEY=
|
||||
# CONTRACT_TEST_TOKEN=
|
||||
@@ -154,11 +154,11 @@ ward/tools/tester/
|
||||
|
||||
If running standalone:
|
||||
```bash
|
||||
cd /home/mariano/wdir/ama/pawprint/ward/tools/tester
|
||||
cd soleprint/station/tools/tester
|
||||
python -m uvicorn main:app --reload --port 12003
|
||||
```
|
||||
|
||||
Or if integrated with ward:
|
||||
Or if integrated with station:
|
||||
```bash
|
||||
# Ward service should pick it up automatically
|
||||
```
|
||||
|
||||
@@ -16,7 +16,9 @@ set -e
|
||||
# Defaults
|
||||
DB_HOST="${DB_HOST:-localhost}"
|
||||
DB_PORT="${DB_PORT:-5432}"
|
||||
DB_NAME="${DB_NAME:-amarback}"
|
||||
# No default database name: this ships in an open framework, and a client's
|
||||
# schema name is not a sensible fallback. Set DB_NAME or pass --name.
|
||||
DB_NAME="${DB_NAME:-}"
|
||||
DB_USER="${DB_USER:-postgres}"
|
||||
DB_PASSWORD="${DB_PASSWORD:-}"
|
||||
DOCKER_CONTAINER=""
|
||||
@@ -25,7 +27,7 @@ DOCKER_CONTAINER=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--docker)
|
||||
DOCKER_CONTAINER="${2:-core_nest_db}"
|
||||
DOCKER_CONTAINER="${2:-}"
|
||||
shift 2 || shift 1
|
||||
;;
|
||||
--host)
|
||||
@@ -52,10 +54,10 @@ while [[ $# -gt 0 ]]; do
|
||||
echo "Usage: $0 [options]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --docker [container] Query via docker exec (default: core_nest_db)"
|
||||
echo " --docker <container> Query via docker exec (container name required)"
|
||||
echo " --host HOST Database host"
|
||||
echo " --port PORT Database port (default: 5432)"
|
||||
echo " --name NAME Database name (default: amarback)"
|
||||
echo " --name NAME Database name (required, or set DB_NAME)"
|
||||
echo " --user USER Database user (default: postgres)"
|
||||
echo " --password PASS Database password"
|
||||
echo ""
|
||||
@@ -69,6 +71,13 @@ while [[ $# -gt 0 ]]; do
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$DB_NAME" ]]; then
|
||||
echo "no database name — pass --name NAME or set DB_NAME" >&2
|
||||
echo "(there is no default: this ships in an open framework, and a client's" >&2
|
||||
echo " schema name is not a sensible fallback)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
QUERY="SELECT key FROM common_apikey WHERE is_active=true LIMIT 1;"
|
||||
|
||||
if [[ -n "$DOCKER_CONTAINER" ]]; then
|
||||
|
||||
@@ -6,7 +6,6 @@ Tests basic HTTP connectivity and authentication flow.
|
||||
"""
|
||||
|
||||
from ..base import ContractTestCase
|
||||
from ..endpoints import Endpoints
|
||||
|
||||
|
||||
class TestHealth(ContractTestCase):
|
||||
|
||||
Reference in New Issue
Block a user