v0.2.0:11 + v0.2.0:12 — Archive, Settings, agent surface, machines redesign
Two release cycles prepared together: v0.2.0:11 (policy archive + safe- delete cleanup + brand-consistent confirm modals) and v0.2.0:12 (Settings tab + agent-friendly operator API + machines tab redesign + buyer-facing copy alignment). Highlights: - Migration 0015: policies.archived_at column. Archive button on tier cards; safe-delete relaxed to ignore revoked-license tombstones; renewal worker refuses archived policies. - Migration 0016: scoped_api_keys table. Four roles (read-only, license-issuer, support, full-admin) with bounded scopes. Master admin_api_key still works on every endpoint; scoped keys gated on endpoints wired through require_scope(). - New /v1/openapi.json — public, no auth. Curated OpenAPI 3.1 spec for agent / SDK discovery. - New Settings tab: Operator name + Payment providers panel + API keys management. Replaces 8 StartOS Actions (Zaprite all, BTCPay all, operator name, switch-provider). StartOS Actions pruned to 4 install-time essentials. - Machines tab rewritten: global default view grouped by product, filter pills with counts, quick-stats row, drill-down via new "Machines" button on each Licenses-tab row. New repo helper list_machines_admin joins machines x licenses x products server-side. - Branded confirmModal replaces every native window.confirm() call in the admin UI (7 callsites). - Enforce mode killed: KEYSAT_LICENSE_ENFORCE compile-time flag retired; daemon always boots; missing self-license -> Creator (free) tier. "Unlicensed" label gone from admin UI. - Zaprite gated on the new zaprite_payments entitlement (renamed from card_payments to reflect the broader gateway). - Creator code cap 5 -> 10. - KEYSAT_AGENT_GUIDE.md: auth, role-to-scope mapping, error envelope, webhook events, worked recipes. - Buyer-facing copy aligned with new positioning: "Bitcoin-native self-hosted software licensing" everywhere on production surfaces. - Cross-product safety section (Section 9a) added to KEYSAT_INTEGRATION.md. - 5 new API integration smoke tests covering OpenAPI, scoped API keys CRUD, role-elevation guard, and Zaprite-tier gating. Test count: 83 passing (was 78). All migration tests pass against 0015 and 0016 applied to populated DBs.
This commit is contained in:
@@ -236,11 +236,24 @@ pub async fn deactivate(
|
||||
|
||||
// ---------- Admin endpoints ----------
|
||||
|
||||
/// Query for the admin Machines list. All filters are optional and
|
||||
/// conjunctive — leaving them all blank returns every machine across
|
||||
/// every license, default-sorted by most-recent heartbeat. The admin UI
|
||||
/// Machines tab uses this default-no-filter form to render a global
|
||||
/// view; the Licenses-tab drill-down sets `license_id`.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AdminListQuery {
|
||||
pub license_id: String,
|
||||
#[serde(default)]
|
||||
pub license_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub product_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub product_slug: Option<String>,
|
||||
#[serde(default)]
|
||||
pub include_inactive: bool,
|
||||
/// Cap on result size; defaults to 500. Admin UI paginates client-side.
|
||||
#[serde(default)]
|
||||
pub limit: Option<i64>,
|
||||
}
|
||||
|
||||
pub async fn admin_list(
|
||||
@@ -249,11 +262,28 @@ pub async fn admin_list(
|
||||
Query(q): Query<AdminListQuery>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
require_admin(&state, &headers)?;
|
||||
let machines = if q.include_inactive {
|
||||
repo::list_all_machines(&state.db, &q.license_id).await?
|
||||
|
||||
// Resolve product_slug → product_id if the caller passed the slug
|
||||
// form. Either works; product_id takes precedence on conflict.
|
||||
let resolved_product_id: Option<String> = if let Some(pid) = q.product_id.as_deref() {
|
||||
Some(pid.to_string())
|
||||
} else if let Some(slug) = q.product_slug.as_deref() {
|
||||
match repo::get_product_by_slug(&state.db, slug).await? {
|
||||
Some(p) => Some(p.id),
|
||||
None => return Err(AppError::NotFound(format!("product '{slug}'"))),
|
||||
}
|
||||
} else {
|
||||
repo::list_active_machines(&state.db, &q.license_id).await?
|
||||
None
|
||||
};
|
||||
|
||||
let machines = repo::list_machines_admin(
|
||||
&state.db,
|
||||
resolved_product_id.as_deref(),
|
||||
q.license_id.as_deref(),
|
||||
q.include_inactive,
|
||||
q.limit.unwrap_or(500).clamp(1, 5000),
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(json!({ "machines": machines })))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user