From 633cda1a5a1fcf2b9a6ba34ab2a2977d4c63023c Mon Sep 17 00:00:00 2001 From: ericokuma Date: Wed, 18 Feb 2026 10:57:46 -0800 Subject: [PATCH 01/35] Add Claude Code PM skills Co-Authored-By: Claude Sonnet 4.6 --- .../skills/pm/competitive-analysis/SKILL.md | 69 +++++++++++++++ .claude/skills/pm/executive-update/SKILL.md | 60 +++++++++++++ .../skills/pm/feature-prioritization/SKILL.md | 59 +++++++++++++ .claude/skills/pm/feedback-analysis/SKILL.md | 64 ++++++++++++++ .claude/skills/pm/go-to-market/SKILL.md | 77 ++++++++++++++++ .claude/skills/pm/metrics-framework/SKILL.md | 87 +++++++++++++++++++ .claude/skills/pm/okr-writer/SKILL.md | 64 ++++++++++++++ .claude/skills/pm/prd-generator/SKILL.md | 82 +++++++++++++++++ .claude/skills/pm/stakeholder-review/SKILL.md | 61 +++++++++++++ .claude/skills/pm/user-story/SKILL.md | 81 +++++++++++++++++ 10 files changed, 704 insertions(+) create mode 100644 .claude/skills/pm/competitive-analysis/SKILL.md create mode 100644 .claude/skills/pm/executive-update/SKILL.md create mode 100644 .claude/skills/pm/feature-prioritization/SKILL.md create mode 100644 .claude/skills/pm/feedback-analysis/SKILL.md create mode 100644 .claude/skills/pm/go-to-market/SKILL.md create mode 100644 .claude/skills/pm/metrics-framework/SKILL.md create mode 100644 .claude/skills/pm/okr-writer/SKILL.md create mode 100644 .claude/skills/pm/prd-generator/SKILL.md create mode 100644 .claude/skills/pm/stakeholder-review/SKILL.md create mode 100644 .claude/skills/pm/user-story/SKILL.md diff --git a/.claude/skills/pm/competitive-analysis/SKILL.md b/.claude/skills/pm/competitive-analysis/SKILL.md new file mode 100644 index 00000000000..d8c5d22c467 --- /dev/null +++ b/.claude/skills/pm/competitive-analysis/SKILL.md @@ -0,0 +1,69 @@ +--- +description: Conduct a competitive analysis using Gibson Biddle's DHM (Delight customers, Hard to copy, Margin-enhancing) framework, with strategic positioning recommendations +allowed-tools: Read, AskUserQuestion, WebSearch, Task +argument-hint: "" +--- + +Build a comprehensive competitive analysis using Gibson Biddle's DHM framework — researching up to 5 competitors in parallel and synthesizing strategic positioning recommendations. + +Input: $ARGUMENTS + +## Instructions + +### 1. Define the Competitive Landscape + +If competitors aren't specified, ask via `AskUserQuestion`: +- What product or space are we analyzing? +- Who are the top 3–5 competitors (direct and adjacent)? +- What is the primary job-to-be-done we're competing on? + +### 2. Research Competitors in Parallel + +Use the `Task` tool to spawn parallel `Explore` agents — one per competitor — each tasked with finding: +- Product positioning and key messaging +- Pricing model and tiers +- Key features and differentiators +- Known strengths and weaknesses (reviews, press, community feedback) +- Recent product moves (launches, pivots, funding) + +Consolidate findings before proceeding. + +### 3. DHM Analysis + +For each competitor (including your own product), evaluate across the three DHM dimensions: + +**Delight (D)**: What features or experiences genuinely delight customers? What do users love about this product? + +**Hard to Copy (H)**: What aspects of their product, data network, brand, or business model are difficult for a new entrant to replicate? Rate each: Low / Medium / High defensibility. + +**Margin-enhancing (M)**: How does this product structure itself to improve margins over time? (e.g., automation, self-serve, network effects, data flywheel) + +### 4. Feature Comparison Matrix + +Create a table comparing all competitors across key dimensions relevant to the space. Mark: ✅ (has it), ⚠️ (partial), ❌ (missing), 🔄 (in progress/announced). + +### 5. Pricing Comparison + +Document each competitor's pricing structure: +- Free tier (if any) +- Core paid plan +- Enterprise pricing signals +- Pricing model (seat-based, usage-based, flat, hybrid) + +Note where your product is priced relative to the field and whether that positioning is strategic or legacy. + +### 6. Strategic Recommendations + +Synthesize the analysis into actionable recommendations: + +**White spaces**: Jobs-to-be-done that no competitor is excelling at — potential areas to own. + +**Defensive moves**: Where are competitors gaining ground that threatens your core? What should you protect? + +**Positioning sharpening**: Based on the DHM analysis, what is the single most defensible and differentiating position for your product to own? + +**Features to not build**: Capabilities where competitors have too much of a head start — better to partner, acquire, or integrate than build. + +## Output Format + +Structure as a competitive brief with: executive summary, DHM scorecards, feature matrix, pricing table, and strategic recommendations. Use tables extensively. Keep the executive summary to 5 bullets — the kind a CEO would read in 2 minutes. diff --git a/.claude/skills/pm/executive-update/SKILL.md b/.claude/skills/pm/executive-update/SKILL.md new file mode 100644 index 00000000000..e8d82c315d7 --- /dev/null +++ b/.claude/skills/pm/executive-update/SKILL.md @@ -0,0 +1,60 @@ +--- +description: Create a crisp executive update or status report using the SCQA (Situation, Complication, Question, Answer) framework +allowed-tools: Read, AskUserQuestion +argument-hint: "" +--- + +Transform messy context into a crisp, structured executive update using the SCQA framework (Situation, Complication, Question, Answer) — the gold standard for executive communication. + +Input: $ARGUMENTS + +## Instructions + +### 1. Gather Context + +If not provided, ask via `AskUserQuestion`: +- What is the audience? (CEO, board, cross-functional leadership, skip-level) +- What format is expected? (email, Slack update, slide, verbal brief) +- What decision or action, if any, do you need from the audience? +- What has changed since the last update? + +### 2. SCQA Structure + +Write the update using the four-part SCQA framework: + +**Situation** (1–2 sentences) +Set shared context. State what's true right now that the audience already knows or would agree with. This is NOT the problem — it's the stable backdrop. +> Example: "We're 6 weeks into Q2, and our pipeline conversion rate has been a focus area since the Q1 review." + +**Complication** (1–3 sentences) +Introduce the tension. What has changed, gone wrong, or become relevant that makes the situation require attention? This creates the "why are you telling me this" moment. +> Example: "Conversion rate improved from 18% to 22%, but deal velocity slowed — average close time increased from 28 to 41 days, which puts us at risk of missing the Q2 revenue target." + +**Question** (1 sentence, implicit or explicit) +The natural question the reader is now asking. Often not stated explicitly, but naming it sharpens the answer. +> Example: "What's driving the slowdown and what should we do about it?" + +**Answer** (the bulk of the update) +Lead with the recommendation or key insight, then support it with evidence. Structure as: +- **Bottom line up front**: Your conclusion or recommendation in one sentence +- **Supporting data**: 2–3 data points or observations that back the bottom line +- **Next steps**: What will happen next and by when, with owners named + +### 3. Calibrate for Audience + +Adjust the update based on audience: +- **CEO / Board**: Lead with business impact and decisions needed. Skip operational detail. +- **Cross-functional leaders**: Emphasize dependencies and what you need from them. +- **Skip-level**: Include enough operational context to be credible, but still lead with the headline. + +### 4. Editing Pass + +Review the draft against these criteria: +- Can the reader understand the key message in the first 30 seconds? +- Is every sentence earning its place — does it add information or can it be cut? +- Are there any weasel words or passive constructions hiding uncertainty? If there's uncertainty, name it directly. +- Is the ask (if any) unambiguous? + +## Output Format + +Present the final SCQA update in clean prose — no sub-bullets within the Situation or Complication. Reserve bullets for the Answer section's supporting evidence and next steps. Aim for under 250 words for async written formats; include a "TL;DR" one-liner at the top for long updates. diff --git a/.claude/skills/pm/feature-prioritization/SKILL.md b/.claude/skills/pm/feature-prioritization/SKILL.md new file mode 100644 index 00000000000..b73b4a6c321 --- /dev/null +++ b/.claude/skills/pm/feature-prioritization/SKILL.md @@ -0,0 +1,59 @@ +--- +description: Score and rank a list of features or initiatives using RICE, ICE, or weighted scoring frameworks, with clear rationale for each score +allowed-tools: Read, AskUserQuestion +argument-hint: "" +--- + +Turn a backlog of features or initiatives into a clearly ranked, defensible prioritization using RICE, ICE, or weighted scoring — with documented rationale for every score. + +Input: $ARGUMENTS + +## Instructions + +### 1. Choose a Scoring Framework + +Ask the user (via `AskUserQuestion`) which framework to use, or recommend based on context: + +- **RICE** — best when you have data on reach and effort estimates. Formula: `(Reach × Impact × Confidence) / Effort` +- **ICE** — best for fast, gut-check prioritization. Formula: `Impact × Confidence × Ease` +- **Weighted Scoring** — best when specific strategic criteria matter (e.g., strategic alignment, revenue potential, technical debt reduction). Ask the user for their weights. + +### 2. Define Scoring Criteria + +For RICE: +- **Reach**: How many users will this affect per quarter? (estimated number) +- **Impact**: How much will this move the needle per user? (3 = massive, 2 = significant, 1 = low, 0.5 = minimal, 0.25 = trivial) +- **Confidence**: How confident are you in these estimates? (100% = high, 80% = medium, 50% = low) +- **Effort**: Total person-months to build, test, and ship. + +For ICE: +- **Impact**: 1–10 scale. How much will this move the key metric? +- **Confidence**: 1–10 scale. How sure are you it will work? +- **Ease**: 1–10 scale. How easy/fast is this to ship? + +For Weighted Scoring, define 4–6 criteria and assign weights that sum to 100%. + +### 3. Score Each Item + +Create a scoring table for each feature: +- Document the score for each dimension +- Provide a 1-sentence rationale for the score +- Calculate the final score + +### 4. Output a Ranked List + +Present: +1. A **ranked table** (highest score first) with all scores visible +2. A **recommended top 3** to ship next, with brief justification +3. **Items to defer or cut**, with the reason why + +### 5. Flag Risks and Dependencies + +For top-ranked items, note: +- Dependencies on other teams or systems +- Technical or business risks that could invalidate the score +- Any items that scored low but have strategic override reasons (e.g., compliance, exec mandate) + +## Output Format + +Use a markdown table for the scoring matrix. Follow with a short narrative summary of the recommendations. Avoid jargon — the output should be presentable directly to stakeholders. diff --git a/.claude/skills/pm/feedback-analysis/SKILL.md b/.claude/skills/pm/feedback-analysis/SKILL.md new file mode 100644 index 00000000000..345b522e226 --- /dev/null +++ b/.claude/skills/pm/feedback-analysis/SKILL.md @@ -0,0 +1,64 @@ +--- +description: Analyze a batch of customer feedback (support tickets, NPS responses, reviews, feature requests) to extract patterns, prioritize opportunities, and surface actionable insights +allowed-tools: Read, Glob, AskUserQuestion +argument-hint: "" +--- + +Analyze 10–500+ customer feedback items from any source — support tickets, NPS verbatims, G2/Capterra reviews, feature requests, user interviews — and extract actionable patterns ranked by frequency and impact. + +Input: $ARGUMENTS + +## Instructions + +### 1. Ingest the Feedback + +Accept feedback in any of these forms: +- A file path (use `Read` tool) +- Pasted text in the conversation +- A description of feedback themes (if raw data isn't available) + +If the feedback is unstructured, ask via `AskUserQuestion`: +- What is the source? (NPS, support, reviews, interviews, sales calls) +- What time period does this cover? +- What product area or feature set does this relate to? + +### 2. Categorize Each Item + +For each piece of feedback, identify: +- **Theme**: The underlying need or pain point (not the surface request) +- **Sentiment**: Positive / Negative / Neutral +- **Urgency signal**: Does the user describe churn risk, workaround behavior, or blocking frustration? +- **User segment** (if inferrable): Power user, new user, enterprise, SMB, etc. + +### 3. Extract Patterns + +Group feedback items by theme and count frequency. Look for: +- **High-frequency pain points**: Themes that appear in 10%+ of items +- **High-severity items**: Individual feedback pieces that signal churn, safety, or legal risk — even if rare +- **Unmet jobs**: Things users are trying to do that the product doesn't support well +- **Unexpected use cases**: Users using the product in ways you didn't anticipate — often signals new market opportunities +- **Positive signals**: What users love — protect these from inadvertent regression in roadmap decisions + +### 4. Prioritize Opportunities + +Rank the top themes using a simple 2×2: +- **X-axis**: Frequency (how many users mentioned this) +- **Y-axis**: Severity (how much does this hurt the user or the business) + +Quadrant labels: +- High frequency + High severity → **Urgent: Fix or build now** +- Low frequency + High severity → **Risk: Monitor and triage** +- High frequency + Low severity → **Polish: Quick wins** +- Low frequency + Low severity → **Backlog: Low priority** + +### 5. Surface Insights + +Write a synthesis covering: +- **Top 3 opportunities** with supporting evidence (quote 2–3 representative pieces of feedback for each) +- **One thing to stop doing** (if the data suggests a feature or behavior is causing net harm) +- **One underserved segment** that shows up in the data with distinct needs +- **Recommended next step**: What should the PM do with this analysis? (user interviews, prototype test, quick fix, escalate to leadership, etc.) + +## Output Format + +Lead with a short executive summary (5 sentences max). Follow with the opportunity ranking table. Then provide detailed write-ups for the top 3 opportunities, each with representative quotes. End with recommendations. Preserve customer voice — use direct quotes rather than paraphrasing whenever possible. diff --git a/.claude/skills/pm/go-to-market/SKILL.md b/.claude/skills/pm/go-to-market/SKILL.md new file mode 100644 index 00000000000..b24f5915a2e --- /dev/null +++ b/.claude/skills/pm/go-to-market/SKILL.md @@ -0,0 +1,77 @@ +--- +description: Create a complete Go-to-Market (GTM) plan for a product, feature, or launch — including positioning, channels, messaging, timeline, and success metrics +allowed-tools: Read, AskUserQuestion, WebSearch +argument-hint: "" +--- + +Build a complete Go-to-Market plan covering positioning, audience targeting, channel strategy, messaging, launch timeline, and success metrics. + +Input: $ARGUMENTS + +## Instructions + +### 1. Gather Launch Context + +If not provided, use `AskUserQuestion` to ask: +- What are we launching? (new product, feature, pricing change, expansion) +- Who is the primary target audience? +- What is the launch date or target window? +- What are the top 1–2 business goals for this launch? +- Any known constraints (budget, team, regions, existing commitments)? + +### 2. Positioning Statement + +Write a positioning statement using the Geoffrey Moore format: + +> For [target customer] who [has this need or problem], [product/feature name] is a [category] that [key benefit]. Unlike [primary alternative], our solution [key differentiator]. + +Test the positioning against these questions: +- Is the "unlike" contrast meaningful to the customer (not just internally)? +- Does the key benefit tie directly to a measurable outcome? +- Is the category framing right — too narrow (limits growth) or too broad (loses clarity)? + +### 3. Audience Segmentation + +Define: +- **Primary audience**: Who this launch is designed for first. Be specific. +- **Secondary audience**: Who else benefits, and how messaging differs for them. +- **Influencers and champions**: Who drives adoption within a company or community? + +### 4. Messaging Framework + +Create a messaging hierarchy: +- **One-liner** (10 words max): Used in headlines, subject lines, social +- **Elevator pitch** (2–3 sentences): Used in sales, demos, outbound +- **Full value proposition** (1 paragraph): Used in landing pages, press materials + +For each audience segment, note any messaging adjustments. + +### 5. Channel Strategy + +For each channel, specify: goal, tactic, owner, and expected impact. + +Evaluate which channels to use based on where the audience lives: +- **Owned**: Blog, in-app notifications, email newsletter, changelog +- **Earned**: Press, analyst briefings, community word-of-mouth, influencer reviews +- **Paid**: Ads, sponsored content, events +- **Partner**: Co-marketing, integrations announcements, referrals + +### 6. Launch Timeline + +Create a phased timeline: +- **T-4 weeks**: Internal alignment, sales enablement, beta/preview customers +- **T-2 weeks**: Soft launch to waitlist or early adopters, collect feedback +- **T-0 (Launch day)**: Public announcement, coordinated across all channels +- **T+2 weeks**: Follow-up content, case studies, retargeting + +### 7. Success Metrics + +Define: +- **Primary metric**: The one number that signals launch success +- **Leading indicators**: What to watch in the first 48–72 hours +- **Lagging indicators**: What to evaluate at 30/60/90 days +- **Failure signal**: At what threshold do we escalate or pivot? + +## Output Format + +Structure the output as a standalone GTM brief in clean markdown — concise enough to share with leadership, detailed enough for the team to execute from. Use tables for the channel strategy and timeline. diff --git a/.claude/skills/pm/metrics-framework/SKILL.md b/.claude/skills/pm/metrics-framework/SKILL.md new file mode 100644 index 00000000000..d1807d74bb0 --- /dev/null +++ b/.claude/skills/pm/metrics-framework/SKILL.md @@ -0,0 +1,87 @@ +--- +description: Build a comprehensive product metrics framework using AARRR (pirate metrics) or input/output methodology, and identify and validate your product's North Star metric +allowed-tools: Read, AskUserQuestion +argument-hint: "" +--- + +Design a complete metrics framework for a product or feature — identifying the North Star metric, building out the full AARRR funnel or input/output model, and defining leading and lagging indicators at each stage. + +Input: $ARGUMENTS + +## Instructions + +### 1. Understand the Product Context + +If not provided, ask via `AskUserQuestion`: +- What does the product do and who uses it? +- What is the primary business model? (subscription, usage-based, marketplace, freemium, etc.) +- What stage is the product at? (early/PMF search, growth, scale, mature) +- Is there an existing set of metrics in use? If so, what are they? + +### 2. North Star Metric + +Identify the North Star metric: the single number that best captures the core value the product delivers to customers. A good North Star: +- Reflects customer value (not just revenue or vanity activity) +- Is leading, not lagging (predicts future business health) +- Is actionable — the team can affect it directly +- Is understandable — a new employee can explain it + +Present 2–3 candidate North Star metrics and recommend one with reasoning. + +**Common examples by business type**: +- SaaS productivity tool → Weekly active users completing core workflow +- Marketplace → Successful transactions per month +- Consumer app → DAU/MAU ratio (stickiness) +- Data platform → Queries run per active user per week + +Validate the North Star by testing: "If this metric goes up while everything else stays the same, is the business actually healthier?" + +### 3. AARRR Funnel Metrics + +Map the full customer lifecycle with 2–3 metrics per stage: + +**Acquisition** — How do users discover and arrive? +- Sources: Organic search, paid, referral, direct, partner +- Key metrics: New signups, CAC by channel, trial starts + +**Activation** — Do users experience core value quickly? +- Define the "aha moment" — the action that correlates most strongly with retention +- Key metrics: Activation rate (% reaching aha moment), time-to-value + +**Retention** — Do users keep coming back? +- Define retention for this product (daily, weekly, monthly depending on use case) +- Key metrics: D7/D30/D90 retention, churn rate, resurrection rate + +**Referral** — Do users tell others? +- Key metrics: NPS, viral coefficient (K-factor), referral rate, word-of-mouth signups + +**Revenue** — Are we monetizing effectively? +- Key metrics: MRR/ARR, ARPU, LTV, LTV:CAC ratio, expansion revenue + +### 4. Input/Output Metric Pairing + +For each AARRR stage, pair: +- **Output metric**: The outcome you're trying to achieve (lagging, harder to move quickly) +- **Input metric**: The leading indicator or lever the team can pull to affect the output + +Example: +> Output: D30 retention = 40% | Input: % of new users who complete onboarding within 24 hours + +### 5. Instrumentation Checklist + +For each key metric, document: +- What event or data point needs to be tracked? +- Where does this data live today? (product analytics, data warehouse, CRM, etc.) +- Is this metric currently being measured? If not, what's needed to instrument it? +- What's the current baseline value (if known)? + +### 6. Anti-metrics + +Name 2–3 metrics to explicitly NOT optimize for — things that could improve without the product actually getting better: +- Vanity metrics (e.g., total registered users without activity filter) +- Metrics that can be gamed easily +- Metrics that could be optimized at the expense of user trust + +## Output Format + +Present the North Star recommendation with rationale, then a metrics table organized by AARRR stage with input/output pairs. Follow with the instrumentation checklist. Use a clean table format throughout. Keep the narrative tight — this should be a working document, not a strategy deck. diff --git a/.claude/skills/pm/okr-writer/SKILL.md b/.claude/skills/pm/okr-writer/SKILL.md new file mode 100644 index 00000000000..992d5c38f1a --- /dev/null +++ b/.claude/skills/pm/okr-writer/SKILL.md @@ -0,0 +1,64 @@ +--- +description: Write and refine Objectives and Key Results (OKRs) using the Measure What Matters methodology, with coaching on common pitfalls +allowed-tools: Read, AskUserQuestion +argument-hint: "" +--- + +Write strong OKRs using John Doerr's *Measure What Matters* methodology — or critique and improve a draft set of OKRs the user provides. + +Input: $ARGUMENTS + +## Instructions + +### 1. Understand Context + +If not provided, ask via `AskUserQuestion`: +- Is this for a company, team, or individual? +- What time period? (quarterly is standard) +- What is the overarching company/product goal this OKR should ladder up to? +- Are these new OKRs or a draft to review? + +### 2. OKR Fundamentals (apply throughout) + +**Objectives** must be: +- Inspirational and qualitative — describe a destination, not a task +- Ambitious but achievable ("moonshot" vs. "roofshot" — label which) +- Memorable in a single sentence +- NOT a metric (no numbers in the objective) + +**Key Results** must be: +- Measurable and time-bound +- Outcome-oriented (measure impact, not output or activity) +- 3–5 per objective +- Scored 0–1.0 at the end of the period (0.7 = success; 1.0 = sandbagging) + +### 3. Common Pitfalls to Check Against + +Flag any of the following: +- **Task masquerading as KR**: "Launch feature X" is output, not outcome. Rewrite as "Feature X drives 20% increase in Y by [date]." +- **Vanity metric**: Metrics that look good but don't signal real value (e.g., page views without conversion) +- **Too safe**: If hitting 1.0 seems easy, push the number higher +- **No owner**: Each KR should have a clear DRI (directly responsible individual) +- **Too many**: More than 3 objectives or 5 KRs per objective dilutes focus + +### 4. Write or Rewrite OKRs + +For each Objective: +``` +Objective: [Inspiring, qualitative goal] + KR1: [Metric] from [baseline] to [target] by [date] + KR2: [Metric] from [baseline] to [target] by [date] + KR3: [Metric] from [baseline] to [target] by [date] +``` + +### 5. Health Check + +After drafting, evaluate each OKR set: +- Does each KR, if achieved, make the objective undeniably true? +- Are any two KRs measuring the same thing? Consolidate if so. +- Is there a leading indicator KR (early signal) alongside lagging KRs? +- Are the KRs collectively sufficient to achieve the objective, or is something missing? + +## Output Format + +Present final OKRs in clean markdown. For each KR, add a one-line note explaining why this metric was chosen. If reviewing a draft, show the original vs. the improved version side by side. diff --git a/.claude/skills/pm/prd-generator/SKILL.md b/.claude/skills/pm/prd-generator/SKILL.md new file mode 100644 index 00000000000..8f144720fb2 --- /dev/null +++ b/.claude/skills/pm/prd-generator/SKILL.md @@ -0,0 +1,82 @@ +--- +description: Generate a complete Product Requirements Document (PRD) from a problem statement or feature idea, using JTBD analysis, opportunity trees, and sprint-ready user stories +allowed-tools: Read, Glob, Grep, AskUserQuestion, WebSearch +argument-hint: "" +--- + +Transform a problem statement or feature idea into an engineering-ready PRD using Jobs-to-be-Done analysis, opportunity mapping, and sprint-ready user stories. + +Input: $ARGUMENTS + +## Instructions + +### 1. Clarify Scope (if input is vague) + +If the input lacks sufficient detail, use `AskUserQuestion` to ask: +- Who is the primary user/customer affected? +- What outcome are we trying to achieve (not the solution)? +- What constraints exist (timeline, platform, team size)? + +### 2. JTBD Analysis + +Frame the problem using the Jobs-to-be-Done format: + +> **When** [situation], **I want to** [motivation/job], **so I can** [expected outcome]. + +Identify: +- **Functional job**: The practical task the user is trying to accomplish +- **Emotional job**: How they want to feel +- **Social job**: How they want to be perceived + +### 3. Opportunity Tree + +Map the problem space before jumping to solutions: + +``` +Goal: [Desired outcome] + └── Opportunity: [Why users can't achieve the goal today] + ├── Assumption: [What must be true for this opportunity to be real] + └── Solution Space: [2–3 possible approaches — do NOT commit to one yet] +``` + +### 4. PRD Structure + +Write the full PRD with the following sections: + +**Overview** +- Problem statement (1–2 sentences) +- Why now (business context, urgency) +- Success metrics (primary KPI + 1–2 supporting metrics) + +**Users & Segments** +- Primary user persona +- Secondary users (if any) +- Out of scope users + +**Requirements** +- P0 (must have for launch) +- P1 (strongly preferred) +- P2 (nice to have, post-launch) + +For each requirement, state: what the system must do, not how. + +**Non-Requirements** +- Explicitly call out what this PRD does NOT cover to prevent scope creep. + +**Open Questions** +- List any unresolved decisions that need stakeholder input before engineering starts. + +### 5. Sprint-Ready User Stories + +Convert P0 requirements into user stories using the format: + +> **As a** [user type], **I want to** [action], **so that** [benefit]. + +For each story, include: +- **Acceptance criteria** (3–5 bullet points using Given/When/Then) +- **Story points estimate** (1, 2, 3, 5, or 8 — Fibonacci scale) +- **Dependencies** (other stories or systems this relies on) + +## Output Format + +Use clear markdown with headers. Keep the PRD scannable — PMs and engineers should be able to grasp the full scope in under 5 minutes. Avoid padding; every sentence should add information. diff --git a/.claude/skills/pm/stakeholder-review/SKILL.md b/.claude/skills/pm/stakeholder-review/SKILL.md new file mode 100644 index 00000000000..86034a88a98 --- /dev/null +++ b/.claude/skills/pm/stakeholder-review/SKILL.md @@ -0,0 +1,61 @@ +--- +description: Run 7 stakeholder perspectives on any document or decision in parallel — Engineering, Design, Executive, Legal, Customer, Devil's Advocate, and Sales — then synthesize consensus, tensions, blockers, and questions +allowed-tools: Read, AskUserQuestion, Task +argument-hint: "" +--- + +Stress-test any document or decision by simulating 7 distinct stakeholder perspectives in parallel, then synthesizing the results into a unified review with consensus, tensions, blockers, and key questions to prepare for. + +Input: $ARGUMENTS + +## Instructions + +### 1. Read and Understand the Input + +Read the provided document thoroughly. If it's a file path, use the `Read` tool. Identify: +- What type of document is this? (PRD, proposal, strategy doc, roadmap, design brief, etc.) +- What decision or approval is being sought? +- What is the timeline for feedback? + +### 2. Run 7 Parallel Stakeholder Reviews + +Use the `Task` tool to spawn 7 parallel agents simultaneously (in a single message), each reviewing the document from a distinct perspective: + +**Agent 1 — Engineering Lead** +Prompt: Review this document as a senior engineering lead. Identify: technical feasibility concerns, missing technical requirements, hidden complexity, system dependencies, and questions you'd ask before starting work. Be specific and practical. + +**Agent 2 — Product Designer / UX** +Prompt: Review this document as a product designer. Identify: user experience gaps, missing user research or validation, design assumptions that need testing, accessibility considerations, and any flows that will be confusing to users. + +**Agent 3 — Executive / Business** +Prompt: Review this document as a C-suite executive. Identify: strategic alignment, ROI clarity, resource implications, risks to company goals, and what's missing that you'd need to approve this. + +**Agent 4 — Legal / Compliance** +Prompt: Review this document as a legal and compliance reviewer. Identify: privacy concerns (data collection, GDPR/CCPA), terms of service implications, regulatory risks, IP considerations, and liability exposure. + +**Agent 5 — Customer Voice** +Prompt: Review this document as an advocate for the end customer. Identify: assumptions about user needs that aren't validated, potential for confusion or frustration, missing use cases, and whether the proposed solution actually solves the stated problem. + +**Agent 6 — Devil's Advocate** +Prompt: Review this document as a skeptic whose job is to find every flaw. What's the strongest case against doing this? What could go wrong? What assumptions are most likely to be wrong? What would you need to be convinced this is worth doing? + +**Agent 7 — Sales / Revenue** +Prompt: Review this document as a sales leader. Identify: how this affects current deals, the sales narrative and messaging implications, pricing or packaging concerns, competitive positioning, and what you'd need to explain this to a customer. + +### 3. Synthesize Results + +After all agents complete, organize findings into: + +**Consensus** — Points raised by 3+ perspectives as significant concerns or needs. + +**Tensions** — Points where two perspectives are in conflict (e.g., Engineering says "keep it simple" while Sales says "we need more configuration options"). + +**Hard Blockers** — Issues that must be resolved before this can move forward (typically Legal, Engineering feasibility, or missing strategic alignment). + +**Open Questions to Prepare For** — The most important questions you'll be asked in your next review meeting, ranked by likelihood. Include a suggested answer for each. + +**Suggested Revisions** — Top 3 changes to the document that would address the most significant feedback. + +## Output Format + +Present the synthesis clearly with sections for Consensus, Tensions, Hard Blockers, and Open Questions. Lead with the most critical finding. Keep the stakeholder-by-stakeholder breakdown in an appendix section at the end for reference. diff --git a/.claude/skills/pm/user-story/SKILL.md b/.claude/skills/pm/user-story/SKILL.md new file mode 100644 index 00000000000..0f1b9bbe1eb --- /dev/null +++ b/.claude/skills/pm/user-story/SKILL.md @@ -0,0 +1,81 @@ +--- +description: Generate sprint-ready user stories with acceptance criteria, story point estimates, and edge cases from a feature description or PRD section +allowed-tools: Read, AskUserQuestion +argument-hint: "" +--- + +Convert feature descriptions or PRD requirements into sprint-ready user stories with clear acceptance criteria, story point estimates, and edge cases documented — ready to paste into Linear or Jira. + +Input: $ARGUMENTS + +## Instructions + +### 1. Understand the Feature + +If the input is a file, use `Read`. If it's vague, ask via `AskUserQuestion`: +- Who is the primary user of this feature? +- What is the "happy path" — the most common way this will be used? +- Are there any known constraints (tech limitations, non-negotiable behaviors)? +- What does "done" look like from a user perspective? + +### 2. Epic Summary (if applicable) + +If the input describes a large feature, first write a one-paragraph **epic summary**: +- What is being built and why +- Who it's for +- What the scope boundaries are (what is NOT included) + +### 3. Break Into Stories + +Apply the INVEST criteria to each story: +- **Independent**: Can be developed without requiring another story to be done first +- **Negotiable**: Details can be discussed with engineering +- **Valuable**: Delivers something meaningful to the user or system +- **Estimable**: Can be pointed +- **Small**: Can be completed in one sprint (ideally 1–3 days of work) +- **Testable**: Has clear, verifiable acceptance criteria + +Story format: +``` +As a [user type], +I want to [perform an action], +so that [I achieve a benefit/outcome]. +``` + +### 4. Acceptance Criteria + +For each story, write 3–6 acceptance criteria using Given/When/Then: +``` +Given [precondition or starting state] +When [the user takes an action] +Then [the expected result occurs] +``` + +Cover: +- The happy path +- Error states and validation +- Boundary conditions (empty states, max limits) +- Permission or role-based behavior (if relevant) + +### 5. Story Point Estimates + +Estimate each story using Fibonacci (1, 2, 3, 5, 8): +- **1 pt**: Trivial, under 2 hours, no unknowns +- **2 pt**: Small, half a day, minimal complexity +- **3 pt**: Medium, 1–2 days, some decisions to make +- **5 pt**: Large, 3–4 days, multiple components, some uncertainty +- **8 pt**: Very large — consider splitting before sprint planning + +For anything 8+, flag it as a splitting candidate and suggest how to break it up. + +### 6. Edge Cases and Notes + +For each story, list: +- **Edge cases**: Unusual but valid user behaviors to handle +- **Out of scope**: Explicitly call out what this story does NOT cover +- **Dependencies**: Other stories, APIs, or team work this relies on +- **Open questions**: Decisions that need to be made before or during development + +## Output Format + +Structure as a numbered list of stories, each with: title, user story statement, acceptance criteria, story points, and notes. Format acceptance criteria as numbered Given/When/Then bullets. Keep the tone direct and technical — these are written for an engineering team. From 51a249097cdc287af36825aa29de6fed56a9cf63 Mon Sep 17 00:00:00 2001 From: ericokuma Date: Thu, 19 Mar 2026 12:21:55 -0700 Subject: [PATCH 02/35] feat(admin-console): add superuser auth guard for admin layout Co-Authored-By: Claude Opus 4.6 --- web-admin/src/routes/-/admin/+layout.ts | 65 +++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 web-admin/src/routes/-/admin/+layout.ts diff --git a/web-admin/src/routes/-/admin/+layout.ts b/web-admin/src/routes/-/admin/+layout.ts new file mode 100644 index 00000000000..819d6170694 --- /dev/null +++ b/web-admin/src/routes/-/admin/+layout.ts @@ -0,0 +1,65 @@ +// web-admin/src/routes/-/admin/+layout.ts +import { + adminServiceGetCurrentUser, + adminServiceListSuperusers, + getAdminServiceGetCurrentUserQueryKey, + getAdminServiceListSuperusersQueryKey, + type V1GetCurrentUserResponse, + type V1ListSuperusersResponse, +} from "@rilldata/web-admin/client"; +import { redirectToLogin } from "@rilldata/web-admin/client/redirect-utils"; +import { queryClient } from "@rilldata/web-common/lib/svelte-query/globalQueryClient"; +import { redirect } from "@sveltejs/kit"; +import { isAxiosError } from "axios"; + +export const load = async () => { + // Get current user + let currentUserEmail: string | undefined; + try { + const userResp = await queryClient.fetchQuery({ + queryKey: getAdminServiceGetCurrentUserQueryKey(), + queryFn: () => adminServiceGetCurrentUser(), + staleTime: 5 * 60 * 1000, + }); + currentUserEmail = userResp.user?.email; + } catch (e) { + if (isAxiosError(e) && e.response?.status === 401) { + // redirectToLogin() throws a SvelteKit redirect internally; + // call it outside the catch to avoid swallowing the redirect exception + } else { + throw redirect(307, "/"); + } + redirectToLogin(); + } + + if (!currentUserEmail) { + throw redirect(307, "/"); + } + + // Check if current user is a superuser + try { + const superusersResp = + await queryClient.fetchQuery({ + queryKey: getAdminServiceListSuperusersQueryKey(), + queryFn: () => adminServiceListSuperusers(), + staleTime: 5 * 60 * 1000, + }); + + const isSuperuser = superusersResp.users?.some( + (u) => u.email === currentUserEmail, + ); + + if (!isSuperuser) { + throw redirect(307, "/"); + } + } catch (e) { + // ListSuperusers itself will 403 if not a superuser + if (isAxiosError(e) && e.response?.status === 403) { + throw redirect(307, "/"); + } + // Re-throw SvelteKit redirects + throw e; + } + + return { currentUserEmail }; +}; From e7158a5d1b6216822571329dbe1778f4adb59381 Mon Sep 17 00:00:00 2001 From: ericokuma Date: Thu, 19 Mar 2026 12:21:56 -0700 Subject: [PATCH 03/35] feat(admin-console): add page header component Co-Authored-By: Claude Opus 4.6 --- .../admin/layout/AdminPageHeader.svelte | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 web-admin/src/features/admin/layout/AdminPageHeader.svelte diff --git a/web-admin/src/features/admin/layout/AdminPageHeader.svelte b/web-admin/src/features/admin/layout/AdminPageHeader.svelte new file mode 100644 index 00000000000..6aaeadbae76 --- /dev/null +++ b/web-admin/src/features/admin/layout/AdminPageHeader.svelte @@ -0,0 +1,25 @@ + + + + + From 417e96b0df9d077bb470daf6e90dc30fc42fcdc8 Mon Sep 17 00:00:00 2001 From: ericokuma Date: Thu, 19 Mar 2026 12:22:00 -0700 Subject: [PATCH 04/35] feat(admin-console): add sidebar navigation component Co-Authored-By: Claude Opus 4.6 --- .../features/admin/layout/AdminSidebar.svelte | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 web-admin/src/features/admin/layout/AdminSidebar.svelte diff --git a/web-admin/src/features/admin/layout/AdminSidebar.svelte b/web-admin/src/features/admin/layout/AdminSidebar.svelte new file mode 100644 index 00000000000..94e97ac81ab --- /dev/null +++ b/web-admin/src/features/admin/layout/AdminSidebar.svelte @@ -0,0 +1,109 @@ + + + + + + From 441d77d90675a7bd2a59d0941d8ff8b5a3ec7009 Mon Sep 17 00:00:00 2001 From: ericokuma Date: Thu, 19 Mar 2026 12:22:22 -0700 Subject: [PATCH 05/35] feat(admin-console): add shared UI components (ConfirmDialog, StatusBadge, ActionResultBanner, SearchInput) Co-Authored-By: Claude Opus 4.6 --- .../admin/shared/ActionResultBanner.svelte | 49 ++++++++++ .../admin/shared/ConfirmDialog.svelte | 91 +++++++++++++++++++ .../features/admin/shared/SearchInput.svelte | 51 +++++++++++ .../features/admin/shared/StatusBadge.svelte | 33 +++++++ 4 files changed, 224 insertions(+) create mode 100644 web-admin/src/features/admin/shared/ActionResultBanner.svelte create mode 100644 web-admin/src/features/admin/shared/ConfirmDialog.svelte create mode 100644 web-admin/src/features/admin/shared/SearchInput.svelte create mode 100644 web-admin/src/features/admin/shared/StatusBadge.svelte diff --git a/web-admin/src/features/admin/shared/ActionResultBanner.svelte b/web-admin/src/features/admin/shared/ActionResultBanner.svelte new file mode 100644 index 00000000000..13bcb545147 --- /dev/null +++ b/web-admin/src/features/admin/shared/ActionResultBanner.svelte @@ -0,0 +1,49 @@ + + + +{#if type && message} + +{/if} + + diff --git a/web-admin/src/features/admin/shared/ConfirmDialog.svelte b/web-admin/src/features/admin/shared/ConfirmDialog.svelte new file mode 100644 index 00000000000..743e7848e34 --- /dev/null +++ b/web-admin/src/features/admin/shared/ConfirmDialog.svelte @@ -0,0 +1,91 @@ + + + +{#if open} + + +
+
+

