@nurix/etna 0.18.19 → 0.18.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/data/harness-index.json +1 -1
- package/data/harnesses/code/CLAUDE.md +1 -1
- package/data/harnesses/code/agents/brainstorm.md +1 -1
- package/data/harnesses/code/agents/maintenance.md +2 -2
- package/data/harnesses/code/harness.json +2 -2
- package/data/harnesses/code/rules/database.md +38 -0
- package/data/harnesses/code/rules/express.md +28 -0
- package/data/harnesses/code/rules/frontend.md +14 -0
- package/data/harnesses/code/rules/general.md +2 -40
- package/data/harnesses/code/rules/nomenclature.md +1 -8
- package/data/harnesses/code/rules/nustack.md +150 -0
- package/data/harnesses/code/skills/brainstorm/README.md +1 -1
- package/data/harnesses/code/skills/brainstorm/SKILL.md +1 -1
- package/data/harnesses/code/skills/brainstorm/exit-artifacts.md +4 -4
- package/data/harnesses/code/skills/brainstorm/templates/source-of-truth.md +3 -3
- package/data/harnesses/code/skills/design/SKILL.md +68 -0
- package/data/harnesses/code/skills/design/data/catalog/rules.csv +132 -0
- package/data/harnesses/code/skills/design/data/combinations/Readme.md +29 -0
- package/data/harnesses/code/skills/design/data/combinations/charts.csv +33 -0
- package/data/harnesses/code/skills/design/data/combinations/fonts-known.json +437 -0
- package/data/harnesses/code/skills/design/data/combinations/icons.csv +157 -0
- package/data/harnesses/code/skills/design/data/combinations/landing.csv +38 -0
- package/data/harnesses/code/skills/design/data/combinations/lucide-names.json +2001 -0
- package/data/harnesses/code/skills/design/data/combinations/manifest.json +283 -0
- package/data/harnesses/code/skills/design/data/combinations/palettes.csv +49 -0
- package/data/harnesses/code/skills/design/data/combinations/product-router.csv +80 -0
- package/data/harnesses/code/skills/design/data/combinations/styles.csv +54 -0
- package/data/harnesses/code/skills/design/data/combinations/typography.csv +61 -0
- package/data/harnesses/code/skills/design/data/design-md/SCHEMA.md +106 -0
- package/data/harnesses/code/skills/design/data/design-md/library/airbnb.md +154 -0
- package/data/harnesses/code/skills/design/data/design-md/library/airtable.md +151 -0
- package/data/harnesses/code/skills/design/data/design-md/library/anthropic-claude.md +139 -0
- package/data/harnesses/code/skills/design/data/design-md/library/apple.md +139 -0
- package/data/harnesses/code/skills/design/data/design-md/library/binance.md +140 -0
- package/data/harnesses/code/skills/design/data/design-md/library/bmw.md +140 -0
- package/data/harnesses/code/skills/design/data/design-md/library/cal.md +140 -0
- package/data/harnesses/code/skills/design/data/design-md/library/clay.md +139 -0
- package/data/harnesses/code/skills/design/data/design-md/library/clickhouse.md +139 -0
- package/data/harnesses/code/skills/design/data/design-md/library/cohere.md +147 -0
- package/data/harnesses/code/skills/design/data/design-md/library/coinbase.md +156 -0
- package/data/harnesses/code/skills/design/data/design-md/library/composio.md +138 -0
- package/data/harnesses/code/skills/design/data/design-md/library/cursor.md +138 -0
- package/data/harnesses/code/skills/design/data/design-md/library/elevenlabs.md +137 -0
- package/data/harnesses/code/skills/design/data/design-md/library/expo.md +148 -0
- package/data/harnesses/code/skills/design/data/design-md/library/figma.md +141 -0
- package/data/harnesses/code/skills/design/data/design-md/library/framer.md +156 -0
- package/data/harnesses/code/skills/design/data/design-md/library/hashicorp.md +140 -0
- package/data/harnesses/code/skills/design/data/design-md/library/ibm.md +153 -0
- package/data/harnesses/code/skills/design/data/design-md/library/intercom.md +158 -0
- package/data/harnesses/code/skills/design/data/design-md/library/kraken.md +156 -0
- package/data/harnesses/code/skills/design/data/design-md/library/linear-app.md +122 -0
- package/data/harnesses/code/skills/design/data/design-md/library/lovable.md +140 -0
- package/data/harnesses/code/skills/design/data/design-md/library/meta.md +139 -0
- package/data/harnesses/code/skills/design/data/design-md/library/minimax.md +139 -0
- package/data/harnesses/code/skills/design/data/design-md/library/mintlify.md +139 -0
- package/data/harnesses/code/skills/design/data/design-md/library/miro.md +153 -0
- package/data/harnesses/code/skills/design/data/design-md/library/mistral-ai.md +140 -0
- package/data/harnesses/code/skills/design/data/design-md/library/mongodb.md +155 -0
- package/data/harnesses/code/skills/design/data/design-md/library/notion.md +140 -0
- package/data/harnesses/code/skills/design/data/design-md/library/nvidia.md +141 -0
- package/data/harnesses/code/skills/design/data/design-md/library/ollama.md +145 -0
- package/data/harnesses/code/skills/design/data/design-md/library/opencode-ai.md +142 -0
- package/data/harnesses/code/skills/design/data/design-md/library/posthog.md +152 -0
- package/data/harnesses/code/skills/design/data/design-md/library/raycast.md +137 -0
- package/data/harnesses/code/skills/design/data/design-md/library/replicate.md +140 -0
- package/data/harnesses/code/skills/design/data/design-md/library/resend.md +170 -0
- package/data/harnesses/code/skills/design/data/design-md/library/revolut.md +145 -0
- package/data/harnesses/code/skills/design/data/design-md/library/runwayml.md +137 -0
- package/data/harnesses/code/skills/design/data/design-md/library/sanity.md +168 -0
- package/data/harnesses/code/skills/design/data/design-md/library/sentry.md +163 -0
- package/data/harnesses/code/skills/design/data/design-md/library/shopify.md +147 -0
- package/data/harnesses/code/skills/design/data/design-md/library/slack.md +158 -0
- package/data/harnesses/code/skills/design/data/design-md/library/spacex.md +171 -0
- package/data/harnesses/code/skills/design/data/design-md/library/spotify.md +166 -0
- package/data/harnesses/code/skills/design/data/design-md/library/stripe.md +160 -0
- package/data/harnesses/code/skills/design/data/design-md/library/supabase.md +173 -0
- package/data/harnesses/code/skills/design/data/design-md/library/superhuman.md +145 -0
- package/data/harnesses/code/skills/design/data/design-md/library/tesla.md +153 -0
- package/data/harnesses/code/skills/design/data/design-md/library/theverge.md +167 -0
- package/data/harnesses/code/skills/design/data/design-md/library/together-ai.md +140 -0
- package/data/harnesses/code/skills/design/data/design-md/library/uber.md +140 -0
- package/data/harnesses/code/skills/design/data/design-md/library/vercel.md +140 -0
- package/data/harnesses/code/skills/design/data/design-md/library/voltagent.md +130 -0
- package/data/harnesses/code/skills/design/data/design-md/library/warp.md +140 -0
- package/data/harnesses/code/skills/design/data/design-md/library/webflow.md +140 -0
- package/data/harnesses/code/skills/design/data/design-md/library/wired.md +139 -0
- package/data/harnesses/code/skills/design/data/design-md/library/wise.md +137 -0
- package/data/harnesses/code/skills/design/data/design-md/library/x-ai.md +138 -0
- package/data/harnesses/code/skills/design/data/design-md/library/zapier.md +158 -0
- package/data/harnesses/code/skills/design/data/design-md/nurix.md +134 -0
- package/data/harnesses/code/skills/design/data/design-tokens/SCHEMA.md +580 -0
- package/data/harnesses/code/skills/design/data/design-tokens/airbnb/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/airbnb/design.tokens.json +964 -0
- package/data/harnesses/code/skills/design/data/design-tokens/airtable/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/airtable/design.tokens.json +960 -0
- package/data/harnesses/code/skills/design/data/design-tokens/anthropic-claude/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/anthropic-claude/design.tokens.json +965 -0
- package/data/harnesses/code/skills/design/data/design-tokens/apple/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/apple/design.tokens.json +954 -0
- package/data/harnesses/code/skills/design/data/design-tokens/binance/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/binance/design.tokens.json +949 -0
- package/data/harnesses/code/skills/design/data/design-tokens/bmw/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/bmw/design.tokens.json +962 -0
- package/data/harnesses/code/skills/design/data/design-tokens/cal/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/cal/design.tokens.json +958 -0
- package/data/harnesses/code/skills/design/data/design-tokens/clay/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/clay/design.tokens.json +953 -0
- package/data/harnesses/code/skills/design/data/design-tokens/clickhouse/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/clickhouse/design.tokens.json +960 -0
- package/data/harnesses/code/skills/design/data/design-tokens/cohere/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/cohere/design.tokens.json +973 -0
- package/data/harnesses/code/skills/design/data/design-tokens/coinbase/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/coinbase/design.tokens.json +954 -0
- package/data/harnesses/code/skills/design/data/design-tokens/composio/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/composio/design.tokens.json +963 -0
- package/data/harnesses/code/skills/design/data/design-tokens/cursor/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/cursor/design.tokens.json +968 -0
- package/data/harnesses/code/skills/design/data/design-tokens/elevenlabs/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/elevenlabs/design.tokens.json +981 -0
- package/data/harnesses/code/skills/design/data/design-tokens/expo/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/expo/design.tokens.json +962 -0
- package/data/harnesses/code/skills/design/data/design-tokens/figma/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/figma/design.tokens.json +964 -0
- package/data/harnesses/code/skills/design/data/design-tokens/framer/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/framer/design.tokens.json +959 -0
- package/data/harnesses/code/skills/design/data/design-tokens/goldens.json +251 -0
- package/data/harnesses/code/skills/design/data/design-tokens/hashicorp/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/hashicorp/design.tokens.json +965 -0
- package/data/harnesses/code/skills/design/data/design-tokens/ibm/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/ibm/design.tokens.json +978 -0
- package/data/harnesses/code/skills/design/data/design-tokens/intercom/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/intercom/design.tokens.json +982 -0
- package/data/harnesses/code/skills/design/data/design-tokens/kraken/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/kraken/design.tokens.json +958 -0
- package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.resolver.json +23 -0
- package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.tokens.dark.json +589 -0
- package/data/harnesses/code/skills/design/data/design-tokens/linear-app/design.tokens.json +964 -0
- package/data/harnesses/code/skills/design/data/design-tokens/lovable/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/lovable/design.tokens.json +955 -0
- package/data/harnesses/code/skills/design/data/design-tokens/meta/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/meta/design.tokens.json +977 -0
- package/data/harnesses/code/skills/design/data/design-tokens/minimax/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/minimax/design.tokens.json +950 -0
- package/data/harnesses/code/skills/design/data/design-tokens/mintlify/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/mintlify/design.tokens.json +976 -0
- package/data/harnesses/code/skills/design/data/design-tokens/miro/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/miro/design.tokens.json +979 -0
- package/data/harnesses/code/skills/design/data/design-tokens/mistral-ai/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/mistral-ai/design.tokens.json +955 -0
- package/data/harnesses/code/skills/design/data/design-tokens/mongodb/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/mongodb/design.tokens.json +958 -0
- package/data/harnesses/code/skills/design/data/design-tokens/notion/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/notion/design.tokens.json +953 -0
- package/data/harnesses/code/skills/design/data/design-tokens/nurix/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/nurix/design.tokens.json +904 -0
- package/data/harnesses/code/skills/design/data/design-tokens/nvidia/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/nvidia/design.tokens.json +957 -0
- package/data/harnesses/code/skills/design/data/design-tokens/ollama/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/ollama/design.tokens.json +945 -0
- package/data/harnesses/code/skills/design/data/design-tokens/opencode-ai/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/opencode-ai/design.tokens.json +960 -0
- package/data/harnesses/code/skills/design/data/design-tokens/posthog/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/posthog/design.tokens.json +953 -0
- package/data/harnesses/code/skills/design/data/design-tokens/raycast/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/raycast/design.tokens.json +955 -0
- package/data/harnesses/code/skills/design/data/design-tokens/replicate/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/replicate/design.tokens.json +958 -0
- package/data/harnesses/code/skills/design/data/design-tokens/resend/design.resolver.json +23 -0
- package/data/harnesses/code/skills/design/data/design-tokens/resend/design.tokens.dark.json +613 -0
- package/data/harnesses/code/skills/design/data/design-tokens/resend/design.tokens.json +968 -0
- package/data/harnesses/code/skills/design/data/design-tokens/revolut/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/revolut/design.tokens.json +956 -0
- package/data/harnesses/code/skills/design/data/design-tokens/runwayml/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/runwayml/design.tokens.json +960 -0
- package/data/harnesses/code/skills/design/data/design-tokens/sanity/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/sanity/design.tokens.json +958 -0
- package/data/harnesses/code/skills/design/data/design-tokens/sentry/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/sentry/design.tokens.json +980 -0
- package/data/harnesses/code/skills/design/data/design-tokens/shopify/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/shopify/design.tokens.json +964 -0
- package/data/harnesses/code/skills/design/data/design-tokens/slack/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/slack/design.tokens.json +957 -0
- package/data/harnesses/code/skills/design/data/design-tokens/spacex/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/spacex/design.tokens.json +979 -0
- package/data/harnesses/code/skills/design/data/design-tokens/spotify/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/spotify/design.tokens.json +974 -0
- package/data/harnesses/code/skills/design/data/design-tokens/stripe/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/stripe/design.tokens.json +973 -0
- package/data/harnesses/code/skills/design/data/design-tokens/supabase/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/supabase/design.tokens.json +979 -0
- package/data/harnesses/code/skills/design/data/design-tokens/superhuman/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/superhuman/design.tokens.json +966 -0
- package/data/harnesses/code/skills/design/data/design-tokens/tesla/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/tesla/design.tokens.json +961 -0
- package/data/harnesses/code/skills/design/data/design-tokens/theverge/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/theverge/design.tokens.json +977 -0
- package/data/harnesses/code/skills/design/data/design-tokens/together-ai/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/together-ai/design.tokens.json +991 -0
- package/data/harnesses/code/skills/design/data/design-tokens/uber/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/uber/design.tokens.json +966 -0
- package/data/harnesses/code/skills/design/data/design-tokens/vercel/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/vercel/design.tokens.json +988 -0
- package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.resolver.json +23 -0
- package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.tokens.dark.json +602 -0
- package/data/harnesses/code/skills/design/data/design-tokens/voltagent/design.tokens.json +974 -0
- package/data/harnesses/code/skills/design/data/design-tokens/warp/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/warp/design.tokens.json +979 -0
- package/data/harnesses/code/skills/design/data/design-tokens/webflow/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/webflow/design.tokens.json +960 -0
- package/data/harnesses/code/skills/design/data/design-tokens/wired/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/wired/design.tokens.json +968 -0
- package/data/harnesses/code/skills/design/data/design-tokens/wise/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/wise/design.tokens.json +965 -0
- package/data/harnesses/code/skills/design/data/design-tokens/x-ai/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/x-ai/design.tokens.json +988 -0
- package/data/harnesses/code/skills/design/data/design-tokens/zapier/design.resolver.json +20 -0
- package/data/harnesses/code/skills/design/data/design-tokens/zapier/design.tokens.json +978 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/command-center.tsx +249 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/create-edit.tsx +249 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/dashboard.tsx +248 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/entity-detail.tsx +245 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/list-detail.tsx +223 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/log-stream.tsx +249 -0
- package/data/harnesses/code/skills/design/data/operator-pack/fixtures.md +88 -0
- package/data/harnesses/code/skills/design/data/operator-pack/surface-catalog.md +132 -0
- package/data/harnesses/code/skills/design/data/operator-pack/trust-rubric.md +76 -0
- package/data/harnesses/code/skills/design/references/create.md +131 -0
- package/data/harnesses/code/skills/design/references/diagnosis.md +108 -0
- package/data/harnesses/code/skills/design/references/hard-rules.md +155 -0
- package/data/harnesses/code/skills/design/references/improve.md +87 -0
- package/data/harnesses/code/skills/design/references/interaction.md +77 -0
- package/data/harnesses/code/skills/design/references/introspect.md +100 -0
- package/data/harnesses/code/skills/design/references/lifecycle.md +73 -0
- package/data/harnesses/code/skills/design/references/personality.md +428 -0
- package/data/harnesses/code/skills/design/references/structure.md +138 -0
- package/data/harnesses/code/skills/design/scripts/convert-design-md.mjs +1032 -0
- package/data/harnesses/code/skills/design/scripts/derive-personality.mjs +1006 -0
- package/data/harnesses/code/skills/design/scripts/generate-fixtures.mjs +551 -0
- package/data/harnesses/code/skills/design/scripts/lib/checks.mjs +903 -0
- package/data/harnesses/code/skills/design/scripts/lib/golden.mjs +26 -0
- package/data/harnesses/code/skills/design/scripts/phase-gate.mjs +454 -0
- package/data/harnesses/code/skills/design/scripts/preship-sweep.mjs +267 -0
- package/data/harnesses/code/skills/design/scripts/render-rung.mjs +690 -0
- package/data/harnesses/code/skills/design/scripts/scaffold.mjs +552 -0
- package/data/harnesses/code/skills/design/scripts/self-integrity.mjs +600 -0
- package/data/harnesses/code/skills/design/scripts/snapshot-goldens.mjs +68 -0
- package/data/harnesses/code/skills/design/scripts/validate-combinations.mjs +314 -0
- package/data/harnesses/code/skills/documentation/SKILL.md +61 -17
- package/data/harnesses/code/skills/documentation/references/examples.md +41 -0
- package/data/harnesses/code/skills/documentation/references/style-guide.md +14 -9
- package/data/harnesses/code/skills/documentation/references/templates.md +305 -51
- package/data/harnesses/etna/CLAUDE.md +1 -1
- package/data/harnesses/etna/agents/brainstorm.md +1 -1
- package/data/harnesses/etna/agents/maintenance.md +2 -2
- package/data/harnesses/etna/harness.json +2 -2
- package/data/harnesses/etna/rules/general.md +2 -40
- package/data/harnesses/etna/rules/nomenclature.md +1 -8
- package/data/harnesses/etna/skills/brainstorm/README.md +1 -1
- package/data/harnesses/etna/skills/brainstorm/SKILL.md +1 -1
- package/data/harnesses/etna/skills/brainstorm/exit-artifacts.md +4 -4
- package/data/harnesses/etna/skills/brainstorm/templates/source-of-truth.md +3 -3
- package/data/harnesses/etna/skills/documentation/SKILL.md +61 -17
- package/data/harnesses/etna/skills/documentation/references/examples.md +41 -0
- package/data/harnesses/etna/skills/documentation/references/style-guide.md +14 -9
- package/data/harnesses/etna/skills/documentation/references/templates.md +305 -51
- package/data/harnesses/mobile/CLAUDE.md +1 -1
- package/data/harnesses/mobile/agents/brainstorm.md +1 -1
- package/data/harnesses/mobile/agents/maintenance.md +2 -2
- package/data/harnesses/mobile/harness.json +2 -2
- package/data/harnesses/mobile/rules/database.md +38 -0
- package/data/harnesses/mobile/rules/general.md +2 -40
- package/data/harnesses/mobile/rules/nomenclature.md +1 -8
- package/data/harnesses/mobile/rules/nustack.md +150 -0
- package/data/harnesses/mobile/skills/brainstorm/README.md +1 -1
- package/data/harnesses/mobile/skills/brainstorm/SKILL.md +1 -1
- package/data/harnesses/mobile/skills/brainstorm/exit-artifacts.md +4 -4
- package/data/harnesses/mobile/skills/brainstorm/templates/source-of-truth.md +3 -3
- package/data/harnesses/mobile/skills/documentation/SKILL.md +61 -17
- package/data/harnesses/mobile/skills/documentation/references/examples.md +41 -0
- package/data/harnesses/mobile/skills/documentation/references/style-guide.md +14 -9
- package/data/harnesses/mobile/skills/documentation/references/templates.md +305 -51
- package/package.json +1 -1
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// skills/design/data/operator-pack/exemplars/dashboard.tsx — personality-invariant: composition anchor, not a look source.
|
|
2
|
+
import { useMemo, useState, type ReactNode } from "react";
|
|
3
|
+
import {
|
|
4
|
+
Activity, ArrowDown, ArrowUp, ArrowUpRight, ChevronsUpDown, CircleAlert,
|
|
5
|
+
Clock, Database, Plug, RefreshCw, TriangleAlert,
|
|
6
|
+
} from "lucide-react";
|
|
7
|
+
import { Alert, AlertDescription, AlertTitle } from "@nurix/components/alert";
|
|
8
|
+
import { Badge } from "@nurix/components/badge";
|
|
9
|
+
import { Button } from "@nurix/components/button";
|
|
10
|
+
import { EmptyState } from "@nurix/components/empty-state";
|
|
11
|
+
import { Skeleton } from "@nurix/components/skeleton";
|
|
12
|
+
import {
|
|
13
|
+
Statistics, StatisticsActions, StatisticsCaption, StatisticsDescription, StatisticsFooter,
|
|
14
|
+
StatisticsHeader, StatisticsHeading, StatisticsIcon, StatisticsLegend, StatisticsLegendItem,
|
|
15
|
+
StatisticsLink, StatisticsMetric, StatisticsSegmentedBar, StatisticsSparkline, StatisticsTitle,
|
|
16
|
+
StatisticsTrend, StatisticsUnit, StatisticsValue, type StatisticsSegment,
|
|
17
|
+
} from "@nurix/components/statistics";
|
|
18
|
+
|
|
19
|
+
type SurfaceStatus = "loading" | "ready" | "empty" | "error";
|
|
20
|
+
type SortKey = "records" | "errorRate";
|
|
21
|
+
type SortDir = "asc" | "desc";
|
|
22
|
+
type Health = "healthy" | "elevated" | "failing";
|
|
23
|
+
interface KpiDatum { key: string; label: string; value: string; unit?: string; trend: number; invertTrend?: boolean; spark: number[]; chart: string; stale?: boolean }
|
|
24
|
+
interface ConnectorRow { id: string; name: string; kind: string; records: number; errorRate: number; health: Health }
|
|
25
|
+
export interface OperationsDashboardProps {
|
|
26
|
+
status?: SurfaceStatus; degraded?: boolean; kpis?: KpiDatum[]; connectors?: ConnectorRow[];
|
|
27
|
+
lastUpdated?: string; onRefresh?: () => void; onRetry?: () => void; onConnectSource?: () => void;
|
|
28
|
+
}
|
|
29
|
+
const TIMEFRAMES = [{ key: "24h", label: "24h" }, { key: "7d", label: "7d" }, { key: "30d", label: "30d" }] as const;
|
|
30
|
+
const HEALTH: Record<Health, { label: string; className: string; icon: ReactNode }> = {
|
|
31
|
+
healthy: { label: "Healthy", className: "text-success", icon: <Activity className="size-4" aria-hidden /> },
|
|
32
|
+
elevated: { label: "Elevated", className: "text-warning", icon: <TriangleAlert className="size-4" aria-hidden /> },
|
|
33
|
+
failing: { label: "Failing", className: "text-destructive", icon: <CircleAlert className="size-4" aria-hidden /> },
|
|
34
|
+
};
|
|
35
|
+
// Signed percentage for a KPI trend chip.
|
|
36
|
+
const formatPct = (n: number) => `${n > 0 ? "+" : ""}${n.toFixed(1)}%`;
|
|
37
|
+
// KPI stat card: headline value, trend, and a per-metric sparkline; the degraded metric shows a cached marker instead.
|
|
38
|
+
function KpiCard({ kpi }: { kpi: KpiDatum }) {
|
|
39
|
+
return (
|
|
40
|
+
<Statistics variant="card" size="md" className="gap-3">
|
|
41
|
+
<StatisticsHeader>
|
|
42
|
+
<StatisticsHeading><StatisticsTitle size="sm">{kpi.label}</StatisticsTitle></StatisticsHeading>
|
|
43
|
+
<StatisticsActions>{kpi.stale
|
|
44
|
+
? <Badge variant="outline" className="gap-1 text-warning"><Clock className="size-3" aria-hidden /> Cached</Badge>
|
|
45
|
+
: <StatisticsIcon variant="primary" tone="soft" size="sm"><Activity className="size-4" aria-hidden /></StatisticsIcon>}</StatisticsActions>
|
|
46
|
+
</StatisticsHeader>
|
|
47
|
+
<StatisticsMetric>
|
|
48
|
+
<StatisticsValue className="tabular-nums">{kpi.value}{/* mock */}</StatisticsValue>
|
|
49
|
+
{kpi.unit ? <StatisticsUnit>{kpi.unit}</StatisticsUnit> : null}
|
|
50
|
+
{kpi.trend !== 0 ? <StatisticsTrend value={kpi.trend} invert={kpi.invertTrend} pill format={formatPct} /> : null}
|
|
51
|
+
</StatisticsMetric>
|
|
52
|
+
<StatisticsFooter>
|
|
53
|
+
{kpi.stale
|
|
54
|
+
? <StatisticsCaption className="text-warning">Last cached at 14:12</StatisticsCaption>
|
|
55
|
+
: <StatisticsSparkline data={kpi.spark} color={kpi.chart} height={28} className="w-full" />}
|
|
56
|
+
</StatisticsFooter>
|
|
57
|
+
</Statistics>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
// One sortable, right-aligned numeric column header carrying its aria-sort state and a keyboard-native toggle.
|
|
61
|
+
function SortHeader({ label, sortKey, active, dir, onSort }: { label: string; sortKey: SortKey; active: boolean; dir: SortDir; onSort: (k: SortKey) => void }) {
|
|
62
|
+
const ariaSort: "none" | "ascending" | "descending" = active ? (dir === "asc" ? "ascending" : "descending") : "none";
|
|
63
|
+
return (
|
|
64
|
+
<th scope="col" aria-sort={ariaSort} className="py-2 pr-4 text-right font-medium">
|
|
65
|
+
<button type="button" onClick={() => onSort(sortKey)} aria-label={`Sort by ${label}`}
|
|
66
|
+
className="inline-flex items-center gap-1 rounded text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
|
|
67
|
+
{label}
|
|
68
|
+
{active ? (dir === "asc" ? <ArrowUp className="size-3" aria-hidden /> : <ArrowDown className="size-3" aria-hidden />) : <ChevronsUpDown className="size-3" aria-hidden />}
|
|
69
|
+
</button>
|
|
70
|
+
</th>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
// The connectors breakdown: a bounded top-volume table with sortable numeric columns and per-row health.
|
|
74
|
+
function ConnectorsTile({ connectors }: { connectors: ConnectorRow[] }) {
|
|
75
|
+
const [sortKey, setSortKey] = useState<SortKey>("records");
|
|
76
|
+
const [sortDir, setSortDir] = useState<SortDir>("desc");
|
|
77
|
+
const rows = useMemo(() => [...connectors].sort((a, b) => (sortDir === "asc" ? a[sortKey] - b[sortKey] : b[sortKey] - a[sortKey])), [connectors, sortKey, sortDir]);
|
|
78
|
+
const onSort = (k: SortKey) => k === sortKey ? setSortDir((d) => (d === "asc" ? "desc" : "asc")) : (setSortKey(k), setSortDir("desc"));
|
|
79
|
+
return (
|
|
80
|
+
<Statistics variant="outline" className="gap-4">
|
|
81
|
+
<StatisticsHeader>
|
|
82
|
+
<StatisticsHeading>
|
|
83
|
+
<StatisticsTitle size="sm">Connectors by volume</StatisticsTitle>
|
|
84
|
+
<StatisticsDescription>Top sources in the selected range.</StatisticsDescription>
|
|
85
|
+
</StatisticsHeading>
|
|
86
|
+
<StatisticsActions>
|
|
87
|
+
<StatisticsLink href="/connectors" className="inline-flex items-center gap-1 text-primary">All connectors <ArrowUpRight className="size-4" aria-hidden /></StatisticsLink>
|
|
88
|
+
</StatisticsActions>
|
|
89
|
+
</StatisticsHeader>
|
|
90
|
+
<div className="overflow-x-auto">
|
|
91
|
+
<table className="w-full text-sm">
|
|
92
|
+
<thead>
|
|
93
|
+
<tr className="border-b border-border text-left text-muted-foreground">
|
|
94
|
+
<th scope="col" className="py-2 pr-4 font-medium">Connector</th>
|
|
95
|
+
<SortHeader label="Records" sortKey="records" active={sortKey === "records"} dir={sortDir} onSort={onSort} />
|
|
96
|
+
<SortHeader label="Error rate" sortKey="errorRate" active={sortKey === "errorRate"} dir={sortDir} onSort={onSort} />
|
|
97
|
+
<th scope="col" className="py-2 pl-4 font-medium">Health</th>
|
|
98
|
+
</tr>
|
|
99
|
+
</thead>
|
|
100
|
+
<tbody>
|
|
101
|
+
{rows.map((c) => (
|
|
102
|
+
<tr key={c.id} className="border-b border-border last:border-0">
|
|
103
|
+
<td className="py-2 pr-4">
|
|
104
|
+
<div className="flex items-center gap-2">
|
|
105
|
+
<Database className="size-4 shrink-0 text-muted-foreground" aria-hidden />
|
|
106
|
+
<span className="truncate font-medium text-foreground">{c.name}</span>
|
|
107
|
+
<span className="shrink-0 font-mono text-muted-foreground">{c.kind}</span>
|
|
108
|
+
</div>
|
|
109
|
+
</td>
|
|
110
|
+
<td className="py-2 pr-4 text-right tabular-nums">{c.records.toLocaleString()}</td>
|
|
111
|
+
<td className="py-2 pr-4 text-right tabular-nums">{c.errorRate.toFixed(2)}%</td>
|
|
112
|
+
<td className="py-2 pl-4"><span className={`inline-flex items-center gap-1 ${HEALTH[c.health].className}`}>{HEALTH[c.health].icon}{HEALTH[c.health].label}</span></td>
|
|
113
|
+
</tr>
|
|
114
|
+
))}
|
|
115
|
+
</tbody>
|
|
116
|
+
</table>
|
|
117
|
+
</div>
|
|
118
|
+
</Statistics>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
// Loading skeleton mirroring the real shape: four KPI cards over two breakdown tiles.
|
|
122
|
+
function DashboardSkeleton() {
|
|
123
|
+
return (
|
|
124
|
+
<div className="flex flex-col gap-6" aria-hidden>
|
|
125
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
|
126
|
+
{[0, 1, 2, 3].map((i) => (
|
|
127
|
+
<div key={i} className="flex flex-col gap-3 rounded-lg border border-border p-4">
|
|
128
|
+
<Skeleton className="h-4 w-24" /><Skeleton className="h-8 w-32" /><Skeleton className="h-7 w-full" />
|
|
129
|
+
</div>
|
|
130
|
+
))}
|
|
131
|
+
</div>
|
|
132
|
+
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
|
133
|
+
{[0, 1].map((i) => (
|
|
134
|
+
<div key={i} className="flex flex-col gap-4 rounded-lg border border-border p-4">
|
|
135
|
+
<Skeleton className="h-4 w-40" /><Skeleton className="h-16 w-full" /><Skeleton className="h-4 w-3/4" />
|
|
136
|
+
</div>
|
|
137
|
+
))}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
// Ingestion-operations overview: a read-mostly KPI + breakdown dashboard with every lifecycle state reachable via the status prop.
|
|
143
|
+
export function OperationsDashboard(props: OperationsDashboardProps) {
|
|
144
|
+
const { status = "ready", degraded = false, lastUpdated = "14:32:08", onRefresh, onRetry, onConnectSource } = props;
|
|
145
|
+
const kpis = props.kpis ?? DEFAULT_KPIS;
|
|
146
|
+
const connectors = props.connectors ?? DEFAULT_CONNECTORS;
|
|
147
|
+
const [timeframe, setTimeframe] = useState<string>("24h");
|
|
148
|
+
return (
|
|
149
|
+
<main className="flex min-h-[100dvh] flex-col bg-background text-foreground" data-density-step="looser">
|
|
150
|
+
<header className="flex flex-col gap-2 border-b border-border px-6 py-4">
|
|
151
|
+
<div className="flex flex-wrap items-center justify-between gap-4">
|
|
152
|
+
<div className="flex flex-col gap-1">
|
|
153
|
+
<h1 className="text-xl font-medium text-foreground">Ingestion operations</h1>
|
|
154
|
+
<p className="text-sm text-muted-foreground">Cross-connector health for the data platform.</p>
|
|
155
|
+
</div>
|
|
156
|
+
<div className="flex items-center gap-2">
|
|
157
|
+
<div className="flex items-center gap-2" role="group" aria-label="Time range">
|
|
158
|
+
{TIMEFRAMES.map((tf) => (
|
|
159
|
+
<Button key={tf.key} size="sm" variant={tf.key === timeframe ? "secondary" : "ghost"} aria-pressed={tf.key === timeframe} onClick={() => setTimeframe(tf.key)}>{tf.label}</Button>
|
|
160
|
+
))}
|
|
161
|
+
</div>
|
|
162
|
+
<Button size="sm" variant="secondary" onClick={onRefresh}><RefreshCw className="size-4" aria-hidden /> Refresh</Button>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
<p role="status" aria-live="polite" className="text-xs text-muted-foreground">{status === "loading" ? "Loading ingestion overview…" : status === "error" ? "Overview failed to load."
|
|
166
|
+
: <>Updated <span className="font-mono">{lastUpdated}</span>{degraded ? " · latency source stale" : ""}</>}</p>
|
|
167
|
+
</header>
|
|
168
|
+
<div className="flex flex-col gap-6 px-6 py-6">
|
|
169
|
+
{status === "loading" ? <DashboardSkeleton /> : null}
|
|
170
|
+
{status === "error" ? (
|
|
171
|
+
<Alert variant="destructive" role="alert">
|
|
172
|
+
<TriangleAlert className="size-4" aria-hidden />
|
|
173
|
+
<AlertTitle>Overview failed to load</AlertTitle>
|
|
174
|
+
<AlertDescription>
|
|
175
|
+
The metrics service did not answer within the request timeout, usually a transient gateway hiccup. Retry now, or check the metrics service status if it keeps failing.
|
|
176
|
+
<span className="mt-3 block"><Button size="sm" variant="secondary" onClick={onRetry}><RefreshCw className="size-4" aria-hidden /> Retry</Button></span>
|
|
177
|
+
</AlertDescription>
|
|
178
|
+
</Alert>
|
|
179
|
+
) : null}
|
|
180
|
+
{status === "empty" ? (
|
|
181
|
+
<EmptyState icon={<Plug className="size-6 text-muted-foreground" aria-hidden />} title="No connectors yet"
|
|
182
|
+
description="Connect a data source to start ingesting records. Throughput, failure rates, and latency appear here once the first batch runs."
|
|
183
|
+
action={<Button size="sm" onClick={onConnectSource}><Plug className="size-4" aria-hidden /> Connect a source</Button>} />
|
|
184
|
+
) : null}
|
|
185
|
+
{status === "ready" ? (
|
|
186
|
+
<>
|
|
187
|
+
{degraded ? (
|
|
188
|
+
<Alert role="status">
|
|
189
|
+
<Clock className="size-4" aria-hidden />
|
|
190
|
+
<AlertTitle>Latency source unreachable</AlertTitle>
|
|
191
|
+
<AlertDescription>The p95 processing panel is showing the last cached value from 14:12. Every other panel is live.</AlertDescription>
|
|
192
|
+
</Alert>
|
|
193
|
+
) : null}
|
|
194
|
+
<section className="flex flex-col gap-3" aria-labelledby="kpi-heading">
|
|
195
|
+
<h2 id="kpi-heading" className="text-sm font-medium text-muted-foreground">Today at a glance</h2>
|
|
196
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">{kpis.map((kpi) => <KpiCard key={kpi.key} kpi={kpi} />)}</div>
|
|
197
|
+
</section>
|
|
198
|
+
<section className="flex flex-col gap-3" aria-labelledby="breakdown-heading">
|
|
199
|
+
<h2 id="breakdown-heading" className="text-sm font-medium text-muted-foreground">Breakdowns</h2>
|
|
200
|
+
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
|
201
|
+
<Statistics variant="outline" className="gap-4">
|
|
202
|
+
<StatisticsHeader>
|
|
203
|
+
<StatisticsHeading>
|
|
204
|
+
<StatisticsTitle size="sm">Batches by outcome</StatisticsTitle>
|
|
205
|
+
<StatisticsDescription>Last 24 hours across all connectors.</StatisticsDescription>
|
|
206
|
+
</StatisticsHeading>
|
|
207
|
+
<StatisticsActions>
|
|
208
|
+
<StatisticsLink href="/batches" className="inline-flex items-center gap-1 text-primary">Open batches <ArrowUpRight className="size-4" aria-hidden /></StatisticsLink>
|
|
209
|
+
</StatisticsActions>
|
|
210
|
+
</StatisticsHeader>
|
|
211
|
+
<StatisticsSegmentedBar segments={OUTCOME_SEGMENTS} />
|
|
212
|
+
<StatisticsLegend>
|
|
213
|
+
{OUTCOME_SEGMENTS.map((s, i) => (
|
|
214
|
+
<StatisticsLegendItem key={i} color={s.color} label={s.label} value={<span className="tabular-nums">{s.value.toLocaleString()}</span>} />
|
|
215
|
+
))}
|
|
216
|
+
</StatisticsLegend>
|
|
217
|
+
</Statistics>
|
|
218
|
+
<ConnectorsTile connectors={connectors} />
|
|
219
|
+
</div>
|
|
220
|
+
</section>
|
|
221
|
+
</>
|
|
222
|
+
) : null}
|
|
223
|
+
</div>
|
|
224
|
+
</main>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// exemplar fixture data — seeded, domain-shaped
|
|
229
|
+
const FIXTURE_SEED = 6820;
|
|
230
|
+
const DEFAULT_KPIS: KpiDatum[] = [
|
|
231
|
+
{ key: "throughput", label: "Records ingested", value: "1,284,930", unit: "today", trend: 6.4, spark: [820, 910, 880, 1020, 1180, 1240, 1284], chart: "var(--chart-1)" },
|
|
232
|
+
{ key: "failures", label: "Batch failure rate", value: "0.42", unit: "%", trend: 1.8, invertTrend: true, spark: [0.3, 0.28, 0.35, 0.31, 0.4, 0.44, 0.42], chart: "var(--chart-5)" },
|
|
233
|
+
{ key: "latency", label: "p95 processing", value: "312", unit: "ms", trend: -4.1, spark: [360, 352, 341, 338, 330, 318, 312], chart: "var(--chart-2)", stale: true },
|
|
234
|
+
{ key: "connectors", label: "Active connectors", value: "18", unit: "of 21", trend: 0, spark: [17, 17, 18, 18, 18, 17, 18], chart: "var(--chart-3)" },
|
|
235
|
+
];
|
|
236
|
+
const OUTCOME_SEGMENTS: StatisticsSegment[] = [
|
|
237
|
+
{ label: "Succeeded", value: 8420, color: "var(--chart-3)" },
|
|
238
|
+
{ label: "Retried", value: 612, color: "var(--chart-4)" },
|
|
239
|
+
{ label: "Failed", value: 148, color: "var(--chart-5)" },
|
|
240
|
+
];
|
|
241
|
+
const DEFAULT_CONNECTORS: ConnectorRow[] = [
|
|
242
|
+
{ id: "cn_stripe_events", name: "Stripe billing events", kind: "webhook", records: 486210, errorRate: 0.08, health: "healthy" },
|
|
243
|
+
{ id: "cn_salesforce_crm", name: "Salesforce CRM accounts and opportunities", kind: "rest", records: 312440, errorRate: 0.31, health: "healthy" },
|
|
244
|
+
{ id: "cn_postgres_orders", name: "Postgres orders replica", kind: "cdc", records: 274900, errorRate: 1.94, health: "elevated" },
|
|
245
|
+
{ id: "cn_segment_web", name: "Segment web analytics", kind: "stream", records: 158030, errorRate: 0.12, health: "healthy" },
|
|
246
|
+
{ id: "cn_zendesk_tickets", name: "Zendesk support tickets", kind: "rest", records: 42110, errorRate: 6.71, health: "failing" },
|
|
247
|
+
{ id: "cn_s3_archive", name: "S3 nightly archive export", kind: "batch", records: 11240, errorRate: 0, health: "healthy" },
|
|
248
|
+
];
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// skills/design/data/operator-pack/exemplars/entity-detail.tsx
|
|
2
|
+
// personality-invariant: composition anchor, not a look source.
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { Button } from "@nurix/components/button";
|
|
5
|
+
import { Badge } from "@nurix/components/badge";
|
|
6
|
+
import { Card, CardContent } from "@nurix/components/card";
|
|
7
|
+
import { Skeleton } from "@nurix/components/skeleton";
|
|
8
|
+
import { EmptyState } from "@nurix/components/empty-state";
|
|
9
|
+
import { Alert, AlertTitle, AlertDescription } from "@nurix/components/alert";
|
|
10
|
+
import { Label } from "@nurix/components/label";
|
|
11
|
+
import { Kbd } from "@nurix/components/kbd";
|
|
12
|
+
import { Input } from "@nurix/components/input";
|
|
13
|
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@nurix/components/tabs";
|
|
14
|
+
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetBody, SheetFooter, SheetClose } from "@nurix/components/sheet";
|
|
15
|
+
import { Table, defineColumn, type ColumnDefinition } from "@nurix/components/table";
|
|
16
|
+
import { Boxes, CircleCheck, CircleX, Pencil, RefreshCw, Rocket, ScrollText, TriangleAlert } from "lucide-react";
|
|
17
|
+
// A deployment-console service: a long-lived entity with many attributes, related instances, and deployment history (entity-detail).
|
|
18
|
+
type ServiceHealth = "healthy" | "degraded" | "down";
|
|
19
|
+
type SaveState = "idle" | "saving" | "saved" | "conflict";
|
|
20
|
+
type LoadStatus = "ready" | "loading" | "error";
|
|
21
|
+
export interface Service {
|
|
22
|
+
id: string; name: string; slug: string; environment: string; health: ServiceHealth;
|
|
23
|
+
owner: string; runtime: string; region: string; replicas: number; lastDeployedAt: string;
|
|
24
|
+
}
|
|
25
|
+
export interface Instance { id: string; region: string; version: string; status: string; cpuPercent: number; startedAt: string }
|
|
26
|
+
export interface Deployment { id: string; version: string; actor: string; status: "succeeded" | "failed" | "rolled_back"; at: string }
|
|
27
|
+
export interface ConfigPatch { replicas: number }
|
|
28
|
+
export interface EntityDetailProps {
|
|
29
|
+
service: Service | null; instances: Instance[]; deployments: Deployment[];
|
|
30
|
+
status?: LoadStatus; metricsStale?: boolean; errorDetail?: string; saveState?: SaveState;
|
|
31
|
+
onRetry?: () => void; onDeploy?: () => void; onSaveConfig?: (patch: ConfigPatch) => void;
|
|
32
|
+
}
|
|
33
|
+
const HEALTH_META: Record<ServiceHealth, { label: string; cls: string; Icon: typeof CircleCheck }> = {
|
|
34
|
+
healthy: { label: "Healthy", cls: "text-success", Icon: CircleCheck },
|
|
35
|
+
degraded: { label: "Degraded", cls: "text-warning", Icon: TriangleAlert },
|
|
36
|
+
down: { label: "Down", cls: "text-destructive", Icon: CircleX },
|
|
37
|
+
};
|
|
38
|
+
const DEPLOY_LABEL: Record<Deployment["status"], string> = { succeeded: "Deployed", failed: "Deploy failed", rolled_back: "Rolled back" };
|
|
39
|
+
const INSTANCE_STATUS = [{ value: "running", label: "Running" }, { value: "starting", label: "Starting" }, { value: "unknown", label: "Unknown" }];
|
|
40
|
+
const instanceColumns: ColumnDefinition<Instance>[] = [
|
|
41
|
+
defineColumn<Instance>({ slug: "id", label: "Instance", type: "text", cell: (r) => <span className="font-mono tabular-nums">{r.id}</span> }),
|
|
42
|
+
defineColumn<Instance>({ slug: "region", label: "Region", type: "text" }),
|
|
43
|
+
defineColumn<Instance>({ slug: "version", label: "Version", type: "text", cell: (r) => <span className="font-mono">{r.version}</span> }),
|
|
44
|
+
defineColumn<Instance>({ slug: "status", label: "Status", type: "enum", component: "status", metadata: { options: INSTANCE_STATUS } }),
|
|
45
|
+
defineColumn<Instance>({ slug: "cpuPercent", label: "CPU", type: "number", metadata: { suffix: "%" } }),
|
|
46
|
+
defineColumn<Instance>({ slug: "startedAt", label: "Started", type: "date", metadata: { relativeFormat: true } }),
|
|
47
|
+
];
|
|
48
|
+
// Loading shell mirrors the header identity and tab bar, never a centered spinner.
|
|
49
|
+
function DetailSkeleton() {
|
|
50
|
+
return (
|
|
51
|
+
<div className="min-h-[100dvh] bg-background p-6">
|
|
52
|
+
<div className="flex items-center gap-3">
|
|
53
|
+
<Skeleton className="h-10 w-10 rounded-md" />
|
|
54
|
+
<div className="flex flex-col gap-2"><Skeleton className="h-5 w-48" /><Skeleton className="h-4 w-32" /></div>
|
|
55
|
+
</div>
|
|
56
|
+
<div className="mt-6 flex gap-2"><Skeleton className="h-8 w-24" /><Skeleton className="h-8 w-24" /><Skeleton className="h-8 w-24" /></div>
|
|
57
|
+
<div className="mt-6"><Skeleton className="h-40 w-full" /></div>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
// Entity load failure states what happened, why, and the recovery path. Never a dead end.
|
|
62
|
+
function DetailError({ detail, onRetry }: { detail?: string; onRetry?: () => void }) {
|
|
63
|
+
return (
|
|
64
|
+
<div className="flex min-h-[100dvh] items-center justify-center bg-background p-6">
|
|
65
|
+
<Alert variant="destructive" className="max-w-md">
|
|
66
|
+
<CircleX className="h-4 w-4" />
|
|
67
|
+
<AlertTitle>Could not load this service</AlertTitle>
|
|
68
|
+
<AlertDescription>{detail ?? "The deployment API did not respond."} Retry the request, or open the platform status page if it keeps failing.</AlertDescription>
|
|
69
|
+
<div className="mt-3"><Button size="sm" onClick={onRetry}><RefreshCw className="h-4 w-4" /> Retry</Button></div>
|
|
70
|
+
</Alert>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
// Config edit opens beside the work; the save path narrates saving, saved, and the concurrent-edit conflict.
|
|
75
|
+
function EditConfigSheet({ service, open, onOpenChange, saveState, onSave }: {
|
|
76
|
+
service: Service; open: boolean; onOpenChange: (v: boolean) => void; saveState: SaveState; onSave?: (patch: ConfigPatch) => void;
|
|
77
|
+
}) {
|
|
78
|
+
const [replicas, setReplicas] = useState(String(service.replicas));
|
|
79
|
+
const [replicaError, setReplicaError] = useState<string | null>(null);
|
|
80
|
+
const saving = saveState === "saving";
|
|
81
|
+
return (
|
|
82
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
83
|
+
<SheetContent side="right">
|
|
84
|
+
<SheetHeader>
|
|
85
|
+
<SheetTitle>Edit configuration</SheetTitle>
|
|
86
|
+
<SheetDescription>Changes apply on the next deploy of {service.slug}.</SheetDescription>
|
|
87
|
+
</SheetHeader>
|
|
88
|
+
<SheetBody className="flex flex-col gap-4">
|
|
89
|
+
{saveState === "conflict" && (
|
|
90
|
+
<Alert variant="destructive">
|
|
91
|
+
<TriangleAlert className="h-4 w-4" />
|
|
92
|
+
<AlertTitle>Configuration changed elsewhere</AlertTitle>
|
|
93
|
+
<AlertDescription>Another operator saved this service while you were editing. Reload to see their values before saving yours.</AlertDescription>
|
|
94
|
+
</Alert>
|
|
95
|
+
)}
|
|
96
|
+
<div className="flex flex-col gap-2">
|
|
97
|
+
<Label htmlFor="replicas">Desired replicas</Label>
|
|
98
|
+
<Input id="replicas" inputMode="numeric" value={replicas} aria-invalid={!!replicaError} onChange={(e) => setReplicas(e.target.value)}
|
|
99
|
+
onBlur={() => { const n = Number(replicas); setReplicaError(!Number.isInteger(n) || n < 1 ? "Enter a whole number of one or more." : null); }} />
|
|
100
|
+
{replicaError && <p className="text-sm text-destructive">{replicaError}</p>}
|
|
101
|
+
</div>
|
|
102
|
+
<p role="status" aria-live="polite" className="min-h-4 text-sm text-muted-foreground">
|
|
103
|
+
{saveState === "saving" && "Saving configuration…"}
|
|
104
|
+
{saveState === "saved" && "Configuration saved."}
|
|
105
|
+
</p>
|
|
106
|
+
</SheetBody>
|
|
107
|
+
<SheetFooter>
|
|
108
|
+
<SheetClose asChild><Button variant="secondary" size="sm">Cancel</Button></SheetClose>
|
|
109
|
+
<Button size="sm" disabled={saving || !!replicaError} onClick={() => onSave?.({ replicas: Number(replicas) })}>
|
|
110
|
+
{saving && <RefreshCw className="h-4 w-4" />} Save changes
|
|
111
|
+
</Button>
|
|
112
|
+
</SheetFooter>
|
|
113
|
+
</SheetContent>
|
|
114
|
+
</Sheet>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
// Service entity-detail: identity header, tabbed overview / instances / activity, edit in a side panel.
|
|
118
|
+
export function EntityDetail(props: EntityDetailProps) {
|
|
119
|
+
const { service, instances, deployments, status = "ready", metricsStale = false, errorDetail, saveState = "idle", onRetry, onDeploy, onSaveConfig } = props;
|
|
120
|
+
const [tab, setTab] = useState("overview");
|
|
121
|
+
const [editOpen, setEditOpen] = useState(false);
|
|
122
|
+
// Keyboard parity for the Edit affordance: press E to open the config panel.
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
function onKey(e: KeyboardEvent) {
|
|
125
|
+
const el = e.target as HTMLElement | null;
|
|
126
|
+
if (el && (el.tagName === "INPUT" || el.tagName === "TEXTAREA")) return;
|
|
127
|
+
if (e.key === "e" && !editOpen) { e.preventDefault(); setEditOpen(true); }
|
|
128
|
+
}
|
|
129
|
+
window.addEventListener("keydown", onKey);
|
|
130
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
131
|
+
}, [editOpen]);
|
|
132
|
+
if (status === "loading") return <DetailSkeleton />;
|
|
133
|
+
if (status === "error" || !service) return <DetailError detail={errorDetail} onRetry={onRetry} />;
|
|
134
|
+
const health = HEALTH_META[service.health];
|
|
135
|
+
const attributes: { label: string; value: string; mono?: boolean; num?: boolean }[] = [
|
|
136
|
+
{ label: "Environment", value: service.environment }, { label: "Runtime", value: service.runtime, mono: true },
|
|
137
|
+
{ label: "Region", value: service.region, mono: true }, { label: "Owner", value: service.owner },
|
|
138
|
+
{ label: "Desired replicas", value: String(service.replicas), num: true }, { label: "Last deployed", value: new Date(service.lastDeployedAt).toLocaleString(), num: true },
|
|
139
|
+
];
|
|
140
|
+
return (
|
|
141
|
+
<div className="min-h-[100dvh] bg-background text-foreground">
|
|
142
|
+
<header className="flex flex-wrap items-start justify-between gap-4 border-b border-border p-6">
|
|
143
|
+
<div className="flex items-start gap-3">
|
|
144
|
+
<span className="grid h-10 w-10 place-items-center rounded-md bg-muted text-muted-foreground"><Boxes className="h-5 w-5" /></span>
|
|
145
|
+
<div>
|
|
146
|
+
<div className="flex items-center gap-2">
|
|
147
|
+
<h1 className="text-xl font-medium">{service.name}</h1>
|
|
148
|
+
<Badge variant="outline" className={`gap-1 ${health.cls}`}><health.Icon className="h-3.5 w-3.5" /> {health.label}</Badge>
|
|
149
|
+
</div>
|
|
150
|
+
<p className="mt-1 text-sm text-muted-foreground">{service.slug} · {service.environment}</p>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
<div className="flex items-center gap-2">
|
|
154
|
+
<Button variant="secondary" size="sm" onClick={() => setEditOpen(true)}><Pencil className="h-4 w-4" /> Edit config <Kbd className="ml-1">E</Kbd></Button>
|
|
155
|
+
<Button size="sm" onClick={onDeploy}><Rocket className="h-4 w-4" /> Deploy</Button>
|
|
156
|
+
</div>
|
|
157
|
+
</header>
|
|
158
|
+
<div className="p-6">
|
|
159
|
+
{metricsStale && (
|
|
160
|
+
<Alert className="mb-4">
|
|
161
|
+
<TriangleAlert className="h-4 w-4 text-warning" />
|
|
162
|
+
<AlertTitle>Live health signals unavailable</AlertTitle>
|
|
163
|
+
<AlertDescription>The health collector is unreachable, so instance status and the figures below reflect the last cached snapshot and may be stale.</AlertDescription>
|
|
164
|
+
</Alert>
|
|
165
|
+
)}
|
|
166
|
+
<Tabs value={tab} onValueChange={setTab}>
|
|
167
|
+
<TabsList>
|
|
168
|
+
<TabsTrigger value="overview">Overview</TabsTrigger>
|
|
169
|
+
<TabsTrigger value="instances">Instances</TabsTrigger>
|
|
170
|
+
<TabsTrigger value="activity">Activity</TabsTrigger>
|
|
171
|
+
</TabsList>
|
|
172
|
+
<TabsContent value="overview">
|
|
173
|
+
<section className="mt-4">
|
|
174
|
+
<h2 className="mb-3 text-sm font-medium">Configuration <span className="text-xs font-normal text-muted-foreground">sample data</span></h2>
|
|
175
|
+
<Card><CardContent className="p-0">
|
|
176
|
+
{/* mock: fixture-supplied values, not authoritative production readings */}
|
|
177
|
+
<dl className="divide-y divide-border">
|
|
178
|
+
{attributes.map((a) => (
|
|
179
|
+
<div key={a.label} className="flex items-center justify-between gap-4 px-4 py-3 text-sm">
|
|
180
|
+
<dt className="text-muted-foreground">{a.label}</dt>
|
|
181
|
+
<dd className={`${a.mono ? "font-mono" : ""} ${a.num ? "tabular-nums" : ""}`}>{a.value}</dd>
|
|
182
|
+
</div>
|
|
183
|
+
))}
|
|
184
|
+
</dl>
|
|
185
|
+
</CardContent></Card>
|
|
186
|
+
</section>
|
|
187
|
+
</TabsContent>
|
|
188
|
+
<TabsContent value="instances">
|
|
189
|
+
<div className="mt-4">
|
|
190
|
+
{instances.length === 0 ? (
|
|
191
|
+
<EmptyState icon={<Boxes className="h-10 w-10 text-muted-foreground" />} title="No running instances"
|
|
192
|
+
description="This service has no instances in this environment yet. Deploy a build to bring the first one online."
|
|
193
|
+
action={<Button size="sm" onClick={onDeploy}><Rocket className="h-4 w-4" /> Deploy</Button>} />
|
|
194
|
+
) : (
|
|
195
|
+
<Table data={instances} columns={instanceColumns} tableConfig={{ header: true, density: "compact" }} />
|
|
196
|
+
)}
|
|
197
|
+
</div>
|
|
198
|
+
</TabsContent>
|
|
199
|
+
<TabsContent value="activity">
|
|
200
|
+
<div className="mt-4">
|
|
201
|
+
{deployments.length === 0 ? (
|
|
202
|
+
<EmptyState icon={<ScrollText className="h-10 w-10 text-muted-foreground" />} title="No deployments recorded"
|
|
203
|
+
description="Deployment history for this service appears here once the first build ships." />
|
|
204
|
+
) : (
|
|
205
|
+
<ol className="flex flex-col">
|
|
206
|
+
{deployments.map((d) => (
|
|
207
|
+
<li key={d.id} className="flex items-center gap-3 border-b border-border py-3 text-sm last:border-0">
|
|
208
|
+
{d.status === "succeeded" ? <CircleCheck className="h-4 w-4 text-success" /> : <CircleX className="h-4 w-4 text-destructive" />}
|
|
209
|
+
<span className="font-mono">{d.version}</span>
|
|
210
|
+
<span className="text-muted-foreground">{DEPLOY_LABEL[d.status]} by {d.actor}</span>
|
|
211
|
+
<time className="ml-auto font-mono tabular-nums text-muted-foreground">{new Date(d.at).toLocaleString()}</time>
|
|
212
|
+
</li>
|
|
213
|
+
))}
|
|
214
|
+
</ol>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
</TabsContent>
|
|
218
|
+
</Tabs>
|
|
219
|
+
</div>
|
|
220
|
+
<EditConfigSheet service={service} open={editOpen} onOpenChange={setEditOpen} saveState={saveState} onSave={onSaveConfig} />
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
// exemplar fixture data — seeded, domain-shaped
|
|
225
|
+
export const FIXTURE_SEED = 7314;
|
|
226
|
+
export const service: Service = {
|
|
227
|
+
id: "svc_0e9c1a7b", name: "checkout-api", slug: "checkout-api", environment: "production", health: "degraded",
|
|
228
|
+
owner: "payments-platform", runtime: "node20", region: "iad", replicas: 6, lastDeployedAt: "2026-07-03T14:22:00Z",
|
|
229
|
+
};
|
|
230
|
+
export const instances: Instance[] = [
|
|
231
|
+
{ id: "checkout-api-7f4b21", region: "iad", version: "2026.7.3-a19f", status: "running", cpuPercent: 34, startedAt: "2026-07-03T14:23:00Z" },
|
|
232
|
+
{ id: "checkout-api-9c02de", region: "iad", version: "2026.7.3-a19f", status: "running", cpuPercent: 51, startedAt: "2026-07-03T14:23:00Z" },
|
|
233
|
+
{ id: "checkout-api-b73a10", region: "sfo", version: "2026.7.3-a19f", status: "running", cpuPercent: 47, startedAt: "2026-07-03T14:24:00Z" },
|
|
234
|
+
{ id: "checkout-api-1de8ff", region: "sfo", version: "2026.6.28-c4e2-hotfix-billing-migration", status: "starting", cpuPercent: 0, startedAt: "2026-07-04T02:11:00Z" },
|
|
235
|
+
{ id: "checkout-api-0000aa", region: "fra", version: "2026.6.28-c4e2", status: "unknown", cpuPercent: 0, startedAt: "2026-07-02T09:05:00Z" },
|
|
236
|
+
{ id: "checkout-api-55b9c1", region: "fra", version: "2026.7.3-a19f", status: "running", cpuPercent: 62, startedAt: "2026-07-03T14:25:00Z" },
|
|
237
|
+
];
|
|
238
|
+
export const deployments: Deployment[] = [
|
|
239
|
+
{ id: "dep_5a1", version: "2026.7.3-a19f", actor: "m.okonkwo", status: "succeeded", at: "2026-07-03T14:22:00Z" },
|
|
240
|
+
{ id: "dep_4f7", version: "2026.7.1-77b0", actor: "priya.nair", status: "rolled_back", at: "2026-07-01T18:40:00Z" },
|
|
241
|
+
{ id: "dep_4e2", version: "2026.6.28-c4e2", actor: "s.delacroix", status: "failed", at: "2026-06-28T22:05:00Z" },
|
|
242
|
+
{ id: "dep_4b0", version: "2026.6.20-11ad", actor: "m.okonkwo", status: "succeeded", at: "2026-06-20T10:33:00Z" },
|
|
243
|
+
];
|
|
244
|
+
export const emptyInstances: Instance[] = [];
|
|
245
|
+
export const emptyDeployments: Deployment[] = [];
|