generated from coulomb/repo-seed
observatory stuff
This commit is contained in:
20
.claude/rules/agents.md
Normal file
20
.claude/rules/agents.md
Normal file
@@ -0,0 +1,20 @@
|
||||
## Kaizen Agents
|
||||
|
||||
Specialized agent personas available on demand via the state-hub MCP.
|
||||
|
||||
**Discover:** `list_kaizen_agents()` — returns all agents with name, description, category
|
||||
**Load:** `get_kaizen_agent("tdd-workflow")` — returns full instructions; read and follow them
|
||||
|
||||
Common agents:
|
||||
|
||||
| Agent | Category | When to use |
|
||||
|-------|----------|-------------|
|
||||
| `tdd-workflow` | testing | Step-by-step TDD8 workflow for any feature |
|
||||
| `code-refactoring` | quality | Code quality analysis and safe refactoring |
|
||||
| `test-maintenance` | testing | Diagnose and fix failing tests |
|
||||
| `requirements-engineering` | process | Prevent interface/mock mismatches upfront |
|
||||
| `keepaTodofile` | process | Maintain TODO.md during work |
|
||||
| `project-management` | process | Track status, determine next steps |
|
||||
| `datamodel-optimization` | quality | Optimize dataclasses and data structures |
|
||||
|
||||
All 17 agents: call `list_kaizen_agents()` for the full list.
|
||||
8
.claude/rules/architecture.md
Normal file
8
.claude/rules/architecture.md
Normal file
@@ -0,0 +1,8 @@
|
||||
## Architecture
|
||||
|
||||
<!-- TODO: Describe the key design decisions and component structure.
|
||||
Key modules, data flows, external integrations, state machines, etc. -->
|
||||
|
||||
## Quick Reference
|
||||
|
||||
`~/state-hub/mcp_server/TOOLS.md` — MCP tool reference
|
||||
38
.claude/rules/first-session.md
Normal file
38
.claude/rules/first-session.md
Normal file
@@ -0,0 +1,38 @@
|
||||
## First Session Protocol
|
||||
|
||||
Triggered when `get_domain_summary("financials")` shows **no workstreams**.
|
||||
The project is registered but work has not yet been structured.
|
||||
|
||||
**Step 1 — Read, don't write**
|
||||
- `~/the-custodian/canon/projects/financials/project_charter_v0.1.md` — purpose, scope
|
||||
- `~/the-custodian/canon/projects/financials/roadmap_v0.1.md` — planned phases
|
||||
- Scan repo root: README, directory structure, existing code or docs
|
||||
|
||||
**Step 2 — Survey in-progress work**
|
||||
Look for TODOs, open branches, half-finished files. Note done vs. started but incomplete.
|
||||
|
||||
**Step 3 — Propose workstreams to Bernd**
|
||||
Propose 1–3 workstreams — each a coherent strand, weeks to months, anchored to a
|
||||
roadmap phase. **Wait for approval before creating.**
|
||||
|
||||
**Step 4 — Create workplan file first, then DB record (ADR-001)**
|
||||
```
|
||||
workplans/ADAPTIVE-WP-NNNN-<slug>.md ← write this first
|
||||
```
|
||||
Then register in the hub:
|
||||
```
|
||||
create_workstream(topic_id="f39fa2a3-c491-414c-a91b-b4c5fcc6139c", title="...", owner="...", description="...")
|
||||
create_task(workstream_id="<id>", title="...", priority="high|medium|low")
|
||||
```
|
||||
|
||||
**Step 5 — Record the setup**
|
||||
```
|
||||
add_progress_event(
|
||||
summary="First session: structured financials into N workstreams, M tasks",
|
||||
event_type="milestone",
|
||||
topic_id="f39fa2a3-c491-414c-a91b-b4c5fcc6139c",
|
||||
detail={"workstreams": [...], "tasks_created": M}
|
||||
)
|
||||
```
|
||||
|
||||
<!-- Delete or archive this file once past first session -->
|
||||
8
.claude/rules/repo-boundary.md
Normal file
8
.claude/rules/repo-boundary.md
Normal file
@@ -0,0 +1,8 @@
|
||||
## Repo boundary
|
||||
|
||||
This repo owns **adaptive-pricing** only. It does not own:
|
||||
|
||||
<!-- TODO: List what belongs in adjacent repos, e.g.:
|
||||
- SSH key management → railiance-infra/
|
||||
- State hub code → state-hub/
|
||||
-->
|
||||
5
.claude/rules/repo-identity.md
Normal file
5
.claude/rules/repo-identity.md
Normal file
@@ -0,0 +1,5 @@
|
||||
**Purpose:** Auto-regulating market value exploring price engine.
|
||||
|
||||
**Domain:** financials
|
||||
**Repo slug:** adaptive-pricing
|
||||
**Topic ID:** f39fa2a3-c491-414c-a91b-b4c5fcc6139c
|
||||
85
.claude/rules/session-protocol.md
Normal file
85
.claude/rules/session-protocol.md
Normal file
@@ -0,0 +1,85 @@
|
||||
## Session Protocol
|
||||
|
||||
Dev Hub (State Hub API): http://127.0.0.1:8000
|
||||
MCP server name in `~/.claude.json`: `dev-hub`
|
||||
|
||||
**Step 1 — Orient**
|
||||
|
||||
Read the offline-safe brief first — it works without a live hub connection:
|
||||
```bash
|
||||
cat .custodian-brief.md
|
||||
```
|
||||
Then call the MCP tool for richer cross-domain context when MCP tools are exposed:
|
||||
```
|
||||
get_domain_summary("financials")
|
||||
```
|
||||
If MCP tools are unavailable in the current agent session, use the REST API:
|
||||
```bash
|
||||
curl -s "http://127.0.0.1:8000/state/summary" | python3 -m json.tool
|
||||
```
|
||||
If the hub is offline: `cd ~/state-hub && make api`
|
||||
|
||||
**Step 2 — Check inbox**
|
||||
With MCP tools:
|
||||
```
|
||||
get_messages(to_agent="adaptive-pricing", unread_only=True)
|
||||
```
|
||||
Mark read with `mark_message_read(message_id)`. Reply or act on coordination
|
||||
requests before proceeding.
|
||||
|
||||
Without MCP tools:
|
||||
```bash
|
||||
curl -s "http://127.0.0.1:8000/messages/?to_agent=adaptive-pricing&unread_only=true" \
|
||||
| python3 -m json.tool
|
||||
curl -s -X PATCH "http://127.0.0.1:8000/messages/<id>/read" \
|
||||
-H "Content-Type: application/json" -d '{}'
|
||||
```
|
||||
|
||||
**Step 3 — Scan workplans**
|
||||
```bash
|
||||
ls workplans/
|
||||
```
|
||||
For each file with `status: ready`, `active`, or `blocked`, note pending
|
||||
`wait`/`todo`/`progress` tasks.
|
||||
|
||||
**Step 4 — Present brief**
|
||||
|
||||
1. **Active workstreams** for `financials` — title, task counts, blocking decisions
|
||||
2. **Pending tasks** from `workplans/` + any `[repo:adaptive-pricing]` hub tasks
|
||||
3. **Goal guidance** — if `goal_guidance` in summary:
|
||||
- `needs_workplan`: surface as top action — *"Repo goal '{title}' has no workplan yet"*
|
||||
- `alignment_warnings`: flag if active work is not aligned with current goal
|
||||
4. **Suggested next action** — highest-priority open item
|
||||
5. **SBOM status** — flag if `last_sbom_at` is unset for this repo
|
||||
|
||||
If no workstreams: follow First Session Protocol (`first-session.md`).
|
||||
|
||||
**During work:** `record_decision()` · `add_progress_event()` · `resolve_decision()`
|
||||
|
||||
> State Hub is a *read model*. Bootstrap tools (`create_workstream`, `create_task`)
|
||||
> are First Session Protocol only. Work structure belongs in repo files (ADR-001).
|
||||
|
||||
**Session close:**
|
||||
With MCP tools:
|
||||
```
|
||||
add_progress_event(summary="...", topic_id="f39fa2a3-c491-414c-a91b-b4c5fcc6139c", workstream_id="<uuid>")
|
||||
```
|
||||
Without MCP tools:
|
||||
```bash
|
||||
curl -s -X POST http://127.0.0.1:8000/progress/ \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"topic_id":"f39fa2a3-c491-414c-a91b-b4c5fcc6139c","workstream_id":"<uuid>","event_type":"note","summary":"what changed","author":"codex"}'
|
||||
```
|
||||
If workplan files were modified, ensure the local copy is up to date first:
|
||||
```bash
|
||||
git -C <repo_path> pull --ff-only
|
||||
cd ~/state-hub && make fix-consistency REPO=adaptive-pricing
|
||||
```
|
||||
For repos where implementation runs on a remote machine (e.g. CoulombCore),
|
||||
use the combined target which pulls before fixing:
|
||||
```bash
|
||||
cd ~/state-hub && make fix-consistency-remote REPO=adaptive-pricing
|
||||
```
|
||||
**C-15** (DB task ahead of file) is normal in multi-machine workflows — writeback
|
||||
will sync the file to match DB. **C-16** (repo behind remote) blocks all writes
|
||||
until you pull — intentional to prevent clobbering remote progress.
|
||||
19
.claude/rules/stack-and-commands.md
Normal file
19
.claude/rules/stack-and-commands.md
Normal file
@@ -0,0 +1,19 @@
|
||||
## Stack
|
||||
|
||||
<!-- TODO: Fill in language, frameworks, and key dependencies -->
|
||||
- **Language:**
|
||||
- **Key deps:**
|
||||
|
||||
## Dev Commands
|
||||
|
||||
```bash
|
||||
# TODO: Fill in the standard commands for this repo
|
||||
|
||||
# Install dependencies
|
||||
|
||||
# Run tests
|
||||
|
||||
# Lint / type check
|
||||
|
||||
# Build / package (if applicable)
|
||||
```
|
||||
40
.claude/rules/workplan-convention.md
Normal file
40
.claude/rules/workplan-convention.md
Normal file
@@ -0,0 +1,40 @@
|
||||
## Workplan Convention (ADR-001)
|
||||
|
||||
File location: `workplans/ADAPTIVE-WP-NNNN-<slug>.md`
|
||||
ID prefix: `ADAPTIVE-WP-`
|
||||
|
||||
Work items originate as files in this repo **before** being registered in the hub.
|
||||
|
||||
Canonical workplan/workstream frontmatter statuses are:
|
||||
`proposed`, `ready`, `active`, `blocked`, `backlog`, `finished`, `archived`.
|
||||
Use `proposed` for a newly drafted plan, `ready` after review against current
|
||||
repo state, and `finished` when implementation is complete. `stalled` and
|
||||
`needs_review` are derived health labels, not stored statuses.
|
||||
|
||||
Closed workplans may be moved to `workplans/archived/` with a completion-date
|
||||
prefix: `YYMMDD-ADAPTIVE-WP-NNNN-<slug>.md`. The frontmatter id remains
|
||||
unchanged; the prefix is only for quick visual reference.
|
||||
|
||||
Small opportunistic tasks discovered during another session use **Ad Hoc Tasks**:
|
||||
`workplans/ADHOC-YYYY-MM-DD.md`, workstream slug `adhoc-YYYY-MM-DD`, and task ids
|
||||
`ADHOC-YYYY-MM-DD-T01`, `T02`, etc. Use adhocs only for low-risk work completed
|
||||
directly. Promote anything requiring analysis, design, approval, dependencies, or
|
||||
multiple planned phases into a normal workplan.
|
||||
|
||||
Ecosystem todos from other agents arrive as `[repo:adaptive-pricing]` hub tasks —
|
||||
visible at session start. Pick one up by creating the workplan file, then registering
|
||||
the workstream.
|
||||
|
||||
Task blocks use this shape:
|
||||
|
||||
```task
|
||||
id: ADAPTIVE-WP-NNNN-T01
|
||||
status: wait | todo | progress | done | cancel
|
||||
priority: high | medium | low
|
||||
state_hub_task_id: "<uuid>" # written by fix-consistency — do not edit
|
||||
```
|
||||
|
||||
Status progression is `todo` → `progress` → `done`; use `wait` for waiting or
|
||||
blocked work and `cancel` for stopped work.
|
||||
|
||||
<!-- Ralph Loop rules and HEUREKA sequence: ~/.claude/CLAUDE.md — do not duplicate here -->
|
||||
28
AGENTS.md
28
AGENTS.md
@@ -4,37 +4,13 @@
|
||||
|
||||
**Purpose:** Auto-regulating market value exploring price engine.
|
||||
|
||||
**Domain:** helix_forge
|
||||
**Domain:** financials
|
||||
**Repo slug:** adaptive-pricing
|
||||
**Topic ID:** `f39fa2a3-c491-414c-a91b-b4c5fcc6139c`
|
||||
**Workplan prefix:** `ADAPTIVE-WP-`
|
||||
|
||||
---
|
||||
|
||||
## Dev Workflow
|
||||
|
||||
Framework docs live at the repo root. The Coulomb MVP implementation lives in
|
||||
`projects/coulomb-pricing/observatory/` (stdlib Python 3.11+, no runtime deps;
|
||||
`pytest` for tests). Run MVP commands from `projects/coulomb-pricing/`.
|
||||
|
||||
| Need | Command |
|
||||
|------|---------|
|
||||
| Python | `python3` (3.11+) |
|
||||
| Install | none — stdlib only; for tests: `pip install pytest` |
|
||||
| Test (MVP) | `cd projects/coulomb-pricing && python3 -m pytest -q` |
|
||||
| Test (repo) | same as MVP until other packages exist |
|
||||
| Lint / format | none configured — match surrounding style |
|
||||
| Build | none |
|
||||
| Run: economics dashboard | `cd projects/coulomb-pricing && python3 -m observatory --period YYYY-MM` |
|
||||
| Run: observatory UI | `cd projects/coulomb-pricing && python3 -m observatory.server` → http://127.0.0.1:8765/ |
|
||||
| Workplan / hub sync | `cd ~/state-hub && make fix-consistency REPO=adaptive-pricing REPO_PATH=~/adaptive-pricing` |
|
||||
| Registry sanity | `grep -q '^version:' registry/indexes/capabilities.yaml && echo OK` |
|
||||
|
||||
**Verify a change before declaring it done:** run `python3 -m pytest` under
|
||||
`projects/coulomb-pricing`, then `make fix-consistency` (expect PASS).
|
||||
|
||||
---
|
||||
|
||||
## State Hub Integration
|
||||
|
||||
The Custodian State Hub tracks work across all domains. Interact via HTTP REST —
|
||||
@@ -176,8 +152,6 @@ get wrong.
|
||||
|
||||
**Canon:** `~/ops-warden/wiki/CredentialRouting.md` · catalog `~/ops-warden/registry/routing/catalog.yaml`
|
||||
|
||||
---
|
||||
|
||||
<!-- REPO-AGENTS-EXTENSIONS -->
|
||||
<!-- Append repo-specific agent instructions below this marker.
|
||||
The state-hub template sync preserves content after this line. -->
|
||||
|
||||
12
CLAUDE.md
Normal file
12
CLAUDE.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# adaptive-pricing — Claude Code Instructions
|
||||
|
||||
@SCOPE.md
|
||||
@.claude/rules/repo-identity.md
|
||||
@.claude/rules/session-protocol.md
|
||||
@.claude/rules/first-session.md
|
||||
@.claude/rules/workplan-convention.md
|
||||
@.claude/rules/stack-and-commands.md
|
||||
@.claude/rules/architecture.md
|
||||
@.claude/rules/repo-boundary.md
|
||||
@.claude/rules/credential-routing.md
|
||||
@.claude/rules/agents.md
|
||||
19
projects/coulomb-pricing/Makefile
Normal file
19
projects/coulomb-pricing/Makefile
Normal file
@@ -0,0 +1,19 @@
|
||||
.PHONY: help design test serve
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
REF ?=
|
||||
|
||||
help: ## List available make targets
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
|
||||
| awk 'BEGIN {FS = ":.*?## "}; {printf " %-12s %s\n", $$1, $$2}'
|
||||
|
||||
design: ## Re-vendor whynot-design (optional: REF=v0.2.1 or commit SHA)
|
||||
./scripts/sync-whynot-design.sh $(REF)
|
||||
python3 -m pytest -q tests/test_ui_vendor.py
|
||||
|
||||
test: ## Run the full pytest suite
|
||||
python3 -m pytest -q
|
||||
|
||||
serve: ## Start the Economic Observatory UI on :8765
|
||||
python3 -m observatory.server
|
||||
@@ -32,11 +32,12 @@ cost-pass-through billing is not active.
|
||||
|
||||
```bash
|
||||
cd projects/coulomb-pricing
|
||||
python3 -m pytest -q
|
||||
make test
|
||||
make design # pull whynot-design tokens/CSS/components
|
||||
make design REF=v0.2.1 # bump to a tag or commit
|
||||
python3 -m observatory --period 2026-06
|
||||
python3 -m observatory --period 2026-06 --output reports/economics-2026-06.md
|
||||
./scripts/sync-whynot-design.sh # pull whynot-design tokens/CSS/components
|
||||
python3 -m observatory.server
|
||||
make serve
|
||||
```
|
||||
|
||||
Open **http://127.0.0.1:8765/** for the Economic Observatory UI (served from
|
||||
|
||||
56
projects/coulomb-pricing/data/market_signals.json
Normal file
56
projects/coulomb-pricing/data/market_signals.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"version": 1,
|
||||
"currency": "EUR",
|
||||
"last_reviewed": "2026-06",
|
||||
"alternatives": [
|
||||
{
|
||||
"id": "github-pro",
|
||||
"name": "GitHub Pro",
|
||||
"category": "indirect",
|
||||
"price_monthly_eur": "4.00",
|
||||
"billing": "monthly",
|
||||
"features": ["private repos", "protected branches", "CI minutes"],
|
||||
"signal_level": "S2",
|
||||
"observed": "2026-06",
|
||||
"source": "public pricing page",
|
||||
"note": "Substitute for repository hosting; lacks community and pricing-intelligence positioning."
|
||||
},
|
||||
{
|
||||
"id": "circle-community",
|
||||
"name": "Circle (community platform)",
|
||||
"category": "indirect",
|
||||
"price_monthly_eur": "49.00",
|
||||
"billing": "monthly",
|
||||
"features": ["courses", "events", "member spaces", "payments"],
|
||||
"signal_level": "S2",
|
||||
"observed": "2026-06",
|
||||
"source": "public pricing page",
|
||||
"note": "Higher floor for hosted community; Coulomb is narrower and operator-led."
|
||||
},
|
||||
{
|
||||
"id": "patreon-creator",
|
||||
"name": "Patreon (creator membership)",
|
||||
"category": "substitute",
|
||||
"price_monthly_eur": "8.00",
|
||||
"billing": "monthly",
|
||||
"features": ["member posts", "Discord integration", "paywall"],
|
||||
"signal_level": "S1",
|
||||
"observed": "2026-06",
|
||||
"source": "public pricing page",
|
||||
"note": "Comparable entry price point; different delivery model (content vs repo access)."
|
||||
},
|
||||
{
|
||||
"id": "build-in-house",
|
||||
"name": "Build in-house community",
|
||||
"category": "workaround",
|
||||
"price_monthly_eur": "0.00",
|
||||
"billing": "operator time",
|
||||
"features": ["self-hosted forum", "manual onboarding", "no shared pricing lab"],
|
||||
"signal_level": "S3",
|
||||
"observed": "2026-06",
|
||||
"source": "operator estimate",
|
||||
"note": "Hidden cost in time; common alternative for technical founders."
|
||||
}
|
||||
],
|
||||
"notes": "Market signals are manually curated until competitive intelligence imports exist."
|
||||
}
|
||||
40
projects/coulomb-pricing/data/value_range.json
Normal file
40
projects/coulomb-pricing/data/value_range.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"version": 1,
|
||||
"currency": "EUR",
|
||||
"product_id": "coulomb-social-membership",
|
||||
"segments": [
|
||||
{
|
||||
"id": "solo-builder",
|
||||
"name": "Solo builder",
|
||||
"low_eur": "8.99",
|
||||
"high_eur": "19.00",
|
||||
"confidence": "hypothesis",
|
||||
"drivers": ["private repository access", "async community", "low switching cost"],
|
||||
"evidence": "Founding member joined at list price without negotiation."
|
||||
},
|
||||
{
|
||||
"id": "small-team",
|
||||
"name": "Small product team",
|
||||
"low_eur": "12.00",
|
||||
"high_eur": "29.00",
|
||||
"confidence": "hypothesis",
|
||||
"drivers": ["shared norms", "pricing experiment sandbox", "operator proximity"],
|
||||
"evidence": "No team-tier offer yet; inferred from comparable community memberships."
|
||||
}
|
||||
],
|
||||
"value_drivers": [
|
||||
{
|
||||
"id": "repo-access",
|
||||
"label": "Repository & community access",
|
||||
"strength": "S2",
|
||||
"note": "Core deliverable today; differentiated by operator involvement, not feature breadth."
|
||||
},
|
||||
{
|
||||
"id": "pricing-lab",
|
||||
"label": "Live pricing laboratory",
|
||||
"strength": "S1",
|
||||
"note": "Members observe real operator economics; value increases as observatory matures."
|
||||
}
|
||||
],
|
||||
"notes": "Value range is observatory-only until willingness-to-pay interviews and usage signals exist."
|
||||
}
|
||||
@@ -11,12 +11,15 @@ from .load import (
|
||||
latest_period,
|
||||
load_budget,
|
||||
load_expense_records,
|
||||
load_market_signals,
|
||||
load_membership,
|
||||
load_monthly_ledger,
|
||||
load_payment_records,
|
||||
load_pricing_models,
|
||||
load_product,
|
||||
load_value_range,
|
||||
)
|
||||
from .pricing_context import build_cost_floor, build_market_price_view, build_value_range_view
|
||||
|
||||
|
||||
def _serialize(value: Any) -> Any:
|
||||
@@ -72,6 +75,9 @@ def build_dashboard_payload(data_dir: Path | None = None, period: str | None = N
|
||||
}
|
||||
)
|
||||
|
||||
value_range_raw = load_value_range(root)
|
||||
market_raw = load_market_signals(root)
|
||||
|
||||
return _serialize(
|
||||
{
|
||||
"design_reference": "https://claude.ai/design/p/fb2eef8c-c1fc-4c75-bff4-3782552e5511",
|
||||
@@ -85,6 +91,9 @@ def build_dashboard_payload(data_dir: Path | None = None, period: str | None = N
|
||||
"members": members,
|
||||
"payments": payments,
|
||||
"expense_record_count": len(expenses),
|
||||
"cost_floor": build_cost_floor(snapshot, models),
|
||||
"value_range": build_value_range_view(value_range_raw, snapshot, product, models),
|
||||
"market_price": build_market_price_view(market_raw),
|
||||
"infrastructure": {
|
||||
"domains": _load_json_catalog(root, "domains.json"),
|
||||
"virtual_servers": _load_json_catalog(root, "virtual_servers.json"),
|
||||
|
||||
@@ -116,6 +116,14 @@ def load_payment_records(data_dir: Path | None = None) -> list[PaymentRecord]:
|
||||
]
|
||||
|
||||
|
||||
def load_value_range(data_dir: Path | None = None) -> dict:
|
||||
return _read_json((data_dir or default_data_dir()) / "value_range.json")
|
||||
|
||||
|
||||
def load_market_signals(data_dir: Path | None = None) -> dict:
|
||||
return _read_json((data_dir or default_data_dir()) / "market_signals.json")
|
||||
|
||||
|
||||
def load_membership(data_dir: Path | None = None) -> list[MembershipRecord]:
|
||||
raw = _read_json((data_dir or default_data_dir()) / "membership.json")
|
||||
return [
|
||||
|
||||
93
projects/coulomb-pricing/observatory/pricing_context.py
Normal file
93
projects/coulomb-pricing/observatory/pricing_context.py
Normal file
@@ -0,0 +1,93 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
|
||||
from .economics import active_pricing_model
|
||||
from .models import EconomicsSnapshot, PricingModel, Product
|
||||
|
||||
TWOPLACES = Decimal("0.01")
|
||||
|
||||
|
||||
def _quantize(value: Decimal) -> Decimal:
|
||||
return value.quantize(TWOPLACES, rounding=ROUND_HALF_UP)
|
||||
|
||||
|
||||
def build_cost_floor(
|
||||
snapshot: EconomicsSnapshot,
|
||||
models: list[PricingModel],
|
||||
) -> dict:
|
||||
active = next((m for m in models if m.status == "active"), None)
|
||||
return {
|
||||
"period": snapshot.period,
|
||||
"currency": snapshot.currency,
|
||||
"monthly_infrastructure_cost": snapshot.monthly_infrastructure_cost,
|
||||
"monthly_payment_processing_cost": snapshot.monthly_payment_processing_cost,
|
||||
"monthly_total_platform_cost": snapshot.monthly_total_platform_cost,
|
||||
"cost_per_member": snapshot.cost_per_member,
|
||||
"active_members": snapshot.active_members,
|
||||
"monthly_revenue": snapshot.monthly_revenue,
|
||||
"gross_margin": snapshot.gross_margin,
|
||||
"gross_margin_pct": snapshot.gross_margin_pct,
|
||||
"active_price": active.access_fee_amount if active else Decimal("0"),
|
||||
"active_model_id": active.id if active else None,
|
||||
"active_model_name": active.name if active else None,
|
||||
}
|
||||
|
||||
|
||||
def build_value_range_view(
|
||||
raw: dict,
|
||||
snapshot: EconomicsSnapshot,
|
||||
product: Product,
|
||||
models: list[PricingModel],
|
||||
) -> dict:
|
||||
model = active_pricing_model(models, product)
|
||||
current = model.access_fee_amount
|
||||
segments = []
|
||||
lows: list[Decimal] = []
|
||||
highs: list[Decimal] = []
|
||||
for item in raw.get("segments", []):
|
||||
low = Decimal(str(item["low_eur"]))
|
||||
high = Decimal(str(item["high_eur"]))
|
||||
lows.append(low)
|
||||
highs.append(high)
|
||||
segments.append(
|
||||
{
|
||||
**item,
|
||||
"headroom_to_high_eur": _quantize(high - current),
|
||||
"below_floor": current < low,
|
||||
}
|
||||
)
|
||||
|
||||
aggregate_low = min(lows) if lows else current
|
||||
aggregate_high = max(highs) if highs else current
|
||||
|
||||
return {
|
||||
"currency": raw.get("currency", snapshot.currency),
|
||||
"product_id": raw.get("product_id", product.id),
|
||||
"current_price_eur": current,
|
||||
"aggregate_low_eur": aggregate_low,
|
||||
"aggregate_high_eur": aggregate_high,
|
||||
"cost_per_member_eur": snapshot.cost_per_member,
|
||||
"segments": segments,
|
||||
"value_drivers": raw.get("value_drivers", []),
|
||||
"notes": raw.get("notes", ""),
|
||||
}
|
||||
|
||||
|
||||
def build_market_price_view(raw: dict) -> dict:
|
||||
alternatives = list(raw.get("alternatives", []))
|
||||
prices = [
|
||||
Decimal(str(item["price_monthly_eur"]))
|
||||
for item in alternatives
|
||||
if item.get("price_monthly_eur") not in (None, "", "0", "0.00")
|
||||
]
|
||||
return {
|
||||
"currency": raw.get("currency", "EUR"),
|
||||
"last_reviewed": raw.get("last_reviewed"),
|
||||
"alternative_count": len(alternatives),
|
||||
"priced_alternative_count": len(prices),
|
||||
"market_low_eur": min(prices) if prices else None,
|
||||
"market_high_eur": max(prices) if prices else None,
|
||||
"alternatives": alternatives,
|
||||
"notes": raw.get("notes", ""),
|
||||
}
|
||||
@@ -18,6 +18,9 @@ def test_dashboard_payload_contains_live_ledger_totals() -> None:
|
||||
assert payload["snapshot"]["monthly_infrastructure_cost"] == "29.73"
|
||||
assert len(payload["history"]) == 18
|
||||
assert payload["expense_record_count"] == 58
|
||||
assert payload["cost_floor"]["active_price"] == "8.99"
|
||||
assert len(payload["value_range"]["segments"]) == 2
|
||||
assert payload["market_price"]["alternative_count"] == 4
|
||||
|
||||
|
||||
def test_payload_json_is_valid() -> None:
|
||||
|
||||
61
projects/coulomb-pricing/tests/test_pricing_context.py
Normal file
61
projects/coulomb-pricing/tests/test_pricing_context.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
from observatory.economics import build_snapshot
|
||||
from observatory.load import (
|
||||
load_market_signals,
|
||||
load_membership,
|
||||
load_monthly_ledger,
|
||||
load_payment_records,
|
||||
load_pricing_models,
|
||||
load_product,
|
||||
load_value_range,
|
||||
)
|
||||
from observatory.pricing_context import (
|
||||
build_cost_floor,
|
||||
build_market_price_view,
|
||||
build_value_range_view,
|
||||
)
|
||||
|
||||
DATA_DIR = Path(__file__).resolve().parent.parent / "data"
|
||||
|
||||
|
||||
def _snapshot_for(period: str = "2026-06"):
|
||||
product = load_product(DATA_DIR)
|
||||
models = load_pricing_models(DATA_DIR)
|
||||
members = load_membership(DATA_DIR)
|
||||
payments = load_payment_records(DATA_DIR)
|
||||
ledger = load_monthly_ledger(DATA_DIR)
|
||||
return build_snapshot(period, product, models, members, payments, ledger)
|
||||
|
||||
|
||||
def test_cost_floor_derives_from_snapshot() -> None:
|
||||
snapshot = _snapshot_for()
|
||||
models = load_pricing_models(DATA_DIR)
|
||||
floor = build_cost_floor(snapshot, models)
|
||||
|
||||
assert floor["cost_per_member"] == snapshot.cost_per_member
|
||||
assert floor["active_price"] == Decimal("8.99")
|
||||
assert floor["active_model_id"] == "flat-899-eur-monthly"
|
||||
|
||||
|
||||
def test_value_range_computes_headroom() -> None:
|
||||
snapshot = _snapshot_for()
|
||||
product = load_product(DATA_DIR)
|
||||
models = load_pricing_models(DATA_DIR)
|
||||
view = build_value_range_view(load_value_range(DATA_DIR), snapshot, product, models)
|
||||
|
||||
assert view["current_price_eur"] == Decimal("8.99")
|
||||
assert len(view["segments"]) == 2
|
||||
solo = next(item for item in view["segments"] if item["id"] == "solo-builder")
|
||||
assert solo["headroom_to_high_eur"] == Decimal("10.01")
|
||||
|
||||
|
||||
def test_market_price_summarises_alternatives() -> None:
|
||||
view = build_market_price_view(load_market_signals(DATA_DIR))
|
||||
|
||||
assert view["alternative_count"] == 4
|
||||
assert view["market_low_eur"] == Decimal("4.00")
|
||||
assert view["market_high_eur"] == Decimal("49.00")
|
||||
@@ -1,5 +1,69 @@
|
||||
const euro = (value) => `€${Number(value).toFixed(2)}`;
|
||||
|
||||
const SECTION_META = {
|
||||
"cost-floor": {
|
||||
eyebrow: "Economic Observatory · Cost Floor",
|
||||
title: "Operator liquidity",
|
||||
lede: (data) =>
|
||||
`Period ${data.period}. Minimum viable economics: platform cost, margin, and liquidity burn from ledgers.`,
|
||||
},
|
||||
"value-range": {
|
||||
eyebrow: "Economic Observatory · Value Range",
|
||||
title: "Customer value bands",
|
||||
lede: (data) =>
|
||||
`Period ${data.period}. Segment-level willingness-to-pay hypotheses above the cost floor.`,
|
||||
},
|
||||
"market-price": {
|
||||
eyebrow: "Economic Observatory · Market Price",
|
||||
title: "Competitive context",
|
||||
lede: (data) =>
|
||||
`Reviewed ${data.market_price.last_reviewed ?? "—"}. Evidence about alternatives, capabilities, and public pricing.`,
|
||||
},
|
||||
};
|
||||
|
||||
let currentData = null;
|
||||
let activeSection = "cost-floor";
|
||||
|
||||
function normalizeData(data) {
|
||||
const snapshot = data.snapshot ?? {};
|
||||
const active = (data.pricing_models ?? []).find((model) => model.status === "active");
|
||||
|
||||
if (!data.cost_floor) {
|
||||
data.cost_floor = {
|
||||
cost_per_member: snapshot.cost_per_member ?? "0",
|
||||
monthly_total_platform_cost: snapshot.monthly_total_platform_cost ?? "0",
|
||||
active_price: active?.access_fee_amount ?? snapshot.monthly_revenue ?? "0",
|
||||
active_model_name: active?.name ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
if (!data.value_range) {
|
||||
data.value_range = {
|
||||
current_price_eur: active?.access_fee_amount ?? snapshot.monthly_revenue ?? "0",
|
||||
aggregate_low_eur: active?.access_fee_amount ?? "0",
|
||||
aggregate_high_eur: active?.access_fee_amount ?? "0",
|
||||
cost_per_member_eur: snapshot.cost_per_member ?? "0",
|
||||
segments: [],
|
||||
value_drivers: [],
|
||||
notes: "Value range data unavailable — restart observatory.server to load the latest API.",
|
||||
};
|
||||
}
|
||||
|
||||
if (!data.market_price) {
|
||||
data.market_price = {
|
||||
alternative_count: 0,
|
||||
priced_alternative_count: 0,
|
||||
market_low_eur: null,
|
||||
market_high_eur: null,
|
||||
last_reviewed: null,
|
||||
alternatives: [],
|
||||
notes: "Market signals unavailable — restart observatory.server to load the latest API.",
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async function loadDashboard(period) {
|
||||
const query = period ? `?period=${encodeURIComponent(period)}` : "";
|
||||
const response = await fetch(`/api/dashboard${query}`);
|
||||
@@ -13,7 +77,36 @@ function setBadge(el, status) {
|
||||
el.draft = status === "burning";
|
||||
}
|
||||
|
||||
function renderMetrics(data) {
|
||||
function setSection(sectionId) {
|
||||
activeSection = sectionId;
|
||||
document.querySelectorAll(".obs-panel").forEach((panel) => {
|
||||
const active = panel.dataset.section === sectionId;
|
||||
panel.hidden = !active;
|
||||
panel.classList.toggle("obs-panel--active", active);
|
||||
});
|
||||
document.querySelectorAll("wn-sidebar-item[data-section]").forEach((item) => {
|
||||
item.active = item.dataset.section === sectionId;
|
||||
});
|
||||
|
||||
const meta = SECTION_META[sectionId];
|
||||
const header = document.getElementById("page-header");
|
||||
header.eyebrow = meta.eyebrow;
|
||||
header.title = meta.title;
|
||||
if (currentData) {
|
||||
header.lede = meta.lede(currentData);
|
||||
}
|
||||
|
||||
const badge = document.getElementById("liquidity-badge");
|
||||
badge.style.display = sectionId === "cost-floor" ? "" : "none";
|
||||
}
|
||||
|
||||
function bindNavigation() {
|
||||
document.querySelectorAll("wn-sidebar-item[data-section]").forEach((item) => {
|
||||
item.addEventListener("click", () => setSection(item.dataset.section));
|
||||
});
|
||||
}
|
||||
|
||||
function renderCostFloor(data) {
|
||||
const { snapshot, liquidity } = data;
|
||||
const cards = [
|
||||
{
|
||||
@@ -27,14 +120,14 @@ function renderMetrics(data) {
|
||||
sub: snapshot.liquidity_status,
|
||||
},
|
||||
{
|
||||
label: "Active members",
|
||||
value: snapshot.active_members,
|
||||
sub: data.members[0]?.username ? `@${data.members[0].username}` : "—",
|
||||
label: "Cost per member",
|
||||
value: euro(data.cost_floor.cost_per_member),
|
||||
sub: `Platform ${euro(data.cost_floor.monthly_total_platform_cost)} / mo`,
|
||||
},
|
||||
{
|
||||
label: "Member payment (gross)",
|
||||
value: euro(snapshot.monthly_revenue),
|
||||
sub: `Net ${euro(data.payments.at(-1)?.net_amount ?? 0)} after Stripe`,
|
||||
label: "Gross margin",
|
||||
value: euro(snapshot.gross_margin),
|
||||
sub: `${snapshot.gross_margin_pct}% of gross revenue`,
|
||||
},
|
||||
{
|
||||
label: "Infrastructure / month",
|
||||
@@ -42,9 +135,9 @@ function renderMetrics(data) {
|
||||
sub: `Processing ${euro(snapshot.monthly_payment_processing_cost)}`,
|
||||
},
|
||||
{
|
||||
label: "Cumulative net liquidity",
|
||||
value: euro(liquidity.cumulative_net_liquidity),
|
||||
sub: liquidity.liquidity_status,
|
||||
label: "Active price",
|
||||
value: euro(data.cost_floor.active_price),
|
||||
sub: data.cost_floor.active_model_name ?? "—",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -58,18 +151,14 @@ function renderMetrics(data) {
|
||||
</wn-card>`
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function renderBudget(data) {
|
||||
const { liquidity } = data;
|
||||
const initial = Number(liquidity.initial_budget);
|
||||
const remaining = Number(liquidity.remaining_budget);
|
||||
const pct = Math.max(0, Math.min(100, (remaining / initial) * 100));
|
||||
const banner = document.getElementById("budget-banner");
|
||||
const caption = remaining >= 0 ? "Within allocated budget" : "Over allocated budget";
|
||||
|
||||
document.getElementById("budget-fill").style.width = `${pct}%`;
|
||||
document.getElementById("budget-caption").textContent = caption;
|
||||
document.getElementById("budget-caption").textContent =
|
||||
remaining >= 0 ? "Within allocated budget" : "Over allocated budget";
|
||||
banner.variant = remaining < initial * 0.25 ? "warn" : "info";
|
||||
|
||||
document.getElementById("budget-stats").innerHTML = [
|
||||
@@ -85,11 +174,9 @@ function renderBudget(data) {
|
||||
</wn-field-row>`
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function renderChart(history) {
|
||||
const max = Math.max(...history.map((row) => Math.abs(Number(row.net_liquidity))), 1);
|
||||
document.getElementById("liquidity-chart").innerHTML = history
|
||||
const max = Math.max(...data.history.map((row) => Math.abs(Number(row.net_liquidity))), 1);
|
||||
document.getElementById("liquidity-chart").innerHTML = data.history
|
||||
.map((row) => {
|
||||
const value = Number(row.net_liquidity);
|
||||
const width = (Math.abs(value) / max) * 50;
|
||||
@@ -105,14 +192,12 @@ function renderChart(history) {
|
||||
</div>`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
function renderInfra(data) {
|
||||
const domains = data.infrastructure.domains?.domains ?? [];
|
||||
const servers = data.infrastructure.virtual_servers?.servers ?? [];
|
||||
const stripe = data.infrastructure.stripe?.membership ?? {};
|
||||
const payout = data.infrastructure.stripe?.payout_account ?? "payout";
|
||||
const items = [
|
||||
const infraItems = [
|
||||
...domains.map(
|
||||
(d) =>
|
||||
`<wn-field-row label="${d.name}" narrow><span class="wn-field-row__value">${d.monthly_eur} EUR/mo · ${d.tld}</span></wn-field-row>`
|
||||
@@ -123,10 +208,8 @@ function renderInfra(data) {
|
||||
),
|
||||
`<wn-field-row label="Stripe · ${stripe.member_username ?? "member"}" narrow><span class="wn-field-row__value">${stripe.gross_monthly_eur} gross · ${stripe.fee_monthly_eur} fee · ${stripe.net_payout_monthly_eur} net to ${payout}</span></wn-field-row>`,
|
||||
];
|
||||
document.getElementById("infra-stack").innerHTML = `<div class="obs-infra-list">${items.join("")}</div>`;
|
||||
}
|
||||
document.getElementById("infra-stack").innerHTML = `<div class="obs-infra-list">${infraItems.join("")}</div>`;
|
||||
|
||||
function renderTables(data) {
|
||||
document.getElementById("history-body").innerHTML = data.history
|
||||
.map(
|
||||
(row) => `
|
||||
@@ -152,6 +235,138 @@ function renderTables(data) {
|
||||
</tr>`
|
||||
)
|
||||
.join("");
|
||||
|
||||
setBadge(document.getElementById("liquidity-badge"), snapshot.liquidity_status);
|
||||
}
|
||||
|
||||
function renderValueRangeBand(low, high, current) {
|
||||
const min = Number(low);
|
||||
const max = Number(high);
|
||||
const cur = Number(current);
|
||||
const span = Math.max(max - min, 0.01);
|
||||
const curPct = Math.max(0, Math.min(100, ((cur - min) / span) * 100));
|
||||
return `
|
||||
<div class="obs-range">
|
||||
<div class="obs-range__labels">
|
||||
<span class="mono">${euro(min)}</span>
|
||||
<span class="mono obs-range__current">${euro(cur)} now</span>
|
||||
<span class="mono">${euro(max)}</span>
|
||||
</div>
|
||||
<div class="obs-range__track">
|
||||
<div class="obs-range__fill" style="width:${curPct}%"></div>
|
||||
<div class="obs-range__marker" style="left:${curPct}%"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderValueRange(data) {
|
||||
const vr = data.value_range;
|
||||
document.getElementById("value-summary").innerHTML = [
|
||||
{
|
||||
label: "Current price",
|
||||
value: euro(vr.current_price_eur),
|
||||
sub: data.cost_floor.active_model_name ?? "—",
|
||||
},
|
||||
{
|
||||
label: "Aggregate band",
|
||||
value: `${euro(vr.aggregate_low_eur)} – ${euro(vr.aggregate_high_eur)}`,
|
||||
sub: `${vr.segments.length} segments`,
|
||||
},
|
||||
{
|
||||
label: "Cost per member",
|
||||
value: euro(vr.cost_per_member_eur),
|
||||
sub: "Cost floor reference",
|
||||
},
|
||||
]
|
||||
.map(
|
||||
(card) => `
|
||||
<wn-card size="sm">
|
||||
<wn-eyebrow slot="header">${card.label}</wn-eyebrow>
|
||||
<div class="obs-metric__value obs-metric__value--sm">${card.value}</div>
|
||||
<span slot="footer">${card.sub}</span>
|
||||
</wn-card>`
|
||||
)
|
||||
.join("");
|
||||
|
||||
document.getElementById("value-range-notes").textContent = vr.notes || "";
|
||||
document.getElementById("value-segments").innerHTML = vr.segments
|
||||
.map(
|
||||
(segment) => `
|
||||
<wn-card>
|
||||
<wn-eyebrow slot="header">${segment.name}</wn-eyebrow>
|
||||
<wn-tag slot="header">${segment.confidence}</wn-tag>
|
||||
${renderValueRangeBand(segment.low_eur, segment.high_eur, vr.current_price_eur)}
|
||||
<p class="small">${segment.evidence}</p>
|
||||
<span slot="footer">Headroom ${euro(segment.headroom_to_high_eur)}</span>
|
||||
<span slot="footer">${segment.drivers.join(" · ")}</span>
|
||||
</wn-card>`
|
||||
)
|
||||
.join("");
|
||||
|
||||
document.getElementById("value-drivers").innerHTML = vr.value_drivers
|
||||
.map(
|
||||
(driver) => `
|
||||
<wn-field-row label="${driver.label}" narrow>
|
||||
<span class="wn-field-row__value">${driver.note}</span>
|
||||
<span class="wn-field-row__aside">${driver.strength}</span>
|
||||
</wn-field-row>`
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function renderMarketPrice(data) {
|
||||
const market = data.market_price;
|
||||
const span =
|
||||
market.market_low_eur != null && market.market_high_eur != null
|
||||
? `${euro(market.market_low_eur)} – ${euro(market.market_high_eur)}`
|
||||
: "—";
|
||||
|
||||
document.getElementById("market-summary").innerHTML = [
|
||||
{
|
||||
label: "Alternatives tracked",
|
||||
value: market.alternative_count,
|
||||
sub: `${market.priced_alternative_count} with public monthly price`,
|
||||
},
|
||||
{
|
||||
label: "Priced span",
|
||||
value: span,
|
||||
sub: `Reviewed ${market.last_reviewed ?? "—"}`,
|
||||
},
|
||||
{
|
||||
label: "Coulomb list price",
|
||||
value: euro(data.value_range.current_price_eur),
|
||||
sub: data.cost_floor.active_model_name ?? "—",
|
||||
},
|
||||
]
|
||||
.map(
|
||||
(card) => `
|
||||
<wn-card size="sm">
|
||||
<wn-eyebrow slot="header">${card.label}</wn-eyebrow>
|
||||
<div class="obs-metric__value obs-metric__value--sm">${card.value}</div>
|
||||
<span slot="footer">${card.sub}</span>
|
||||
</wn-card>`
|
||||
)
|
||||
.join("");
|
||||
|
||||
document.getElementById("market-notes").textContent = market.notes || "";
|
||||
document.getElementById("market-body").innerHTML = market.alternatives
|
||||
.map((alt) => {
|
||||
const price =
|
||||
alt.price_monthly_eur && Number(alt.price_monthly_eur) > 0
|
||||
? euro(alt.price_monthly_eur)
|
||||
: "—";
|
||||
const features = (alt.features ?? []).join(", ");
|
||||
return `
|
||||
<tr>
|
||||
<td><strong>${alt.name}</strong><div class="small mono">${alt.id}</div></td>
|
||||
<td><wn-tag>${alt.category}</wn-tag></td>
|
||||
<td class="obs-num mono">${price}</td>
|
||||
<td><wn-stage-dot level="${alt.signal_level}">${alt.signal_level}</wn-stage-dot></td>
|
||||
<td>${features}</td>
|
||||
<td class="small">${alt.source} · ${alt.observed}<br>${alt.note}</td>
|
||||
</tr>`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
function populatePeriods(history, current) {
|
||||
@@ -182,19 +397,18 @@ async function loadDesignRefLabel() {
|
||||
}
|
||||
|
||||
function render(data) {
|
||||
currentData = normalizeData(data);
|
||||
document.getElementById("design-link").href = data.design_reference;
|
||||
loadDesignRefLabel();
|
||||
const header = document.getElementById("page-header");
|
||||
header.lede = `Period ${data.period}. Ledger-backed view of infrastructure spend, member payments, and remaining budget. Customer cost-pass-through billing is not active.`;
|
||||
setBadge(document.getElementById("liquidity-badge"), data.snapshot.liquidity_status);
|
||||
renderMetrics(data);
|
||||
renderBudget(data);
|
||||
renderChart(data.history);
|
||||
renderInfra(data);
|
||||
renderTables(data);
|
||||
setSection(activeSection);
|
||||
renderCostFloor(data);
|
||||
renderValueRange(data);
|
||||
renderMarketPrice(data);
|
||||
populatePeriods(data.history, data.period);
|
||||
}
|
||||
|
||||
bindNavigation();
|
||||
|
||||
loadDashboard()
|
||||
.then(render)
|
||||
.catch((error) => {
|
||||
|
||||
@@ -26,109 +26,182 @@
|
||||
<wn-tag slot="right" id="liquidity-badge">—</wn-tag>
|
||||
</wn-top-nav>
|
||||
|
||||
<main class="wn-main obs-main">
|
||||
<wn-page-header
|
||||
eyebrow="Economic Observatory · MVP"
|
||||
title="Operator liquidity"
|
||||
lede="Ledger-backed view of infrastructure spend, member payments, and remaining budget. Customer cost-pass-through billing is not active."
|
||||
id="page-header"
|
||||
></wn-page-header>
|
||||
<div class="wn-app obs-app">
|
||||
<wn-sidebar id="obs-sidebar">
|
||||
<wn-sidebar-group label="Observatory">
|
||||
<wn-sidebar-item icon="activity" active data-section="cost-floor" id="nav-cost-floor">
|
||||
Cost Floor
|
||||
</wn-sidebar-item>
|
||||
<wn-sidebar-item icon="lightbulb" data-section="value-range" id="nav-value-range">
|
||||
Value Range
|
||||
</wn-sidebar-item>
|
||||
<wn-sidebar-item icon="users" data-section="market-price" id="nav-market-price">
|
||||
Market Price
|
||||
</wn-sidebar-item>
|
||||
</wn-sidebar-group>
|
||||
</wn-sidebar>
|
||||
|
||||
<section class="obs-section">
|
||||
<div class="obs-section__head">
|
||||
<wn-eyebrow>Snapshot</wn-eyebrow>
|
||||
<div class="obs-section__rule"></div>
|
||||
</div>
|
||||
<div class="obs-metric-grid" id="metric-grid"></div>
|
||||
</section>
|
||||
<main class="wn-main obs-main">
|
||||
<wn-page-header id="page-header"></wn-page-header>
|
||||
|
||||
<section class="obs-section">
|
||||
<div class="obs-section__head">
|
||||
<wn-eyebrow>Liquidity & budget</wn-eyebrow>
|
||||
<div class="obs-section__rule"></div>
|
||||
</div>
|
||||
<wn-banner variant="info" title="Budget position" id="budget-banner">
|
||||
<p id="budget-caption">—</p>
|
||||
</wn-banner>
|
||||
<div class="obs-budget-meter" aria-hidden="true">
|
||||
<div class="obs-budget-meter__fill" id="budget-fill"></div>
|
||||
</div>
|
||||
<div class="obs-field-sheet" id="budget-stats"></div>
|
||||
</section>
|
||||
<section class="obs-panel obs-panel--active" id="panel-cost-floor" data-section="cost-floor">
|
||||
<section class="obs-section">
|
||||
<div class="obs-section__head">
|
||||
<wn-eyebrow>Snapshot</wn-eyebrow>
|
||||
<div class="obs-section__rule"></div>
|
||||
</div>
|
||||
<div class="obs-metric-grid" id="metric-grid"></div>
|
||||
</section>
|
||||
|
||||
<div class="obs-split">
|
||||
<section class="obs-section">
|
||||
<div class="obs-section__head">
|
||||
<wn-eyebrow>Monthly net liquidity</wn-eyebrow>
|
||||
<div class="obs-section__rule"></div>
|
||||
<section class="obs-section">
|
||||
<div class="obs-section__head">
|
||||
<wn-eyebrow>Liquidity & budget</wn-eyebrow>
|
||||
<div class="obs-section__rule"></div>
|
||||
</div>
|
||||
<wn-banner variant="info" title="Budget position" id="budget-banner">
|
||||
<p id="budget-caption">—</p>
|
||||
</wn-banner>
|
||||
<div class="obs-budget-meter" aria-hidden="true">
|
||||
<div class="obs-budget-meter__fill" id="budget-fill"></div>
|
||||
</div>
|
||||
<div class="obs-field-sheet" id="budget-stats"></div>
|
||||
</section>
|
||||
|
||||
<div class="obs-split">
|
||||
<section class="obs-section">
|
||||
<div class="obs-section__head">
|
||||
<wn-eyebrow>Monthly net liquidity</wn-eyebrow>
|
||||
<div class="obs-section__rule"></div>
|
||||
</div>
|
||||
<p class="lead obs-section-note">Member net payments minus infrastructure, by period.</p>
|
||||
<div class="obs-liquidity-list" id="liquidity-chart"></div>
|
||||
</section>
|
||||
|
||||
<section class="obs-section">
|
||||
<div class="obs-section__head">
|
||||
<wn-eyebrow>Infrastructure stack</wn-eyebrow>
|
||||
<div class="obs-section__rule"></div>
|
||||
</div>
|
||||
<p class="lead obs-section-note">Domains, hosting, and Stripe reference rates.</p>
|
||||
<div id="infra-stack"></div>
|
||||
</section>
|
||||
</div>
|
||||
<p class="lead obs-section-note">Member net payments minus infrastructure, by period.</p>
|
||||
<div class="obs-liquidity-list" id="liquidity-chart"></div>
|
||||
|
||||
<section class="obs-section">
|
||||
<div class="obs-section__head">
|
||||
<wn-eyebrow>Monthly ledger</wn-eyebrow>
|
||||
<div class="obs-section__rule"></div>
|
||||
</div>
|
||||
<p class="lead obs-section-note">Computed from expense and payment record tables.</p>
|
||||
<div class="obs-table-wrap">
|
||||
<table class="wn-table--native obs-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Period</th>
|
||||
<th>Members</th>
|
||||
<th class="obs-num">Gross</th>
|
||||
<th class="obs-num">Infrastructure</th>
|
||||
<th class="obs-num">Processing</th>
|
||||
<th class="obs-num">Net liquidity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="history-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="obs-section">
|
||||
<div class="obs-section__head">
|
||||
<wn-eyebrow>Pricing model registry</wn-eyebrow>
|
||||
<div class="obs-section__rule"></div>
|
||||
</div>
|
||||
<div class="obs-table-wrap">
|
||||
<table class="wn-table--native obs-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="pricing-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class="obs-section">
|
||||
<div class="obs-section__head">
|
||||
<wn-eyebrow>Infrastructure stack</wn-eyebrow>
|
||||
<div class="obs-section__rule"></div>
|
||||
</div>
|
||||
<p class="lead obs-section-note">Domains, hosting, and Stripe reference rates.</p>
|
||||
<div id="infra-stack"></div>
|
||||
<section class="obs-panel" id="panel-value-range" data-section="value-range" hidden>
|
||||
<section class="obs-section">
|
||||
<div class="obs-section__head">
|
||||
<wn-eyebrow>Current position</wn-eyebrow>
|
||||
<div class="obs-section__rule"></div>
|
||||
</div>
|
||||
<div class="obs-metric-grid" id="value-summary"></div>
|
||||
</section>
|
||||
|
||||
<section class="obs-section">
|
||||
<div class="obs-section__head">
|
||||
<wn-eyebrow>Segment bands</wn-eyebrow>
|
||||
<div class="obs-section__rule"></div>
|
||||
</div>
|
||||
<p class="lead obs-section-note" id="value-range-notes"></p>
|
||||
<div class="obs-value-grid" id="value-segments"></div>
|
||||
</section>
|
||||
|
||||
<section class="obs-section">
|
||||
<div class="obs-section__head">
|
||||
<wn-eyebrow>Value drivers</wn-eyebrow>
|
||||
<div class="obs-section__rule"></div>
|
||||
</div>
|
||||
<div class="obs-field-sheet" id="value-drivers"></div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="obs-section">
|
||||
<div class="obs-section__head">
|
||||
<wn-eyebrow>Monthly ledger</wn-eyebrow>
|
||||
<div class="obs-section__rule"></div>
|
||||
</div>
|
||||
<p class="lead obs-section-note">Computed from expense and payment record tables.</p>
|
||||
<div class="obs-table-wrap">
|
||||
<table class="wn-table--native obs-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Period</th>
|
||||
<th>Members</th>
|
||||
<th class="obs-num">Gross</th>
|
||||
<th class="obs-num">Infrastructure</th>
|
||||
<th class="obs-num">Processing</th>
|
||||
<th class="obs-num">Net liquidity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="history-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<section class="obs-panel" id="panel-market-price" data-section="market-price" hidden>
|
||||
<section class="obs-section">
|
||||
<div class="obs-section__head">
|
||||
<wn-eyebrow>Market span</wn-eyebrow>
|
||||
<div class="obs-section__rule"></div>
|
||||
</div>
|
||||
<div class="obs-metric-grid" id="market-summary"></div>
|
||||
</section>
|
||||
|
||||
<section class="obs-section">
|
||||
<div class="obs-section__head">
|
||||
<wn-eyebrow>Pricing model registry</wn-eyebrow>
|
||||
<div class="obs-section__rule"></div>
|
||||
</div>
|
||||
<div class="obs-table-wrap">
|
||||
<table class="wn-table--native obs-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="pricing-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<section class="obs-section">
|
||||
<div class="obs-section__head">
|
||||
<wn-eyebrow>Competitive alternatives</wn-eyebrow>
|
||||
<div class="obs-section__rule"></div>
|
||||
</div>
|
||||
<p class="lead obs-section-note" id="market-notes"></p>
|
||||
<div class="obs-table-wrap">
|
||||
<table class="wn-table--native obs-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Alternative</th>
|
||||
<th>Category</th>
|
||||
<th class="obs-num">Price / mo</th>
|
||||
<th>Signal</th>
|
||||
<th>Features</th>
|
||||
<th>Evidence</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="market-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<footer class="obs-footer">
|
||||
<p class="small">
|
||||
Design:
|
||||
<a id="design-link" href="#" target="_blank" rel="noreferrer">Claude design share</a>
|
||||
·
|
||||
<span class="mono" id="design-ref-label">whynot-design</span>
|
||||
· totals computed programmatically from ledgers
|
||||
</p>
|
||||
</footer>
|
||||
</main>
|
||||
<footer class="obs-footer">
|
||||
<p class="small">
|
||||
Design:
|
||||
<a id="design-link" href="#" target="_blank" rel="noreferrer">Claude design share</a>
|
||||
·
|
||||
<span class="mono" id="design-ref-label">whynot-design</span>
|
||||
· totals computed programmatically from ledgers
|
||||
</p>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/ui/app.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -4,10 +4,70 @@ body {
|
||||
background: var(--paper);
|
||||
}
|
||||
|
||||
.obs-app {
|
||||
min-height: calc(100vh - 56px);
|
||||
}
|
||||
|
||||
.obs-main {
|
||||
max-width: 1080px;
|
||||
margin: 0 auto;
|
||||
padding: var(--sp-6) var(--sp-5) var(--sp-9);
|
||||
max-width: 960px;
|
||||
}
|
||||
|
||||
.obs-panel[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
wn-sidebar-item[data-section] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.obs-metric__value--sm {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.obs-value-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: var(--sp-3);
|
||||
}
|
||||
|
||||
.obs-range {
|
||||
display: grid;
|
||||
gap: var(--sp-2);
|
||||
}
|
||||
|
||||
.obs-range__labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--sp-2);
|
||||
font-size: 12px;
|
||||
color: var(--fg-3);
|
||||
}
|
||||
|
||||
.obs-range__current {
|
||||
color: var(--fg-1);
|
||||
}
|
||||
|
||||
.obs-range__track {
|
||||
position: relative;
|
||||
height: 10px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--paper-2);
|
||||
}
|
||||
|
||||
.obs-range__fill {
|
||||
height: 100%;
|
||||
background: var(--line-strong);
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.obs-range__marker {
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
width: 2px;
|
||||
height: 16px;
|
||||
background: var(--ink);
|
||||
transform: translateX(-1px);
|
||||
}
|
||||
|
||||
.obs-period {
|
||||
|
||||
Reference in New Issue
Block a user