{title}

+ {#if description} +

{description}

+ {/if} +
+ + +
+
+
+{/if} + + diff --git a/web-admin/src/features/admin/shared/SearchInput.svelte b/web-admin/src/features/admin/shared/SearchInput.svelte new file mode 100644 index 00000000000..161055c618a --- /dev/null +++ b/web-admin/src/features/admin/shared/SearchInput.svelte @@ -0,0 +1,51 @@ + + + +
+ e.key === "Enter" && handleSubmit()} + /> +
+ + diff --git a/web-admin/src/features/admin/shared/StatusBadge.svelte b/web-admin/src/features/admin/shared/StatusBadge.svelte new file mode 100644 index 00000000000..bc52339df82 --- /dev/null +++ b/web-admin/src/features/admin/shared/StatusBadge.svelte @@ -0,0 +1,33 @@ + + + + + {label} + + + From f8f3cc0e7c11685f4a90f1b1796dc59abd178a0b Mon Sep 17 00:00:00 2001 From: ericokuma Date: Thu, 19 Mar 2026 12:23:08 -0700 Subject: [PATCH 06/35] feat(admin-console): add admin layout with sidebar + content area Co-Authored-By: Claude Opus 4.6 --- web-admin/src/routes/-/admin/+layout.svelte | 24 +++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 web-admin/src/routes/-/admin/+layout.svelte diff --git a/web-admin/src/routes/-/admin/+layout.svelte b/web-admin/src/routes/-/admin/+layout.svelte new file mode 100644 index 00000000000..4f02d21a778 --- /dev/null +++ b/web-admin/src/routes/-/admin/+layout.svelte @@ -0,0 +1,24 @@ + + + + Admin Console | Rill + + +
+ +
+ +
+
+ + From 397919069ffa0bf16cb49c013cf310a4b1085a97 Mon Sep 17 00:00:00 2001 From: ericokuma Date: Thu, 19 Mar 2026 12:23:13 -0700 Subject: [PATCH 07/35] feat(admin-console): add dashboard home page with quick action cards Co-Authored-By: Claude Opus 4.6 --- web-admin/src/routes/-/admin/+page.svelte | 56 +++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 web-admin/src/routes/-/admin/+page.svelte diff --git a/web-admin/src/routes/-/admin/+page.svelte b/web-admin/src/routes/-/admin/+page.svelte new file mode 100644 index 00000000000..ba4dbfb05a6 --- /dev/null +++ b/web-admin/src/routes/-/admin/+page.svelte @@ -0,0 +1,56 @@ + + + + + + + + From 29a291557ce7402e0a9d256a988d3710aa760d7d Mon Sep 17 00:00:00 2001 From: ericokuma Date: Thu, 19 Mar 2026 12:23:50 -0700 Subject: [PATCH 08/35] feat(admin-console): add user management API selectors Co-Authored-By: Claude Opus 4.6 --- .../src/features/admin/users/selectors.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 web-admin/src/features/admin/users/selectors.ts diff --git a/web-admin/src/features/admin/users/selectors.ts b/web-admin/src/features/admin/users/selectors.ts new file mode 100644 index 00000000000..859f37dbc4a --- /dev/null +++ b/web-admin/src/features/admin/users/selectors.ts @@ -0,0 +1,26 @@ +// web-admin/src/features/admin/users/selectors.ts +import { + createAdminServiceSearchUsers, + createAdminServiceIssueRepresentativeAuthToken, + createAdminServiceRevokeRepresentativeAuthTokens, + createAdminServiceDeleteUser, +} from "@rilldata/web-admin/client"; + +export function searchUsers(emailQuery: string) { + return createAdminServiceSearchUsers( + { emailQuery }, + { query: { enabled: emailQuery.length >= 2 } }, + ); +} + +export function createAssumeUserMutation() { + return createAdminServiceIssueRepresentativeAuthToken(); +} + +export function createUnassumeUserMutation() { + return createAdminServiceRevokeRepresentativeAuthTokens(); +} + +export function createDeleteUserMutation() { + return createAdminServiceDeleteUser(); +} From 14647e122fd9a232118c926f47cf2d99822d0b94 Mon Sep 17 00:00:00 2001 From: ericokuma Date: Thu, 19 Mar 2026 12:23:59 -0700 Subject: [PATCH 09/35] feat(admin-console): add billing management API selectors Co-Authored-By: Claude Opus 4.6 --- .../src/features/admin/billing/selectors.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 web-admin/src/features/admin/billing/selectors.ts diff --git a/web-admin/src/features/admin/billing/selectors.ts b/web-admin/src/features/admin/billing/selectors.ts new file mode 100644 index 00000000000..62f9aba8c44 --- /dev/null +++ b/web-admin/src/features/admin/billing/selectors.ts @@ -0,0 +1,32 @@ +// web-admin/src/features/admin/billing/selectors.ts +import { + createAdminServiceSudoExtendTrial, + createAdminServiceSudoTriggerBillingRepair, + createAdminServiceSudoDeleteOrganizationBillingIssue, + createAdminServiceSudoUpdateOrganizationBillingCustomer, + createAdminServiceListOrganizationBillingIssues, +} from "@rilldata/web-admin/client"; + +export function createExtendTrialMutation() { + return createAdminServiceSudoExtendTrial(); +} + +export function createBillingRepairMutation() { + return createAdminServiceSudoTriggerBillingRepair(); +} + +export function createDeleteBillingIssueMutation() { + return createAdminServiceSudoDeleteOrganizationBillingIssue(); +} + +export function createSetBillingCustomerMutation() { + return createAdminServiceSudoUpdateOrganizationBillingCustomer(); +} + +export function getBillingIssues(org: string) { + return createAdminServiceListOrganizationBillingIssues( + org, + { superuserForceAccess: true }, + { query: { enabled: org.length > 0 } }, + ); +} From f502d41716e3b6ed85842a4a86c598d6701ec5ff Mon Sep 17 00:00:00 2001 From: ericokuma Date: Thu, 19 Mar 2026 12:24:48 -0700 Subject: [PATCH 10/35] feat(admin-console): add user management page with search, assume, and delete Co-Authored-By: Claude Opus 4.6 --- .../src/routes/-/admin/users/+page.svelte | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 web-admin/src/routes/-/admin/users/+page.svelte diff --git a/web-admin/src/routes/-/admin/users/+page.svelte b/web-admin/src/routes/-/admin/users/+page.svelte new file mode 100644 index 00000000000..75e4d6b9613 --- /dev/null +++ b/web-admin/src/routes/-/admin/users/+page.svelte @@ -0,0 +1,184 @@ + + + + + + + +
+ +
+ +{#if $usersQuery.isLoading && searchQuery.length >= 2} +

Searching...

+{:else if $usersQuery.data?.users?.length} +
+ + + + + + + + + + + {#each $usersQuery.data.users as user} + + + + + + + {/each} + +
EmailDisplay NameCreatedActions
{user.email}{user.displayName ?? "-"} + {user.createdOn + ? new Date(user.createdOn).toLocaleDateString() + : "-"} + +
+ + + +
+
+
+{:else if searchQuery.length >= 2 && $usersQuery.isSuccess} +

No users found for "{searchQuery}"

+{/if} + + + + From ccd69055bf8d10d4c46ba83c48ab9f70b20702b7 Mon Sep 17 00:00:00 2001 From: ericokuma Date: Thu, 19 Mar 2026 12:25:10 -0700 Subject: [PATCH 11/35] feat(admin-console): add billing management page with trial extension, repair, and issue management Co-Authored-By: Claude Opus 4.6 --- .../src/routes/-/admin/billing/+page.svelte | 264 ++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 web-admin/src/routes/-/admin/billing/+page.svelte diff --git a/web-admin/src/routes/-/admin/billing/+page.svelte b/web-admin/src/routes/-/admin/billing/+page.svelte new file mode 100644 index 00000000000..254c6fa746a --- /dev/null +++ b/web-admin/src/routes/-/admin/billing/+page.svelte @@ -0,0 +1,264 @@ + + + + + + +
+ +
+

Extend Trial

+

Add days to an organization's trial period.

+
+ + + +
+
+ + +
+

Set Billing Customer ID

+

Associate a Stripe customer ID with an organization.

+
+ + + +
+
+ + +
+

Billing Repair

+

Trigger a billing state recalculation for an organization.

+
+ + +
+
+ + +
+

Billing Issues

+

View and resolve billing issues for an organization.

+
+ +
+ {#if $billingIssuesQuery.data?.issues?.length} +
+ {#each $billingIssuesQuery.data.issues as issue} +
+
+ {issue.type} + {issue.metadata ?? ""} +
+ +
+ {/each} +
+ {:else if issuesOrg && $billingIssuesQuery.isSuccess} +

No billing issues found.

+ {/if} +
+
+ + + + From efff27cdc48be10c1901a52cbd37299fea92d5d3 Mon Sep 17 00:00:00 2001 From: ericokuma Date: Thu, 19 Mar 2026 12:26:27 -0700 Subject: [PATCH 12/35] feat(admin-console): add admin link in top nav for superusers Co-Authored-By: Claude Opus 4.6 --- .../features/organizations/OrgHeader.svelte | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/web-admin/src/features/organizations/OrgHeader.svelte b/web-admin/src/features/organizations/OrgHeader.svelte index 31502a53960..5507aaa0228 100644 --- a/web-admin/src/features/organizations/OrgHeader.svelte +++ b/web-admin/src/features/organizations/OrgHeader.svelte @@ -3,7 +3,10 @@ import Breadcrumbs from "@rilldata/web-common/components/navigation/breadcrumbs/Breadcrumbs.svelte"; import Header from "@rilldata/web-common/layout/header/Header.svelte"; import HeaderLogo from "@rilldata/web-common/layout/header/HeaderLogo.svelte"; - import { createAdminServiceGetCurrentUser } from "../../client"; + import { + createAdminServiceGetCurrentUser, + createAdminServiceListSuperusers, + } from "../../client"; import { useBreadcrumbOrgPaths, useBreadcrumbProjectPaths, @@ -27,6 +30,15 @@ $: loggedIn = !!$user.data?.user; $: rillLogoHref = !loggedIn ? "https://www.rilldata.com" : "/"; + // Check if the current user is a superuser; the ListSuperusers call returns 403 for non-superusers + const superusers = createAdminServiceListSuperusers(); + $: isSuperuser = + $superusers.isSuccess && + !!$user.data?.user?.email && + ($superusers.data?.users ?? []).some( + (su) => su.email === $user.data?.user?.email, + ); + $: orgPathsQuery = useBreadcrumbOrgPaths( loggedIn, organization, @@ -48,6 +60,14 @@ {/if}
+ {#if isSuperuser} + + Admin + + {/if} {#if $user.isSuccess} {#if $user.data?.user} From 55affa1e18aa4101b5ea2f1f45dd0037b3db544d Mon Sep 17 00:00:00 2001 From: ericokuma Date: Thu, 19 Mar 2026 12:27:11 -0700 Subject: [PATCH 13/35] feat(admin-console): add quota management selectors Co-Authored-By: Claude Opus 4.6 --- .../src/features/admin/quotas/selectors.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 web-admin/src/features/admin/quotas/selectors.ts diff --git a/web-admin/src/features/admin/quotas/selectors.ts b/web-admin/src/features/admin/quotas/selectors.ts new file mode 100644 index 00000000000..6968d035c55 --- /dev/null +++ b/web-admin/src/features/admin/quotas/selectors.ts @@ -0,0 +1,22 @@ +// web-admin/src/features/admin/quotas/selectors.ts +import { + createAdminServiceGetOrganization, + createAdminServiceSudoUpdateOrganizationQuotas, + createAdminServiceSudoUpdateUserQuotas, +} from "@rilldata/web-admin/client"; + +export function getOrgForQuotas(org: string) { + return createAdminServiceGetOrganization( + org, + { superuserForceAccess: true }, + { query: { enabled: org.length > 0 } }, + ); +} + +export function createUpdateOrgQuotasMutation() { + return createAdminServiceSudoUpdateOrganizationQuotas(); +} + +export function createUpdateUserQuotasMutation() { + return createAdminServiceSudoUpdateUserQuotas(); +} From 722369888743f43244d1ad31ad5fc1167876eac6 Mon Sep 17 00:00:00 2001 From: ericokuma Date: Thu, 19 Mar 2026 12:27:17 -0700 Subject: [PATCH 14/35] feat(admin-console): add organization management selectors Co-Authored-By: Claude Opus 4.6 --- .../features/admin/organizations/selectors.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 web-admin/src/features/admin/organizations/selectors.ts diff --git a/web-admin/src/features/admin/organizations/selectors.ts b/web-admin/src/features/admin/organizations/selectors.ts new file mode 100644 index 00000000000..1cb48e23402 --- /dev/null +++ b/web-admin/src/features/admin/organizations/selectors.ts @@ -0,0 +1,31 @@ +// web-admin/src/features/admin/organizations/selectors.ts +import { + createAdminServiceGetOrganization, + createAdminServiceSudoUpdateOrganizationCustomDomain, + createAdminServiceListOrganizationMemberUsers, + createAdminServiceAddOrganizationMemberUser, +} from "@rilldata/web-admin/client"; + +export function getOrganization(org: string) { + return createAdminServiceGetOrganization( + org, + { superuserForceAccess: true }, + { query: { enabled: org.length > 0 } }, + ); +} + +export function getOrgAdmins(org: string) { + return createAdminServiceListOrganizationMemberUsers( + org, + { superuserForceAccess: true }, + { query: { enabled: org.length > 0 } }, + ); +} + +export function createSetCustomDomainMutation() { + return createAdminServiceSudoUpdateOrganizationCustomDomain(); +} + +export function createJoinOrgMutation() { + return createAdminServiceAddOrganizationMemberUser(); +} From 5b7a4198a1449cc3a5a6c744c94bddec2e161be3 Mon Sep 17 00:00:00 2001 From: ericokuma Date: Thu, 19 Mar 2026 12:28:11 -0700 Subject: [PATCH 15/35] feat(admin-console): add quota management page with editable fields Co-Authored-By: Claude Opus 4.6 --- .../routes/-/admin/organizations/+page.svelte | 165 ++++++++++++++++ .../src/routes/-/admin/quotas/+page.svelte | 184 ++++++++++++++++++ 2 files changed, 349 insertions(+) create mode 100644 web-admin/src/routes/-/admin/organizations/+page.svelte create mode 100644 web-admin/src/routes/-/admin/quotas/+page.svelte diff --git a/web-admin/src/routes/-/admin/organizations/+page.svelte b/web-admin/src/routes/-/admin/organizations/+page.svelte new file mode 100644 index 00000000000..8a14eb4d4b3 --- /dev/null +++ b/web-admin/src/routes/-/admin/organizations/+page.svelte @@ -0,0 +1,165 @@ + + + + + + +
+ +
+

Organization Lookup

+
+ e.key === "Enter" && handleLookup()} + /> + +
+ + {#if $orgQuery.data?.organization} + {@const org = $orgQuery.data.organization} +
+
+ ID + {org.id} +
+
+ Name + {org.name} +
+
+ Description + {org.description ?? "-"} +
+
+ Billing Plan + {org.billingPlanDisplayName ?? "-"} +
+
+ Custom Domain + {org.customDomain ?? "None"} +
+
+ Created + + {org.createdOn ? new Date(org.createdOn).toLocaleDateString() : "-"} + +
+
+ + {#if $adminsQuery.data?.members?.length} +

Members

+
+ {#each $adminsQuery.data.members as member} +
+ {member.userEmail} + {member.roleName} +
+ {/each} +
+ {/if} + {/if} +
+ + +
+

Set Custom Domain

+
+ + + +
+
+ + +
+

Add User to Organization

+
+ + + + +
+
+
+ + diff --git a/web-admin/src/routes/-/admin/quotas/+page.svelte b/web-admin/src/routes/-/admin/quotas/+page.svelte new file mode 100644 index 00000000000..e187e989006 --- /dev/null +++ b/web-admin/src/routes/-/admin/quotas/+page.svelte @@ -0,0 +1,184 @@ + + + + + + + +
+
+
+ + +
+ +
+ e.key === "Enter" && handleLookup()} + /> + +
+ + {#if quotaType === "org" && activeOrg && $orgQuery.data?.organization} +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+ {:else if quotaType === "user" && activeUser && lookupDone} +
+
+ + +
+
+

User quotas are limited to the single-user orgs field. Other quotas are managed at the org level.

+ +
+ +
+ {/if} +
+
+ + From 61f2da305752d095a2f6728e4d7c27e043cc7fb5 Mon Sep 17 00:00:00 2001 From: ericokuma Date: Thu, 19 Mar 2026 12:31:22 -0700 Subject: [PATCH 16/35] feat(admin-console): add project management selectors Co-Authored-By: Claude Opus 4.6 --- .../src/features/admin/projects/selectors.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 web-admin/src/features/admin/projects/selectors.ts diff --git a/web-admin/src/features/admin/projects/selectors.ts b/web-admin/src/features/admin/projects/selectors.ts new file mode 100644 index 00000000000..9f7b8121d07 --- /dev/null +++ b/web-admin/src/features/admin/projects/selectors.ts @@ -0,0 +1,31 @@ +// web-admin/src/features/admin/projects/selectors.ts +import { + createAdminServiceSearchProjectNames, + createAdminServiceGetProject, + createAdminServiceUpdateProject, + createAdminServiceRedeployProject, + createAdminServiceHibernateProject, +} from "@rilldata/web-admin/client"; + +export function searchProjects(namePattern: string) { + return createAdminServiceSearchProjectNames( + { namePattern, pageSize: 50 }, + { query: { enabled: namePattern.length >= 2 } }, + ); +} + +export function getProject(org: string, project: string) { + return createAdminServiceGetProject(org, project); +} + +export function createUpdateProjectMutation() { + return createAdminServiceUpdateProject(); +} + +export function createRedeployProjectMutation() { + return createAdminServiceRedeployProject(); +} + +export function createHibernateProjectMutation() { + return createAdminServiceHibernateProject(); +} From c92255fb6a2d516fa34e9f63bdcdc7fcb8f591fc Mon Sep 17 00:00:00 2001 From: ericokuma Date: Thu, 19 Mar 2026 12:31:35 -0700 Subject: [PATCH 17/35] feat(admin-console): add domain whitelist management page Co-Authored-By: Claude Opus 4.6 --- .../src/features/admin/whitelist/selectors.ts | 21 +++ .../src/routes/-/admin/whitelist/+page.svelte | 131 ++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 web-admin/src/features/admin/whitelist/selectors.ts create mode 100644 web-admin/src/routes/-/admin/whitelist/+page.svelte diff --git a/web-admin/src/features/admin/whitelist/selectors.ts b/web-admin/src/features/admin/whitelist/selectors.ts new file mode 100644 index 00000000000..715c2c9248c --- /dev/null +++ b/web-admin/src/features/admin/whitelist/selectors.ts @@ -0,0 +1,21 @@ +import { + createAdminServiceCreateWhitelistedDomain, + createAdminServiceRemoveWhitelistedDomain, + createAdminServiceListWhitelistedDomains, +} from "@rilldata/web-admin/client"; + +export function getWhitelistedDomains(org: string) { + return createAdminServiceListWhitelistedDomains( + org, + { superuserForceAccess: true }, + { query: { enabled: org.length > 0 } }, + ); +} + +export function createAddWhitelistMutation() { + return createAdminServiceCreateWhitelistedDomain(); +} + +export function createRemoveWhitelistMutation() { + return createAdminServiceRemoveWhitelistedDomain(); +} diff --git a/web-admin/src/routes/-/admin/whitelist/+page.svelte b/web-admin/src/routes/-/admin/whitelist/+page.svelte new file mode 100644 index 00000000000..7c463d61bd3 --- /dev/null +++ b/web-admin/src/routes/-/admin/whitelist/+page.svelte @@ -0,0 +1,131 @@ + + + + + + +
+
+
+ e.key === "Enter" && handleLookup()} /> + +
+ + {#if activeOrg} +
+ + + +
+ + {#if $domainsQuery.data?.domains?.length} + + + + + + {#each $domainsQuery.data.domains as d} + + + + + + {/each} + +
DomainRoleActions
{d.domain}{d.role} + +
+ {:else if $domainsQuery.isSuccess} +

No whitelisted domains.

+ {/if} + {/if} +
+
+ + + + From 2abf00e0554c261eb069c45e88aa439b6945567b Mon Sep 17 00:00:00 2001 From: ericokuma Date: Thu, 19 Mar 2026 12:32:41 -0700 Subject: [PATCH 18/35] feat(admin-console): add project management page with search, hibernate, and redeploy Co-Authored-By: Claude Opus 4.6 --- .../src/routes/-/admin/projects/+page.svelte | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 web-admin/src/routes/-/admin/projects/+page.svelte diff --git a/web-admin/src/routes/-/admin/projects/+page.svelte b/web-admin/src/routes/-/admin/projects/+page.svelte new file mode 100644 index 00000000000..4f1b9815a15 --- /dev/null +++ b/web-admin/src/routes/-/admin/projects/+page.svelte @@ -0,0 +1,145 @@ + + + + + + + +
+ +
+ +{#if $projectsQuery.isLoading && searchQuery.length >= 2} +

Searching...

+{:else if $projectsQuery.data?.names?.length} + + + + + + + + + {#each $projectsQuery.data.names as name} + + + + + {/each} + +
ProjectActions
{name} +
+ + View + + + +
+
+{:else if searchQuery.length >= 2 && $projectsQuery.isSuccess} +

No projects found for "{searchQuery}"

+{/if} + + + + From b5561a86bd6d3934ede36731fad0086164e81741 Mon Sep 17 00:00:00 2001 From: ericokuma Date: Thu, 19 Mar 2026 12:33:27 -0700 Subject: [PATCH 19/35] feat(admin-console): add annotations management page Co-Authored-By: Claude Opus 4.6 --- .../routes/-/admin/annotations/+page.svelte | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 web-admin/src/routes/-/admin/annotations/+page.svelte diff --git a/web-admin/src/routes/-/admin/annotations/+page.svelte b/web-admin/src/routes/-/admin/annotations/+page.svelte new file mode 100644 index 00000000000..f3016fcaf0c --- /dev/null +++ b/web-admin/src/routes/-/admin/annotations/+page.svelte @@ -0,0 +1,91 @@ + + + + + + +
+
+ + + +
+
+ + +
+ +
+ + From 0e76ab74fd084f930126c1d0b754b116bc2f3707 Mon Sep 17 00:00:00 2001 From: ericokuma Date: Thu, 19 Mar 2026 12:33:28 -0700 Subject: [PATCH 20/35] feat(admin-console): add superuser management page Co-Authored-By: Claude Opus 4.6 --- .../routes/-/admin/superusers/+page.svelte | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 web-admin/src/routes/-/admin/superusers/+page.svelte diff --git a/web-admin/src/routes/-/admin/superusers/+page.svelte b/web-admin/src/routes/-/admin/superusers/+page.svelte new file mode 100644 index 00000000000..9138d475149 --- /dev/null +++ b/web-admin/src/routes/-/admin/superusers/+page.svelte @@ -0,0 +1,106 @@ + + + + + + + +
+

Add Superuser

+
+ e.key === "Enter" && handleAdd()} /> + +
+
+ +{#if $superusersQuery.data?.users?.length} + + + + + + {#each $superusersQuery.data.users as user} + + + + + + {/each} + +
EmailDisplay NameActions
{user.email}{user.displayName ?? "-"} + +
+{/if} + + + + From 6d280083fb0968784fba39217c2493718cd54fb6 Mon Sep 17 00:00:00 2001 From: ericokuma Date: Thu, 19 Mar 2026 12:33:30 -0700 Subject: [PATCH 21/35] feat(admin-console): add stub pages for virtual files and runtime management Co-Authored-By: Claude Opus 4.6 --- .../src/routes/-/admin/runtime/+page.svelte | 30 ++++++++++++++ .../routes/-/admin/virtual-files/+page.svelte | 41 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 web-admin/src/routes/-/admin/runtime/+page.svelte create mode 100644 web-admin/src/routes/-/admin/virtual-files/+page.svelte diff --git a/web-admin/src/routes/-/admin/runtime/+page.svelte b/web-admin/src/routes/-/admin/runtime/+page.svelte new file mode 100644 index 00000000000..85b2f582369 --- /dev/null +++ b/web-admin/src/routes/-/admin/runtime/+page.svelte @@ -0,0 +1,30 @@ + + + + + + +
+

Runtime management will be available in a future update. Use the CLI for now:

+
rill sudo runtime list-instances <host>
+rill sudo runtime delete-instance <host> <instance_id>
+rill sudo runtime manager-token <host>
+
+ + diff --git a/web-admin/src/routes/-/admin/virtual-files/+page.svelte b/web-admin/src/routes/-/admin/virtual-files/+page.svelte new file mode 100644 index 00000000000..e5ae4c824c9 --- /dev/null +++ b/web-admin/src/routes/-/admin/virtual-files/+page.svelte @@ -0,0 +1,41 @@ + + + + + + +
+
+ + + +
+

Virtual file management will be available in a future update.

+
+ + From 33f445e1cbb9ed06b310e4e3e4b41ecfd359c674 Mon Sep 17 00:00:00 2001 From: ericokuma Date: Thu, 19 Mar 2026 12:53:21 -0700 Subject: [PATCH 22/35] fix(admin-console): fix annotations page Svelte template compile error --- web-admin/src/routes/-/admin/annotations/+page.svelte | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web-admin/src/routes/-/admin/annotations/+page.svelte b/web-admin/src/routes/-/admin/annotations/+page.svelte index f3016fcaf0c..21c177e023d 100644 --- a/web-admin/src/routes/-/admin/annotations/+page.svelte +++ b/web-admin/src/routes/-/admin/annotations/+page.svelte @@ -1,7 +1,6 @@ -
- - Users - Search, lookup, and manage user accounts - - - Billing - Extend trials, repair billing, manage subscriptions - - - Organizations - Lookup orgs, set custom domains, manage plans - - - Projects - Search projects, edit settings, hibernate or reset - - - Quotas - View and adjust organization and user quotas - - - Superusers - Manage superuser access - +{#if $assumedUser} +
+ Currently assumed as {$assumedUser} + +
+{/if} + +
+
+{#if $usersQuery.isFetching && searchQuery.length >= 3} +
+
+ Searching users... +
+{:else if $usersQuery.data?.users?.length} +

+ {$usersQuery.data.users.length} result{$usersQuery.data.users.length === 1 + ? "" + : "s"} +

+
+ + + + + + + + + + + {#each $usersQuery.data.users as user} + {@const isAssumed = $assumedUser === user.email} + + + + + + + {/each} + +
EmailDisplay NameCreatedActions
{user.email}{user.displayName ?? "-"} + {user.createdOn + ? new Date(user.createdOn).toLocaleDateString() + : "-"} + +
+ {#if isAssumed} + + {:else} + + {/if} + +
+
+
+{:else if searchQuery.length >= 3 && $usersQuery.isSuccess} +

No users found for "{searchQuery}"

+{:else if searchQuery.length < 3} +

+ Type at least 3 characters to search across all organizations. +

+{/if} + + + diff --git a/web-admin/src/routes/-/admin/billing/+page.svelte b/web-admin/src/routes/-/admin/billing/+page.svelte index 254c6fa746a..37df7f2205e 100644 --- a/web-admin/src/routes/-/admin/billing/+page.svelte +++ b/web-admin/src/routes/-/admin/billing/+page.svelte @@ -1,8 +1,14 @@ - -
+ +
+

Billing Setup

+

+ Generate a Stripe checkout page link for an organization to enter their + billing information. +

+
+
+ +
+ +
+ {#if setupUrl} +
+ Share this link with the customer: +
+ + {setupUrl} + + +
+
+ {/if} +
+

Extend Trial

Add days to an organization's trial period.

- +
+ +
-
@@ -122,22 +229,33 @@

Set Billing Customer ID

-

Associate a Stripe customer ID with an organization.

+

+ Associate a Stripe customer ID with an organization. +

- +
+ +
-
@@ -145,15 +263,21 @@

Billing Repair

-

Trigger a billing state recalculation for an organization.

+

+ Trigger a billing state recalculation for an organization. +

- -
@@ -162,16 +286,23 @@

Billing Issues

-

View and resolve billing issues for an organization.

+

+ View and resolve billing issues for an organization. +

- +
+ +
- {#if $billingIssuesQuery.data?.issues?.length} + {#if $billingIssuesQuery.isFetching} +
+
+ Loading issues... +
+ {:else if $billingIssuesQuery.data?.issues?.length}
{#each $billingIssuesQuery.data.issues as issue}
@@ -181,7 +312,8 @@
@@ -203,13 +335,17 @@ diff --git a/web-admin/src/routes/-/admin/organizations/+page.svelte b/web-admin/src/routes/-/admin/organizations/+page.svelte index 8a14eb4d4b3..52a065bd457 100644 --- a/web-admin/src/routes/-/admin/organizations/+page.svelte +++ b/web-admin/src/routes/-/admin/organizations/+page.svelte @@ -1,86 +1,181 @@ - - -
- -
-

Organization Lookup

-
- e.key === "Enter" && handleLookup()} - /> - -
- - {#if $orgQuery.data?.organization} - {@const org = $orgQuery.data.organization} +
+
+ + {#if $orgSearchQuery.isFetching && searchInput.length >= 3} +
+ {/if} + {#if showDropdown && orgNames.length > 0} + + {:else if showDropdown && searchInput.length >= 3 && $orgSearchQuery.isSuccess && orgNames.length === 0} + + {/if} +
+

+ Type to search, then select or press Enter for exact match. +

+
+ +{#if $orgQuery.isFetching} +
+
+ Looking up organization... +
+{:else if $orgQuery.isError && lookupOrg} +

+ Organization "{lookupOrg}" not found or access denied. +

+{:else if $orgQuery.data?.organization} + {@const org = $orgQuery.data.organization} +
+
+

Organization Details

ID - {org.id} + {org.id}
Name @@ -94,6 +189,12 @@ Billing Plan {org.billingPlanDisplayName ?? "-"}
+
+ Billing Customer ID + {org.billingCustomerId ?? "-"} +
Custom Domain {org.customDomain ?? "None"} @@ -101,65 +202,205 @@
Created - {org.createdOn ? new Date(org.createdOn).toLocaleDateString() : "-"} + {org.createdOn + ? new Date(org.createdOn).toLocaleDateString() + : "-"} + +
+
+ Projects + + {#if $projectsQuery.isFetching} + Loading... + {:else if $projectsQuery.data?.projects} + {$projectsQuery.data.projects.length} + {:else} + 0 + {/if}
+
- {#if $adminsQuery.data?.members?.length} -

Members

-
- {#each $adminsQuery.data.members as member} -
- {member.userEmail} - {member.roleName} + + {#if $projectsQuery.data?.projects?.length} +
+

+ Projects ({$projectsQuery.data.projects.length}) +

+
+ {#each $projectsQuery.data.projects as project} +
+ + {project.name} + +
+ + +
{/each}
- {/if} +
{/if} -
- - -
-

Set Custom Domain

-
- - - -
-
- - -
-

Add User to Organization

-
- - - - -
-
-
+ + + {#if $membersQuery.isFetching} +
+
+ Loading members... +
+ {:else if $membersQuery.data?.members?.length} +
+

+ Members ({$membersQuery.data.members.length}) +

+ + + + + + + + + {#each $membersQuery.data.members as member} + + + + + {/each} + +
EmailRole
{member.userEmail}{member.roleName}
+
+ {/if} +
+{/if} + + diff --git a/web-admin/src/routes/-/admin/projects/+page.svelte b/web-admin/src/routes/-/admin/projects/+page.svelte index 4f1b9815a15..b0eb6ff8253 100644 --- a/web-admin/src/routes/-/admin/projects/+page.svelte +++ b/web-admin/src/routes/-/admin/projects/+page.svelte @@ -1,8 +1,11 @@ - -
-
- - -
-
- e.key === "Enter" && handleLookup()} - /> - +
+ +
- {#if quotaType === "org" && activeOrg && $orgQuery.data?.organization} + {#if activeOrg && $orgQuery.isFetching} +
+
+ Loading quotas... +
+ {:else if activeOrg && $orgQuery.data?.organization}
@@ -149,18 +118,6 @@
-
- -
- {:else if quotaType === "user" && activeUser && lookupDone} -
-
- - -
-
-

User quotas are limited to the single-user orgs field. Other quotas are managed at the org level.

-
@@ -181,4 +138,6 @@ .quota-grid { @apply grid grid-cols-2 lg:grid-cols-3 gap-4; } .quota-field { @apply flex flex-col gap-1; } .quota-label { @apply text-xs font-medium text-slate-500 dark:text-slate-400; } + .loading { @apply flex items-center gap-2 py-4; } + .spinner { @apply w-4 h-4 border-2 border-slate-300 border-t-blue-600 rounded-full animate-spin; } diff --git a/web-admin/src/routes/-/admin/superusers/+page.svelte b/web-admin/src/routes/-/admin/superusers/+page.svelte index 9138d475149..be6a6bce02d 100644 --- a/web-admin/src/routes/-/admin/superusers/+page.svelte +++ b/web-admin/src/routes/-/admin/superusers/+page.svelte @@ -5,12 +5,15 @@ createAdminServiceSetSuperuser, } from "@rilldata/web-admin/client"; import AdminPageHeader from "@rilldata/web-admin/features/admin/layout/AdminPageHeader.svelte"; - import ActionResultBanner from "@rilldata/web-admin/features/admin/shared/ActionResultBanner.svelte"; import ConfirmDialog from "@rilldata/web-admin/features/admin/shared/ConfirmDialog.svelte"; + import { + notifySuccess, + notifyError, + } from "@rilldata/web-admin/features/admin/shared/notify"; import { useQueryClient } from "@tanstack/svelte-query"; - let bannerRef: ActionResultBanner; let newEmail = ""; + let addLoading = false; let confirmOpen = false; let confirmTitle = ""; let confirmDescription = ""; @@ -23,13 +26,19 @@ async function handleAdd() { if (!newEmail) return; + addLoading = true; try { await $setSuperuser.mutateAsync({ data: { email: newEmail, superuser: true } }); - bannerRef.show("success", `${newEmail} added as superuser`); + notifySuccess( `${newEmail} added as superuser`); newEmail = ""; - await queryClient.invalidateQueries(); + await queryClient.invalidateQueries({ + predicate: (q) => + (q.queryKey[0] as string)?.includes("/v1/superuser"), + }); } catch (err) { - bannerRef.show("error", `Failed: ${err}`); + notifyError( `Failed: ${err}`); + } finally { + addLoading = false; } } @@ -40,10 +49,13 @@ confirmAction = async () => { try { await $setSuperuser.mutateAsync({ data: { email, superuser: false } }); - bannerRef.show("success", `${email} removed as superuser`); - await queryClient.invalidateQueries(); + notifySuccess( `${email} removed as superuser`); + await queryClient.invalidateQueries({ + predicate: (q) => + (q.queryKey[0] as string)?.includes("/v1/superuser"), + }); } catch (err) { - bannerRef.show("error", `Failed: ${err}`); + notifyError( `Failed: ${err}`); } }; confirmOpen = true; @@ -52,24 +64,46 @@ - -

Add Superuser

- e.key === "Enter" && handleAdd()} /> - + e.key === "Enter" && handleAdd()} + /> +
-{#if $superusersQuery.data?.users?.length} +{#if $superusersQuery.isFetching} +
+
+ Loading superusers... +
+{:else if $superusersQuery.data?.users?.length} +

+ {$superusersQuery.data.users.length} superuser{$superusersQuery.data.users.length === 1 ? "" : "s"} +

- + + + + + {#each $superusersQuery.data.users as user} @@ -77,7 +111,10 @@ @@ -87,20 +124,66 @@
EmailDisplay NameActions
EmailDisplay NameActions
{user.email} {user.displayName ?? "-"} -
{/if} - + From e058d493c4486a3524ce9cfdf351bed58afef6df Mon Sep 17 00:00:00 2001 From: ericokuma Date: Fri, 20 Mar 2026 23:44:16 -0700 Subject: [PATCH 25/35] fix(admin-console): add missing new files and remove deleted pages Add new shared components (OrgSearchInput, UserSearchInput, notify, RepresentingBanner, assume-state) that were missing from previous commits. Remove obsolete pages (annotations, runtime, users, virtual-files, whitelist) and unused components (ActionResultBanner, StatusBadge, whitelist selectors) that were deleted locally but not staged. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../admin/shared/ActionResultBanner.svelte | 49 ----- .../admin/shared/OrgSearchInput.svelte | 118 +++++++++++ .../features/admin/shared/StatusBadge.svelte | 33 ---- .../admin/shared/UserSearchInput.svelte | 107 ++++++++++ web-admin/src/features/admin/shared/notify.ts | 9 + .../admin/users/RepresentingBanner.svelte | 50 +++++ .../src/features/admin/users/assume-state.ts | 53 +++++ .../src/features/admin/whitelist/selectors.ts | 21 -- .../routes/-/admin/annotations/+page.svelte | 90 --------- .../src/routes/-/admin/runtime/+page.svelte | 30 --- .../src/routes/-/admin/users/+page.svelte | 184 ------------------ .../routes/-/admin/virtual-files/+page.svelte | 41 ---- .../src/routes/-/admin/whitelist/+page.svelte | 131 ------------- 13 files changed, 337 insertions(+), 579 deletions(-) delete mode 100644 web-admin/src/features/admin/shared/ActionResultBanner.svelte create mode 100644 web-admin/src/features/admin/shared/OrgSearchInput.svelte delete mode 100644 web-admin/src/features/admin/shared/StatusBadge.svelte create mode 100644 web-admin/src/features/admin/shared/UserSearchInput.svelte create mode 100644 web-admin/src/features/admin/shared/notify.ts create mode 100644 web-admin/src/features/admin/users/RepresentingBanner.svelte create mode 100644 web-admin/src/features/admin/users/assume-state.ts delete mode 100644 web-admin/src/features/admin/whitelist/selectors.ts delete mode 100644 web-admin/src/routes/-/admin/annotations/+page.svelte delete mode 100644 web-admin/src/routes/-/admin/runtime/+page.svelte delete mode 100644 web-admin/src/routes/-/admin/users/+page.svelte delete mode 100644 web-admin/src/routes/-/admin/virtual-files/+page.svelte delete mode 100644 web-admin/src/routes/-/admin/whitelist/+page.svelte diff --git a/web-admin/src/features/admin/shared/ActionResultBanner.svelte b/web-admin/src/features/admin/shared/ActionResultBanner.svelte deleted file mode 100644 index 13bcb545147..00000000000 --- a/web-admin/src/features/admin/shared/ActionResultBanner.svelte +++ /dev/null @@ -1,49 +0,0 @@ - - - -{#if type && message} - -{/if} - - diff --git a/web-admin/src/features/admin/shared/OrgSearchInput.svelte b/web-admin/src/features/admin/shared/OrgSearchInput.svelte new file mode 100644 index 00000000000..5d981a5b940 --- /dev/null +++ b/web-admin/src/features/admin/shared/OrgSearchInput.svelte @@ -0,0 +1,118 @@ + + + +
+ + {#if $orgSearchQuery.isFetching && value.length >= 3} +
+ {/if} + {#if showDropdown && orgNames.length > 0} + + {/if} +
+ + diff --git a/web-admin/src/features/admin/shared/StatusBadge.svelte b/web-admin/src/features/admin/shared/StatusBadge.svelte deleted file mode 100644 index bc52339df82..00000000000 --- a/web-admin/src/features/admin/shared/StatusBadge.svelte +++ /dev/null @@ -1,33 +0,0 @@ - - - - - {label} - - - diff --git a/web-admin/src/features/admin/shared/UserSearchInput.svelte b/web-admin/src/features/admin/shared/UserSearchInput.svelte new file mode 100644 index 00000000000..e0950221f4d --- /dev/null +++ b/web-admin/src/features/admin/shared/UserSearchInput.svelte @@ -0,0 +1,107 @@ + + + +
+ + {#if $usersQuery.isFetching && value.length >= 3} +
+ {/if} + {#if showDropdown && emails.length > 0} + + {/if} +
+ + diff --git a/web-admin/src/features/admin/shared/notify.ts b/web-admin/src/features/admin/shared/notify.ts new file mode 100644 index 00000000000..515ed09b97b --- /dev/null +++ b/web-admin/src/features/admin/shared/notify.ts @@ -0,0 +1,9 @@ +import { eventBus } from "@rilldata/web-common/lib/event-bus/event-bus"; + +export function notifySuccess(message: string) { + eventBus.emit("notification", { type: "success", message }); +} + +export function notifyError(message: string) { + eventBus.emit("notification", { type: "error", message }); +} diff --git a/web-admin/src/features/admin/users/RepresentingBanner.svelte b/web-admin/src/features/admin/users/RepresentingBanner.svelte new file mode 100644 index 00000000000..a834fdd734c --- /dev/null +++ b/web-admin/src/features/admin/users/RepresentingBanner.svelte @@ -0,0 +1,50 @@ + + diff --git a/web-admin/src/features/admin/users/assume-state.ts b/web-admin/src/features/admin/users/assume-state.ts new file mode 100644 index 00000000000..a770d05bc1a --- /dev/null +++ b/web-admin/src/features/admin/users/assume-state.ts @@ -0,0 +1,53 @@ +// Manages assume/unassume user state for the admin console. +// Uses sessionStorage to track the assumed user across navigations, +// and server-side auth endpoints for cookie management. +import { writable } from "svelte/store"; +import { browser } from "$app/environment"; +import { ADMIN_URL } from "@rilldata/web-admin/client/http-client"; + +export const STORAGE_KEY = "rill-representing-user"; + +function appendPath(path: string, suffix: string) { + return `${path.replace(/\/$/, "")}/${suffix}`; +} + +// Store tracks the currently assumed user email +const initial = browser ? sessionStorage.getItem(STORAGE_KEY) ?? "" : ""; +const { subscribe, set } = writable(initial); + +export const assumedUser = { + subscribe, + + /** + * Navigates to Rill Cloud as the given user in the current tab. + * Stores the email in sessionStorage so the banner knows who we're browsing as. + */ + assume(email: string, ttlMinutes = 60) { + set(email); + if (browser) sessionStorage.setItem(STORAGE_KEY, email); + + const u = new URL(ADMIN_URL); + u.pathname = appendPath(u.pathname, "auth/assume-open"); + u.searchParams.set("representing_user", email); + u.searchParams.set("ttl_minutes", String(ttlMinutes)); + window.location.href = u.toString(); + }, + + /** + * Reverts to the original superuser session. + * Redirects to /auth/login which re-authenticates through the auth provider. + * Since the superuser's auth provider session is still active, this auto-completes + * and issues a fresh superuser cookie, effectively "unassuming". + */ + unassume() { + set(""); + if (browser) sessionStorage.removeItem(STORAGE_KEY); + + // Redirect to login; the auth provider (Auth0) session is the real superuser, + // so it auto-completes and issues a fresh superuser token. + const u = new URL(ADMIN_URL); + u.pathname = appendPath(u.pathname, "auth/login"); + u.searchParams.set("redirect", window.location.origin); + window.location.href = u.toString(); + }, +}; diff --git a/web-admin/src/features/admin/whitelist/selectors.ts b/web-admin/src/features/admin/whitelist/selectors.ts deleted file mode 100644 index 715c2c9248c..00000000000 --- a/web-admin/src/features/admin/whitelist/selectors.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { - createAdminServiceCreateWhitelistedDomain, - createAdminServiceRemoveWhitelistedDomain, - createAdminServiceListWhitelistedDomains, -} from "@rilldata/web-admin/client"; - -export function getWhitelistedDomains(org: string) { - return createAdminServiceListWhitelistedDomains( - org, - { superuserForceAccess: true }, - { query: { enabled: org.length > 0 } }, - ); -} - -export function createAddWhitelistMutation() { - return createAdminServiceCreateWhitelistedDomain(); -} - -export function createRemoveWhitelistMutation() { - return createAdminServiceRemoveWhitelistedDomain(); -} diff --git a/web-admin/src/routes/-/admin/annotations/+page.svelte b/web-admin/src/routes/-/admin/annotations/+page.svelte deleted file mode 100644 index 21c177e023d..00000000000 --- a/web-admin/src/routes/-/admin/annotations/+page.svelte +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - -
-
- - - -
-
- - -
- -
- - diff --git a/web-admin/src/routes/-/admin/runtime/+page.svelte b/web-admin/src/routes/-/admin/runtime/+page.svelte deleted file mode 100644 index 85b2f582369..00000000000 --- a/web-admin/src/routes/-/admin/runtime/+page.svelte +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - -
-

Runtime management will be available in a future update. Use the CLI for now:

-
rill sudo runtime list-instances <host>
-rill sudo runtime delete-instance <host> <instance_id>
-rill sudo runtime manager-token <host>
-
- - diff --git a/web-admin/src/routes/-/admin/users/+page.svelte b/web-admin/src/routes/-/admin/users/+page.svelte deleted file mode 100644 index 75e4d6b9613..00000000000 --- a/web-admin/src/routes/-/admin/users/+page.svelte +++ /dev/null @@ -1,184 +0,0 @@ - - - - - - - -
- -
- -{#if $usersQuery.isLoading && searchQuery.length >= 2} -

Searching...

-{:else if $usersQuery.data?.users?.length} -
- - - - - - - - - - - {#each $usersQuery.data.users as user} - - - - - - - {/each} - -
EmailDisplay NameCreatedActions
{user.email}{user.displayName ?? "-"} - {user.createdOn - ? new Date(user.createdOn).toLocaleDateString() - : "-"} - -
- - - -
-
-
-{:else if searchQuery.length >= 2 && $usersQuery.isSuccess} -

No users found for "{searchQuery}"

-{/if} - - - - diff --git a/web-admin/src/routes/-/admin/virtual-files/+page.svelte b/web-admin/src/routes/-/admin/virtual-files/+page.svelte deleted file mode 100644 index e5ae4c824c9..00000000000 --- a/web-admin/src/routes/-/admin/virtual-files/+page.svelte +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - -
-
- - - -
-

Virtual file management will be available in a future update.

-
- - diff --git a/web-admin/src/routes/-/admin/whitelist/+page.svelte b/web-admin/src/routes/-/admin/whitelist/+page.svelte deleted file mode 100644 index 7c463d61bd3..00000000000 --- a/web-admin/src/routes/-/admin/whitelist/+page.svelte +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - -
-
-
- e.key === "Enter" && handleLookup()} /> - -
- - {#if activeOrg} -
- - - -
- - {#if $domainsQuery.data?.domains?.length} - - - - - - {#each $domainsQuery.data.domains as d} - - - - - - {/each} - -
DomainRoleActions
{d.domain}{d.role} - -
- {:else if $domainsQuery.isSuccess} -

No whitelisted domains.

- {/if} - {/if} -
-
- - - - From f2766845ef6b62532c552898dff64f160d09d955 Mon Sep 17 00:00:00 2001 From: ericokuma Date: Fri, 20 Mar 2026 23:54:12 -0700 Subject: [PATCH 26/35] fix(admin-console): sync sidebar, selectors with intended state - Remove Dashboard, Whitelist, and Advanced nav sections from sidebar - Point Users link to root admin page (/-/admin) - Fix users/selectors: use emailPattern with wildcards, threshold >= 3, remove unused assume/unassume mutations - Fix projects/selectors: add wildcard wrapping, threshold >= 3 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../features/admin/layout/AdminSidebar.svelte | 15 +-------------- .../src/features/admin/projects/selectors.ts | 4 ++-- web-admin/src/features/admin/users/selectors.ts | 16 +++------------- 3 files changed, 6 insertions(+), 29 deletions(-) diff --git a/web-admin/src/features/admin/layout/AdminSidebar.svelte b/web-admin/src/features/admin/layout/AdminSidebar.svelte index 94e97ac81ab..12da6eb7d44 100644 --- a/web-admin/src/features/admin/layout/AdminSidebar.svelte +++ b/web-admin/src/features/admin/layout/AdminSidebar.svelte @@ -3,14 +3,10 @@ import { page } from "$app/stores"; const navGroups = [ - { - heading: "Overview", - items: [{ label: "Dashboard", href: "/-/admin" }], - }, { heading: "People", items: [ - { label: "Users", href: "/-/admin/users" }, + { label: "Users", href: "/-/admin" }, { label: "Superusers", href: "/-/admin/superusers" }, ], }, @@ -26,15 +22,6 @@ items: [ { label: "Organizations", href: "/-/admin/organizations" }, { label: "Projects", href: "/-/admin/projects" }, - { label: "Whitelist", href: "/-/admin/whitelist" }, - ], - }, - { - heading: "Advanced", - items: [ - { label: "Annotations", href: "/-/admin/annotations" }, - { label: "Virtual Files", href: "/-/admin/virtual-files" }, - { label: "Runtime", href: "/-/admin/runtime" }, ], }, ]; diff --git a/web-admin/src/features/admin/projects/selectors.ts b/web-admin/src/features/admin/projects/selectors.ts index 9f7b8121d07..b1c2595ab1c 100644 --- a/web-admin/src/features/admin/projects/selectors.ts +++ b/web-admin/src/features/admin/projects/selectors.ts @@ -9,8 +9,8 @@ import { export function searchProjects(namePattern: string) { return createAdminServiceSearchProjectNames( - { namePattern, pageSize: 50 }, - { query: { enabled: namePattern.length >= 2 } }, + { namePattern: `%${namePattern}%`, pageSize: 50 }, + { query: { enabled: namePattern.length >= 3 } }, ); } diff --git a/web-admin/src/features/admin/users/selectors.ts b/web-admin/src/features/admin/users/selectors.ts index 859f37dbc4a..e72ea26636d 100644 --- a/web-admin/src/features/admin/users/selectors.ts +++ b/web-admin/src/features/admin/users/selectors.ts @@ -1,26 +1,16 @@ // web-admin/src/features/admin/users/selectors.ts import { createAdminServiceSearchUsers, - createAdminServiceIssueRepresentativeAuthToken, - createAdminServiceRevokeRepresentativeAuthTokens, createAdminServiceDeleteUser, } from "@rilldata/web-admin/client"; -export function searchUsers(emailQuery: string) { +export function searchUsers(emailPattern: string) { return createAdminServiceSearchUsers( - { emailQuery }, - { query: { enabled: emailQuery.length >= 2 } }, + { emailPattern: `%${emailPattern}%` }, + { query: { enabled: emailPattern.length >= 3 } }, ); } -export function createAssumeUserMutation() { - return createAdminServiceIssueRepresentativeAuthToken(); -} - -export function createUnassumeUserMutation() { - return createAdminServiceRevokeRepresentativeAuthTokens(); -} - export function createDeleteUserMutation() { return createAdminServiceDeleteUser(); } From 1580e70849a501e469f9d8c61cbae0356b8f9709 Mon Sep 17 00:00:00 2001 From: ericokuma Date: Sat, 21 Mar 2026 00:06:24 -0700 Subject: [PATCH 27/35] fix(admin-console): move admin link to profile menu, fix billing 500, mount assume banner - Move Admin Console link from top nav (OrgHeader) to profile dropdown (AvatarButton), removing duplicate ListSuperusers query from OrgHeader - Add missing getBillingSetupURL function to billing selectors (was imported but never defined, causing 500 on billing page) - Mount RepresentingBanner in root layout so assume-user banner appears when browsing as another user Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/features/admin/billing/selectors.ts | 9 ++++++++ .../authentication/AvatarButton.svelte | 17 +++++++++++++- .../features/organizations/OrgHeader.svelte | 22 +------------------ web-admin/src/routes/+layout.svelte | 2 ++ 4 files changed, 28 insertions(+), 22 deletions(-) diff --git a/web-admin/src/features/admin/billing/selectors.ts b/web-admin/src/features/admin/billing/selectors.ts index 62f9aba8c44..34effd26dca 100644 --- a/web-admin/src/features/admin/billing/selectors.ts +++ b/web-admin/src/features/admin/billing/selectors.ts @@ -1,5 +1,6 @@ // web-admin/src/features/admin/billing/selectors.ts import { + adminServiceGetPaymentsPortalURL, createAdminServiceSudoExtendTrial, createAdminServiceSudoTriggerBillingRepair, createAdminServiceSudoDeleteOrganizationBillingIssue, @@ -7,6 +8,14 @@ import { createAdminServiceListOrganizationBillingIssues, } from "@rilldata/web-admin/client"; +export async function getBillingSetupURL(org: string): Promise { + const resp = await adminServiceGetPaymentsPortalURL(org, { + setup: true, + superuserForceAccess: true, + }); + return resp.url ?? ""; +} + export function createExtendTrialMutation() { return createAdminServiceSudoExtendTrial(); } diff --git a/web-admin/src/features/authentication/AvatarButton.svelte b/web-admin/src/features/authentication/AvatarButton.svelte index 95dc0bb5d04..5fe56042474 100644 --- a/web-admin/src/features/authentication/AvatarButton.svelte +++ b/web-admin/src/features/authentication/AvatarButton.svelte @@ -22,12 +22,22 @@ type UserLike, } from "@rilldata/web-common/features/help/initPylonChat"; import { posthogIdentify } from "@rilldata/web-common/lib/analytics/posthog"; - import { createAdminServiceGetCurrentUser } from "../../client"; + import { + createAdminServiceGetCurrentUser, + createAdminServiceListSuperusers, + } from "../../client"; import ProjectAccessControls from "../projects/ProjectAccessControls.svelte"; import ViewAsUserPopover from "../view-as-user/ViewAsUserPopover.svelte"; import ThemeToggle from "@rilldata/web-common/features/themes/ThemeToggle.svelte"; const user = createAdminServiceGetCurrentUser(); + const superusers = createAdminServiceListSuperusers(); + $: isSuperuser = + $superusers.isSuccess && + !!$user.data?.user?.email && + ($superusers.data?.users ?? []).some( + (su) => su.email === $user.data?.user?.email, + ); let imgContainer: HTMLElement; let primaryMenuOpen = false; @@ -126,6 +136,11 @@ {/if} {/if} + {#if isSuperuser} + Admin Console + + {/if} + diff --git a/web-admin/src/features/organizations/OrgHeader.svelte b/web-admin/src/features/organizations/OrgHeader.svelte index 5507aaa0228..31502a53960 100644 --- a/web-admin/src/features/organizations/OrgHeader.svelte +++ b/web-admin/src/features/organizations/OrgHeader.svelte @@ -3,10 +3,7 @@ import Breadcrumbs from "@rilldata/web-common/components/navigation/breadcrumbs/Breadcrumbs.svelte"; import Header from "@rilldata/web-common/layout/header/Header.svelte"; import HeaderLogo from "@rilldata/web-common/layout/header/HeaderLogo.svelte"; - import { - createAdminServiceGetCurrentUser, - createAdminServiceListSuperusers, - } from "../../client"; + import { createAdminServiceGetCurrentUser } from "../../client"; import { useBreadcrumbOrgPaths, useBreadcrumbProjectPaths, @@ -30,15 +27,6 @@ $: loggedIn = !!$user.data?.user; $: rillLogoHref = !loggedIn ? "https://www.rilldata.com" : "/"; - // Check if the current user is a superuser; the ListSuperusers call returns 403 for non-superusers - const superusers = createAdminServiceListSuperusers(); - $: isSuperuser = - $superusers.isSuccess && - !!$user.data?.user?.email && - ($superusers.data?.users ?? []).some( - (su) => su.email === $user.data?.user?.email, - ); - $: orgPathsQuery = useBreadcrumbOrgPaths( loggedIn, organization, @@ -60,14 +48,6 @@ {/if}
- {#if isSuperuser} - - Admin - - {/if} {#if $user.isSuccess} {#if $user.data?.user} diff --git a/web-admin/src/routes/+layout.svelte b/web-admin/src/routes/+layout.svelte index 76ba904d827..63c863532ad 100644 --- a/web-admin/src/routes/+layout.svelte +++ b/web-admin/src/routes/+layout.svelte @@ -5,6 +5,7 @@ import { createUserFacingError } from "@rilldata/web-admin/components/errors/user-facing-errors"; import { dynamicHeight } from "@rilldata/web-common/layout/layout-settings.ts"; import BillingBannerManager from "@rilldata/web-admin/features/billing/banner/BillingBannerManager.svelte"; + import RepresentingBanner from "@rilldata/web-admin/features/admin/users/RepresentingBanner.svelte"; import { isBillingUpgradePage, isProjectInvitePage, @@ -144,6 +145,7 @@ use:pageContentSizeHandler > + {#if !hideBillingManager} {/if} From 11f34d286e79d2a7db17155e7555b8aab5ed0570 Mon Sep 17 00:00:00 2001 From: ericokuma Date: Sat, 21 Mar 2026 00:55:46 -0700 Subject: [PATCH 28/35] fix(admin-console): replace `@apply` PostCSS with inline Tailwind classes svelte-check cannot parse `@apply` with `dark:` variants in ` diff --git a/web-admin/src/features/admin/layout/AdminSidebar.svelte b/web-admin/src/features/admin/layout/AdminSidebar.svelte index 12da6eb7d44..8035dca503c 100644 --- a/web-admin/src/features/admin/layout/AdminSidebar.svelte +++ b/web-admin/src/features/admin/layout/AdminSidebar.svelte @@ -32,20 +32,32 @@ } -