cortexapps-cli 1.27.0__tar.gz → 1.29.0__tar.gz
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.
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/PKG-INFO +1 -1
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/backup.py +44 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/plugins.py +11 -3
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/solutions.py +3 -1
- cortexapps_cli-1.29.0/cortexapps_cli/solutions/ai-spend/.github/workflows/sync-claude-spend.yaml +27 -0
- cortexapps_cli-1.29.0/cortexapps_cli/solutions/ai-spend/README.md +165 -0
- cortexapps_cli-1.29.0/cortexapps_cli/solutions/ai-spend/catalog/employee-alice-chen.yaml +7 -0
- cortexapps_cli-1.29.0/cortexapps_cli/solutions/ai-spend/catalog/employee-bob-martinez.yaml +7 -0
- cortexapps_cli-1.29.0/cortexapps_cli/solutions/ai-spend/catalog/employee-carol-kim.yaml +7 -0
- cortexapps_cli-1.29.0/cortexapps_cli/solutions/ai-spend/catalog/employee-david-osei.yaml +7 -0
- cortexapps_cli-1.29.0/cortexapps_cli/solutions/ai-spend/catalog/employee-emma-johnson.yaml +7 -0
- cortexapps_cli-1.29.0/cortexapps_cli/solutions/ai-spend/catalog/team-data.yaml +20 -0
- cortexapps_cli-1.29.0/cortexapps_cli/solutions/ai-spend/catalog/team-engineering.yaml +30 -0
- cortexapps_cli-1.29.0/cortexapps_cli/solutions/ai-spend/catalog/team-frontend.yaml +23 -0
- cortexapps_cli-1.29.0/cortexapps_cli/solutions/ai-spend/catalog/team-platform.yaml +23 -0
- cortexapps_cli-1.29.0/cortexapps_cli/solutions/ai-spend/custom-metrics/ai-spend.json +84 -0
- cortexapps_cli-1.29.0/cortexapps_cli/solutions/ai-spend/entity-relationship-types/team-member.json +20 -0
- cortexapps_cli-1.29.0/cortexapps_cli/solutions/ai-spend/entity-types/employee.json +7 -0
- cortexapps_cli-1.29.0/cortexapps_cli/solutions/ai-spend/plugins/team-ai-spend.json +14 -0
- cortexapps_cli-1.29.0/cortexapps_cli/solutions/ai-spend/scorecards/ai-spend-scorecard.yaml +58 -0
- cortexapps_cli-1.29.0/cortexapps_cli/solutions/ai-spend/scripts/sync-claude-spend.py +334 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/pyproject.toml +1 -1
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/LICENSE +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/README.rst +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/__init__.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/cli.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/command_options.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/ai_skills.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/api_keys.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/audit_logs.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/backup_commands/cortex_export.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/catalog.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/custom_data.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/custom_events.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/custom_metrics.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/dependencies.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/deploys.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/discovery_audit.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/docs.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/entity_relationship_types.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/entity_relationships.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/entity_types.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/gitops_logs.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/groups.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/initiatives.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/apiiro.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/argocd.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/aws.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/azure_active_directory.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/azure_devops.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/azure_resources.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/bamboohr.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/bitbucket.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/bugsnag.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/buildkite.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/checkmarx_sast.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/circleci.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/clickup.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/codecov.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/coralogix.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/datadog.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/dynatrace.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/firehydrant.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/github.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/gitlab.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/incidentio.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/instana.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/jenkins.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/jira.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/launchdarkly.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/mend_sast.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/mend_sca.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/newrelic.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/okta.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/opsgenie.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/pagerduty.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/prometheus.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/rollbar.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/rootly.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/semgrep.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/sentry.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/servicenow.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/servicenow_cloud_observability.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/snyk.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/sonarqube.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/splunk_observability_cloud.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/splunk_on_call.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/sumologic.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/veracode.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/wiz.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/workday.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/integrations_commands/xmatters.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/ip_allowlist.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/on_call.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/packages.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/packages_commands/go.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/packages_commands/java.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/packages_commands/node.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/packages_commands/nuget.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/packages_commands/python.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/queries.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/rest.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/scim.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/scorecards.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/scorecards_commands/exemptions.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/secrets.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/teams.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/users.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/users_commands/roles.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/commands/workflows.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/cortex_client.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/models/team.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/__init__.py +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/ai-agents/README.md +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/ai-agents/catalog/address-reviews.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/ai-agents/catalog/audit-flags.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/ai-agents/catalog/code-review.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/ai-agents/catalog/fix-issue.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/ai-agents/catalog/incident-trends.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/ai-agents/catalog/jira-to-kb.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/ai-agents/catalog/release-to-sha.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/ai-agents/catalog/search-troubleshooting-kb.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/ai-agents/catalog/simplify-test.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/ai-agents/entity-types/ai-agent.json +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/ai-agents/scorecards/ai-agent-governance.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/ai-skills/README.md +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/ai-skills/catalog/api-doc-sync.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/ai-skills/catalog/changelog-writer.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/ai-skills/catalog/docs-generator.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/ai-skills/catalog/docs-portal.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/ai-skills/entity-relationship-types/ai-plugin-service.json +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/ai-skills/entity-relationship-types/ai-plugin-skills.json +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/ai-skills/entity-relationship-types/ai-skill-service.json +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/ai-skills/entity-types/ai-plugin.json +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/ai-skills/entity-types/ai-skill.json +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/ai-skills/scorecards/ai-skills-quality.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/.github/workflows/cortex-service-version.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/README.md +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/api-gateway-3.0.1.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/api-gateway-3.1.0.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/api-gateway-3.1.1.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/api-gateway.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/auth-2.1.0.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/auth-2.1.1.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/auth-2.2.0.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/auth.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/aws-dev-us-west-2.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/billing-1.1.0.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/billing-1.2.0.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/billing.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/cart-1.0.2.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/cart.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/checkout-2.2.0.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/checkout-2.3.0.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/checkout-2.4.0.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/checkout.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/gcp-prod-us-east-1.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/gcp-prod-us-west-1.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/gcp-staging-us-east-1.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/notifications-1.0.0.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/notifications-1.0.1.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/notifications-1.1.0.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/notifications.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/order-service-2.0.1.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/order-service-2.1.0.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/order-service.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/payments-1.6.1.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/payments-1.7.0.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/payments-1.7.1.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/payments.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/release-2026-06-20.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/release-2026-07-01.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/release-2026-07-09.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/release-2026-07-10.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/search-4.0.0.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/search-4.1.0.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/search.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/user-service-1.2.0.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/user-service-1.3.0.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/catalog/user-service.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/entity-relationship-types/environments.json +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/entity-relationship-types/versions.json +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/entity-types/environment.json +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/entity-types/release.json +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/entity-types/service-version.json +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/workflows/create-environment.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/workflows/create-release.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/solutions/environments/workflows/trigger-github-release.yaml +0 -0
- {cortexapps_cli-1.27.0 → cortexapps_cli-1.29.0}/cortexapps_cli/utils.py +0 -0
|
@@ -471,6 +471,47 @@ def _import_entity_relationships(ctx, directory):
|
|
|
471
471
|
|
|
472
472
|
return ("entity-relationships", len(results) - failed_count, [(fp, et, em) for rt, fp, et, em in results if et])
|
|
473
473
|
|
|
474
|
+
def _import_custom_metrics(ctx, directory):
|
|
475
|
+
imported = 0
|
|
476
|
+
failed = []
|
|
477
|
+
if os.path.isdir(directory):
|
|
478
|
+
print("Processing: " + directory)
|
|
479
|
+
client = ctx.obj["client"]
|
|
480
|
+
for filename in sorted(os.listdir(directory)):
|
|
481
|
+
if not filename.endswith(".json"):
|
|
482
|
+
continue
|
|
483
|
+
file_path = os.path.join(directory, filename)
|
|
484
|
+
if not os.path.isfile(file_path):
|
|
485
|
+
continue
|
|
486
|
+
metric_key = filename[:-5] # strip .json
|
|
487
|
+
try:
|
|
488
|
+
print(" Importing: " + filename)
|
|
489
|
+
with open(file_path) as f:
|
|
490
|
+
data = json.load(f)
|
|
491
|
+
|
|
492
|
+
# Group flat values list by entityTag
|
|
493
|
+
grouped = {}
|
|
494
|
+
for entry in data.get("values", []):
|
|
495
|
+
tag = entry["entityTag"]
|
|
496
|
+
if tag not in grouped:
|
|
497
|
+
grouped[tag] = []
|
|
498
|
+
grouped[tag].append({
|
|
499
|
+
"timestamp": entry["timestamp"],
|
|
500
|
+
"value": entry["value"],
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
# Call per-entity bulk endpoint once per entity
|
|
504
|
+
for entity_tag, series in grouped.items():
|
|
505
|
+
client.post(
|
|
506
|
+
f"api/v1/eng-intel/custom-metrics/{metric_key}/entity/{entity_tag}/bulk",
|
|
507
|
+
data={"series": series},
|
|
508
|
+
)
|
|
509
|
+
imported += 1
|
|
510
|
+
except Exception as e:
|
|
511
|
+
print(f" Failed to import {filename}: {type(e).__name__} - {str(e)}")
|
|
512
|
+
failed.append((file_path, type(e).__name__, str(e)))
|
|
513
|
+
return ("custom-metrics", imported, failed)
|
|
514
|
+
|
|
474
515
|
def _has_relationships(file_path):
|
|
475
516
|
"""Check if a catalog YAML file contains x-cortex-relationships."""
|
|
476
517
|
try:
|
|
@@ -760,6 +801,7 @@ def import_tenant(
|
|
|
760
801
|
all_stats.append(_import_entity_relationship_types(ctx, directory + "/entity-relationship-types"))
|
|
761
802
|
all_stats.append(_import_catalog(ctx, directory + "/catalog"))
|
|
762
803
|
all_stats.append(_import_entity_relationships(ctx, directory + "/entity-relationships"))
|
|
804
|
+
all_stats.append(_import_custom_metrics(ctx, directory + "/custom-metrics"))
|
|
763
805
|
all_stats.append(_import_plugins(ctx, directory + "/plugins"))
|
|
764
806
|
all_stats.append(_import_scorecards(ctx, directory + "/scorecards"))
|
|
765
807
|
all_stats.append(_import_workflows(ctx, directory + "/workflows"))
|
|
@@ -811,6 +853,8 @@ def import_tenant(
|
|
|
811
853
|
elif import_type == "entity-relationships":
|
|
812
854
|
# These need special handling - would need the relationship type
|
|
813
855
|
print(f"# Manual retry needed for entity-relationships: {file_path}")
|
|
856
|
+
elif import_type == "custom-metrics":
|
|
857
|
+
print(f"# Manual retry needed for custom-metrics: {file_path}")
|
|
814
858
|
elif import_type == "plugins":
|
|
815
859
|
print(f"cortex plugins create --force -f \"{file_path}\"")
|
|
816
860
|
elif import_type == "scorecards":
|
|
@@ -87,6 +87,8 @@ def create(
|
|
|
87
87
|
# Remove the 'tag' attribute if it exists
|
|
88
88
|
data.pop("tag", None)
|
|
89
89
|
r = client.put("api/v1/plugins/" + tag, data, raw_response=True)
|
|
90
|
+
else:
|
|
91
|
+
r = client.post("api/v1/plugins", data, raw_response=True)
|
|
90
92
|
else:
|
|
91
93
|
r = client.post("api/v1/plugins", data, raw_response=True)
|
|
92
94
|
|
|
@@ -142,12 +144,18 @@ def get(
|
|
|
142
144
|
def replace(
|
|
143
145
|
ctx: typer.Context,
|
|
144
146
|
file_input: Annotated[typer.FileText, typer.Option("--file", "-f", help="File containing contents of plugin using schema defined at https://docs.cortex.io/docs/api/create-plugin")] = None,
|
|
145
|
-
tag_or_id: str = typer.Option(
|
|
147
|
+
tag_or_id: str = typer.Option(None, "--tag-or-id", "-t", help="The tag or ID of the plugin to replace. Defaults to the tag field in the file."),
|
|
146
148
|
):
|
|
147
149
|
"""
|
|
148
150
|
Replace an existing plugin by tag
|
|
149
151
|
"""
|
|
150
152
|
|
|
151
153
|
client = ctx.obj["client"]
|
|
152
|
-
|
|
153
|
-
|
|
154
|
+
|
|
155
|
+
data = json.loads(file_input.read())
|
|
156
|
+
resolved = tag_or_id or data.get("tag")
|
|
157
|
+
if not resolved:
|
|
158
|
+
typer.echo("Error: --tag-or-id is required when the file does not contain a 'tag' field.")
|
|
159
|
+
raise typer.Exit(1)
|
|
160
|
+
|
|
161
|
+
client.put("api/v1/plugins/" + resolved, data=data)
|
|
@@ -280,6 +280,7 @@ def _collect_solution_resources(path: Path) -> dict[str, list[str]]:
|
|
|
280
280
|
"catalog": [],
|
|
281
281
|
"scorecards": [],
|
|
282
282
|
"workflows": [],
|
|
283
|
+
"plugins": [],
|
|
283
284
|
}
|
|
284
285
|
for kind in resources:
|
|
285
286
|
subdir = path / kind
|
|
@@ -336,7 +337,7 @@ def _run_uninstall(client, path: Path, yes: bool) -> None:
|
|
|
336
337
|
return
|
|
337
338
|
|
|
338
339
|
typer.echo("\nThis will remove the following resources:")
|
|
339
|
-
for kind in ("workflows", "scorecards", "catalog", "entity-relationship-types", "entity-types"):
|
|
340
|
+
for kind in ("workflows", "scorecards", "plugins", "catalog", "entity-relationship-types", "entity-types"):
|
|
340
341
|
count = len(resources[kind])
|
|
341
342
|
if count:
|
|
342
343
|
typer.echo(f" {kind}: {count}")
|
|
@@ -352,6 +353,7 @@ def _run_uninstall(client, path: Path, yes: bool) -> None:
|
|
|
352
353
|
steps = [
|
|
353
354
|
("workflows", lambda t: f"api/v1/workflows/{t}"),
|
|
354
355
|
("scorecards", lambda t: f"api/v1/scorecards/{t}"),
|
|
356
|
+
("plugins", lambda t: f"api/v1/plugins/{t}"),
|
|
355
357
|
("catalog", lambda t: f"api/v1/catalog/{t}"),
|
|
356
358
|
("entity-relationship-types", lambda t: f"api/v1/relationship-types/{t}"),
|
|
357
359
|
("entity-types", lambda t: f"api/v1/catalog/definitions/{t}"),
|
cortexapps_cli-1.29.0/cortexapps_cli/solutions/ai-spend/.github/workflows/sync-claude-spend.yaml
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: Sync Claude AI Spend to Cortex
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
- cron: "0 6 * * 1" # Every Monday at 06:00 UTC
|
|
6
|
+
workflow_dispatch: # Allow manual runs from the Actions tab
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
sync:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- name: Checkout
|
|
13
|
+
uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- name: Set up Python
|
|
16
|
+
uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.11"
|
|
19
|
+
|
|
20
|
+
- name: Install dependencies
|
|
21
|
+
run: pip install requests
|
|
22
|
+
|
|
23
|
+
- name: Sync Claude spend to Cortex
|
|
24
|
+
env:
|
|
25
|
+
ANTHROPIC_ANALYTICS_KEY: ${{ secrets.ANTHROPIC_ANALYTICS_KEY }}
|
|
26
|
+
CORTEX_API_KEY: ${{ secrets.CORTEX_API_KEY }}
|
|
27
|
+
run: python scripts/sync-claude-spend.py
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: AI Spend
|
|
3
|
+
description: Track per-employee Claude AI spend in Cortex using custom metrics, with a full team hierarchy for rollup visibility.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# AI Spend
|
|
7
|
+
|
|
8
|
+
Answers the question: **"How much are we spending on Claude AI, and who's spending it?"**
|
|
9
|
+
|
|
10
|
+
Register every employee as a Cortex entity linked to their team, push weekly Claude spend as a custom metric, and roll costs up the org hierarchy — from individual → sub-team → top-level engineering.
|
|
11
|
+
|
|
12
|
+
## How It Works
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
┌─────────────────────┐ every Monday 06:00 UTC
|
|
16
|
+
│ GitHub Actions │◄──────────────────────────────────┐
|
|
17
|
+
│ sync-claude-spend │ │
|
|
18
|
+
└────────┬────────────┘ (cron schedule)
|
|
19
|
+
│
|
|
20
|
+
│ GET /v1/organizations/analytics/costs
|
|
21
|
+
▼
|
|
22
|
+
┌─────────────────────┐
|
|
23
|
+
│ Anthropic Claude │ per-user spend for the week
|
|
24
|
+
│ Analytics API │
|
|
25
|
+
└────────┬────────────┘
|
|
26
|
+
│
|
|
27
|
+
│ map email → employee-first-last
|
|
28
|
+
│ sum members → team rollups
|
|
29
|
+
▼
|
|
30
|
+
┌─────────────────────┐
|
|
31
|
+
│ Cortex API │ POST ai-spend custom metric
|
|
32
|
+
│ Custom Metrics │ per employee + per team
|
|
33
|
+
└────────┬────────────┘
|
|
34
|
+
│
|
|
35
|
+
▼
|
|
36
|
+
┌──────────────────────────────────────────────┐
|
|
37
|
+
│ Cortex Catalog │
|
|
38
|
+
│ │
|
|
39
|
+
│ team-engineering $1,212/wk Silver │
|
|
40
|
+
│ ├── team-platform $473/wk Gold │
|
|
41
|
+
│ │ ├── employee-alice $291/wk │
|
|
42
|
+
│ │ └── employee-bob $182/wk │
|
|
43
|
+
│ ├── team-frontend $380/wk Silver │
|
|
44
|
+
│ │ ├── employee-carol $245/wk │
|
|
45
|
+
│ │ └── employee-david $136/wk │
|
|
46
|
+
│ └── team-data $359/wk Bronze │
|
|
47
|
+
│ └── employee-emma $359/wk │
|
|
48
|
+
│ │
|
|
49
|
+
│ Scorecard: ai-spend-scorecard │
|
|
50
|
+
│ Plugin: team-ai-spend (per-team chart) │
|
|
51
|
+
└──────────────────────────────────────────────┘
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## What's Included
|
|
55
|
+
|
|
56
|
+
| Resource | Tag / Key |
|
|
57
|
+
|---|---|
|
|
58
|
+
| Entity type | `employee` |
|
|
59
|
+
| Relationship type | `team-member` (team → team\|employee) |
|
|
60
|
+
| Teams | `team-engineering`, `team-platform`, `team-frontend`, `team-data` |
|
|
61
|
+
| Employees | `employee-alice-chen`, `employee-bob-martinez`, `employee-carol-kim`, `employee-david-osei`, `employee-emma-johnson` |
|
|
62
|
+
| Custom metric sample data | `ai-spend` (8 weeks, fictional, per-employee and team rollups) |
|
|
63
|
+
| Plugin | `team-ai-spend` (team-scoped spend visualization) |
|
|
64
|
+
| Scorecard | `ai-spend-scorecard` (bronze/silver/gold budget compliance) |
|
|
65
|
+
| Sync script | `scripts/sync-claude-spend.py` |
|
|
66
|
+
| GH Actions workflow | `.github/workflows/sync-claude-spend.yaml` |
|
|
67
|
+
|
|
68
|
+
## Prerequisites
|
|
69
|
+
|
|
70
|
+
Before installing, create the `ai-spend` custom metric definition in your Cortex instance:
|
|
71
|
+
**Eng Intel → Custom Metrics → New Metric**, key: `ai-spend`.
|
|
72
|
+
|
|
73
|
+
## Installation
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
cortex solutions install -s ai-spend
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## After Installing
|
|
80
|
+
|
|
81
|
+
**Create the team-member catalog**
|
|
82
|
+
|
|
83
|
+
Enable the relationship type catalog so you can browse team membership from the Cortex UI:
|
|
84
|
+
|
|
85
|
+
1. Go to **Settings → Entity Relationship Types → team-member**
|
|
86
|
+
2. Click **Edit** and enable **Create relationship type catalog**
|
|
87
|
+
3. Save
|
|
88
|
+
|
|
89
|
+
**View the AI Spend Budget Compliance scorecard**
|
|
90
|
+
|
|
91
|
+
An `ai-spend-scorecard` is installed automatically and tracks whether each team's weekly spend stays within budget:
|
|
92
|
+
|
|
93
|
+
- **Bronze** — team has `ai-spend` metric data in the last 8 days and a budget set
|
|
94
|
+
- **Silver** — spend is within 25% of budget
|
|
95
|
+
- **Gold** — spend is at or under budget
|
|
96
|
+
|
|
97
|
+
The sample data is pre-loaded with budgets that produce an interesting distribution: team-platform achieves Gold, team-frontend and team-engineering achieve Silver, and team-data achieves Bronze.
|
|
98
|
+
|
|
99
|
+
To set a budget for a real team, add `ai-budget-weekly` as custom data on the team entity:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
cortex custom-data add -t <team-tag> -k ai-budget-weekly -v <weekly-budget-dollars>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**View the Team AI Spend plugin**
|
|
106
|
+
|
|
107
|
+
A `team-ai-spend` plugin is installed automatically and appears on every team entity page. It shows the team's total weekly AI spend and a per-member breakdown bar chart, pulling live data from the `ai-spend` custom metric.
|
|
108
|
+
|
|
109
|
+
**Create a Tabular View for AI spend**
|
|
110
|
+
|
|
111
|
+
Build a Data Explorer table to compare spend across employees and teams:
|
|
112
|
+
|
|
113
|
+
1. Go to **Eng Intelligence → Data Explorer**
|
|
114
|
+
2. Select the **Table** view
|
|
115
|
+
3. Click **Add column**, find `ai-spend` under the **Custom** category, and click **View metric**
|
|
116
|
+
4. Set **Group by → Team** and enable **Show hierarchy** to roll up spend to team level
|
|
117
|
+
5. Click **Save As** to name and save the view for future use
|
|
118
|
+
|
|
119
|
+
> Note: Tabular View creation is not yet available via API. It must be configured manually.
|
|
120
|
+
|
|
121
|
+
**Set up live Claude spend sync**
|
|
122
|
+
|
|
123
|
+
The sample entities include fictional spend data. To push real data from your Anthropic Claude Enterprise account weekly:
|
|
124
|
+
|
|
125
|
+
1. **Get an Analytics API key:**
|
|
126
|
+
- Sign in to claude.ai as the **primary owner** of your organization
|
|
127
|
+
- Go to **Organization settings → API**
|
|
128
|
+
- Enable public API access and create an Analytics API key
|
|
129
|
+
- (Only the primary owner can create this key — admin role is not sufficient)
|
|
130
|
+
|
|
131
|
+
2. **Add secrets to your GitHub repo:**
|
|
132
|
+
- `ANTHROPIC_ANALYTICS_KEY` — the Analytics API key from step 1
|
|
133
|
+
- `CORTEX_API_KEY` — your Cortex API key
|
|
134
|
+
|
|
135
|
+
3. **Copy the workflow** to your repo's `.github/workflows/` directory:
|
|
136
|
+
```bash
|
|
137
|
+
cp .github/workflows/sync-claude-spend.yaml <your-repo>/.github/workflows/
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
4. **Copy the script** to your repo's `scripts/` directory:
|
|
141
|
+
```bash
|
|
142
|
+
cp scripts/sync-claude-spend.py <your-repo>/scripts/
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The workflow runs every Monday at 06:00 UTC and can be triggered manually from the GitHub Actions tab.
|
|
146
|
+
|
|
147
|
+
**Customize the email domain**
|
|
148
|
+
|
|
149
|
+
The sync script maps `first.last@cortex.io` → `employee-first-last`. Set `EMAIL_DOMAIN` in the workflow env to match your company's domain:
|
|
150
|
+
|
|
151
|
+
```yaml
|
|
152
|
+
env:
|
|
153
|
+
EMAIL_DOMAIN: yourcompany.com
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Add your real employees**
|
|
157
|
+
|
|
158
|
+
The sample entities are fictional. Add your real employees as catalog entities with `x-cortex-type: employee` and tag them `employee-<first>-<last>` to match the email mapping.
|
|
159
|
+
|
|
160
|
+
**Notes**
|
|
161
|
+
|
|
162
|
+
- Users who authenticate Claude Code with a personal API key (not Enterprise OAuth) show $0 spend in the Analytics API and are skipped automatically.
|
|
163
|
+
- Cost data may take up to 24 hours to appear; dates at least 30 days old are considered final for billing purposes.
|
|
164
|
+
- The `ai-spend` custom metric definition must currently be created manually before installing. A future release will support auto-creation of custom metric definitions as part of `cortex solutions install`.
|
|
165
|
+
- The scorecard's Bronze rule uses a `P1Y` lookback to accommodate sample data. Once your weekly sync is running consistently, consider tightening it to `P8D` to ensure the rule only passes when data is fresh. The Silver and Gold rules use `P8D` and can similarly be adjusted to match your sync frequency.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
openapi: "3.0.0"
|
|
2
|
+
info:
|
|
3
|
+
title: Data
|
|
4
|
+
x-cortex-tag: team-data
|
|
5
|
+
x-cortex-type: team
|
|
6
|
+
x-cortex-description: Data engineering team
|
|
7
|
+
x-cortex-definition: {}
|
|
8
|
+
x-cortex-groups:
|
|
9
|
+
- ai-spend-demo
|
|
10
|
+
x-cortex-team:
|
|
11
|
+
members:
|
|
12
|
+
- name: Emma Johnson
|
|
13
|
+
email: emma.johnson@cortex.io
|
|
14
|
+
x-cortex-custom-data:
|
|
15
|
+
- key: ai-budget-weekly
|
|
16
|
+
value: 280
|
|
17
|
+
x-cortex-relationships:
|
|
18
|
+
- type: team-member
|
|
19
|
+
destinations:
|
|
20
|
+
- tag: employee-emma-johnson
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
openapi: "3.0.0"
|
|
2
|
+
info:
|
|
3
|
+
title: Engineering
|
|
4
|
+
x-cortex-tag: team-engineering
|
|
5
|
+
x-cortex-type: team
|
|
6
|
+
x-cortex-description: Top-level engineering organization
|
|
7
|
+
x-cortex-definition: {}
|
|
8
|
+
x-cortex-groups:
|
|
9
|
+
- ai-spend-demo
|
|
10
|
+
x-cortex-team:
|
|
11
|
+
members:
|
|
12
|
+
- name: Alice Chen
|
|
13
|
+
email: alice.chen@cortex.io
|
|
14
|
+
- name: Bob Martinez
|
|
15
|
+
email: bob.martinez@cortex.io
|
|
16
|
+
- name: Carol Kim
|
|
17
|
+
email: carol.kim@cortex.io
|
|
18
|
+
- name: David Osei
|
|
19
|
+
email: david.osei@cortex.io
|
|
20
|
+
- name: Emma Johnson
|
|
21
|
+
email: emma.johnson@cortex.io
|
|
22
|
+
x-cortex-custom-data:
|
|
23
|
+
- key: ai-budget-weekly
|
|
24
|
+
value: 1100
|
|
25
|
+
x-cortex-relationships:
|
|
26
|
+
- type: team-member
|
|
27
|
+
destinations:
|
|
28
|
+
- tag: team-platform
|
|
29
|
+
- tag: team-frontend
|
|
30
|
+
- tag: team-data
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
openapi: "3.0.0"
|
|
2
|
+
info:
|
|
3
|
+
title: Frontend
|
|
4
|
+
x-cortex-tag: team-frontend
|
|
5
|
+
x-cortex-type: team
|
|
6
|
+
x-cortex-description: Frontend engineering team
|
|
7
|
+
x-cortex-definition: {}
|
|
8
|
+
x-cortex-groups:
|
|
9
|
+
- ai-spend-demo
|
|
10
|
+
x-cortex-team:
|
|
11
|
+
members:
|
|
12
|
+
- name: Carol Kim
|
|
13
|
+
email: carol.kim@cortex.io
|
|
14
|
+
- name: David Osei
|
|
15
|
+
email: david.osei@cortex.io
|
|
16
|
+
x-cortex-custom-data:
|
|
17
|
+
- key: ai-budget-weekly
|
|
18
|
+
value: 360
|
|
19
|
+
x-cortex-relationships:
|
|
20
|
+
- type: team-member
|
|
21
|
+
destinations:
|
|
22
|
+
- tag: employee-carol-kim
|
|
23
|
+
- tag: employee-david-osei
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
openapi: "3.0.0"
|
|
2
|
+
info:
|
|
3
|
+
title: Platform
|
|
4
|
+
x-cortex-tag: team-platform
|
|
5
|
+
x-cortex-type: team
|
|
6
|
+
x-cortex-description: Platform engineering team
|
|
7
|
+
x-cortex-definition: {}
|
|
8
|
+
x-cortex-groups:
|
|
9
|
+
- ai-spend-demo
|
|
10
|
+
x-cortex-team:
|
|
11
|
+
members:
|
|
12
|
+
- name: Alice Chen
|
|
13
|
+
email: alice.chen@cortex.io
|
|
14
|
+
- name: Bob Martinez
|
|
15
|
+
email: bob.martinez@cortex.io
|
|
16
|
+
x-cortex-custom-data:
|
|
17
|
+
- key: ai-budget-weekly
|
|
18
|
+
value: 480
|
|
19
|
+
x-cortex-relationships:
|
|
20
|
+
- type: team-member
|
|
21
|
+
destinations:
|
|
22
|
+
- tag: employee-alice-chen
|
|
23
|
+
- tag: employee-bob-martinez
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"values": [
|
|
3
|
+
{ "entityTag": "employee-alice-chen", "timestamp": "2026-06-09T00:00:00", "value": 38.20 },
|
|
4
|
+
{ "entityTag": "employee-alice-chen", "timestamp": "2026-06-16T00:00:00", "value": 56.40 },
|
|
5
|
+
{ "entityTag": "employee-alice-chen", "timestamp": "2026-06-23T00:00:00", "value": 79.80 },
|
|
6
|
+
{ "entityTag": "employee-alice-chen", "timestamp": "2026-06-30T00:00:00", "value": 127.30 },
|
|
7
|
+
{ "entityTag": "employee-alice-chen", "timestamp": "2026-07-07T00:00:00", "value": 179.60 },
|
|
8
|
+
{ "entityTag": "employee-alice-chen", "timestamp": "2026-07-14T00:00:00", "value": 234.80 },
|
|
9
|
+
{ "entityTag": "employee-alice-chen", "timestamp": "2026-07-21T00:00:00", "value": 268.90 },
|
|
10
|
+
{ "entityTag": "employee-alice-chen", "timestamp": "2026-07-28T00:00:00", "value": 291.40 },
|
|
11
|
+
|
|
12
|
+
{ "entityTag": "employee-bob-martinez", "timestamp": "2026-06-09T00:00:00", "value": 24.10 },
|
|
13
|
+
{ "entityTag": "employee-bob-martinez", "timestamp": "2026-06-16T00:00:00", "value": 31.80 },
|
|
14
|
+
{ "entityTag": "employee-bob-martinez", "timestamp": "2026-06-23T00:00:00", "value": 54.20 },
|
|
15
|
+
{ "entityTag": "employee-bob-martinez", "timestamp": "2026-06-30T00:00:00", "value": 75.40 },
|
|
16
|
+
{ "entityTag": "employee-bob-martinez", "timestamp": "2026-07-07T00:00:00", "value": 118.30 },
|
|
17
|
+
{ "entityTag": "employee-bob-martinez", "timestamp": "2026-07-14T00:00:00", "value": 141.70 },
|
|
18
|
+
{ "entityTag": "employee-bob-martinez", "timestamp": "2026-07-21T00:00:00", "value": 173.90 },
|
|
19
|
+
{ "entityTag": "employee-bob-martinez", "timestamp": "2026-07-28T00:00:00", "value": 181.60 },
|
|
20
|
+
|
|
21
|
+
{ "entityTag": "employee-carol-kim", "timestamp": "2026-06-09T00:00:00", "value": 27.30 },
|
|
22
|
+
{ "entityTag": "employee-carol-kim", "timestamp": "2026-06-16T00:00:00", "value": 47.10 },
|
|
23
|
+
{ "entityTag": "employee-carol-kim", "timestamp": "2026-06-23T00:00:00", "value": 66.80 },
|
|
24
|
+
{ "entityTag": "employee-carol-kim", "timestamp": "2026-06-30T00:00:00", "value": 108.40 },
|
|
25
|
+
{ "entityTag": "employee-carol-kim", "timestamp": "2026-07-07T00:00:00", "value": 157.20 },
|
|
26
|
+
{ "entityTag": "employee-carol-kim", "timestamp": "2026-07-14T00:00:00", "value": 189.60 },
|
|
27
|
+
{ "entityTag": "employee-carol-kim", "timestamp": "2026-07-21T00:00:00", "value": 231.50 },
|
|
28
|
+
{ "entityTag": "employee-carol-kim", "timestamp": "2026-07-28T00:00:00", "value": 244.70 },
|
|
29
|
+
|
|
30
|
+
{ "entityTag": "employee-david-osei", "timestamp": "2026-06-09T00:00:00", "value": 14.80 },
|
|
31
|
+
{ "entityTag": "employee-david-osei", "timestamp": "2026-06-16T00:00:00", "value": 27.30 },
|
|
32
|
+
{ "entityTag": "employee-david-osei", "timestamp": "2026-06-23T00:00:00", "value": 41.20 },
|
|
33
|
+
{ "entityTag": "employee-david-osei", "timestamp": "2026-06-30T00:00:00", "value": 55.60 },
|
|
34
|
+
{ "entityTag": "employee-david-osei", "timestamp": "2026-07-07T00:00:00", "value": 89.40 },
|
|
35
|
+
{ "entityTag": "employee-david-osei", "timestamp": "2026-07-14T00:00:00", "value": 104.80 },
|
|
36
|
+
{ "entityTag": "employee-david-osei", "timestamp": "2026-07-21T00:00:00", "value": 129.70 },
|
|
37
|
+
{ "entityTag": "employee-david-osei", "timestamp": "2026-07-28T00:00:00", "value": 135.50 },
|
|
38
|
+
|
|
39
|
+
{ "entityTag": "employee-emma-johnson", "timestamp": "2026-06-09T00:00:00", "value": 46.20 },
|
|
40
|
+
{ "entityTag": "employee-emma-johnson", "timestamp": "2026-06-16T00:00:00", "value": 62.90 },
|
|
41
|
+
{ "entityTag": "employee-emma-johnson", "timestamp": "2026-06-23T00:00:00", "value": 107.40 },
|
|
42
|
+
{ "entityTag": "employee-emma-johnson", "timestamp": "2026-06-30T00:00:00", "value": 149.80 },
|
|
43
|
+
{ "entityTag": "employee-emma-johnson", "timestamp": "2026-07-07T00:00:00", "value": 231.60 },
|
|
44
|
+
{ "entityTag": "employee-emma-johnson", "timestamp": "2026-07-14T00:00:00", "value": 279.30 },
|
|
45
|
+
{ "entityTag": "employee-emma-johnson", "timestamp": "2026-07-21T00:00:00", "value": 341.20 },
|
|
46
|
+
{ "entityTag": "employee-emma-johnson", "timestamp": "2026-07-28T00:00:00", "value": 358.70 },
|
|
47
|
+
|
|
48
|
+
{ "entityTag": "team-platform", "timestamp": "2026-06-09T00:00:00", "value": 62.30 },
|
|
49
|
+
{ "entityTag": "team-platform", "timestamp": "2026-06-16T00:00:00", "value": 88.20 },
|
|
50
|
+
{ "entityTag": "team-platform", "timestamp": "2026-06-23T00:00:00", "value": 134.00 },
|
|
51
|
+
{ "entityTag": "team-platform", "timestamp": "2026-06-30T00:00:00", "value": 202.70 },
|
|
52
|
+
{ "entityTag": "team-platform", "timestamp": "2026-07-07T00:00:00", "value": 297.90 },
|
|
53
|
+
{ "entityTag": "team-platform", "timestamp": "2026-07-14T00:00:00", "value": 376.50 },
|
|
54
|
+
{ "entityTag": "team-platform", "timestamp": "2026-07-21T00:00:00", "value": 442.80 },
|
|
55
|
+
{ "entityTag": "team-platform", "timestamp": "2026-07-28T00:00:00", "value": 473.00 },
|
|
56
|
+
|
|
57
|
+
{ "entityTag": "team-frontend", "timestamp": "2026-06-09T00:00:00", "value": 42.10 },
|
|
58
|
+
{ "entityTag": "team-frontend", "timestamp": "2026-06-16T00:00:00", "value": 74.40 },
|
|
59
|
+
{ "entityTag": "team-frontend", "timestamp": "2026-06-23T00:00:00", "value": 108.00 },
|
|
60
|
+
{ "entityTag": "team-frontend", "timestamp": "2026-06-30T00:00:00", "value": 164.00 },
|
|
61
|
+
{ "entityTag": "team-frontend", "timestamp": "2026-07-07T00:00:00", "value": 246.60 },
|
|
62
|
+
{ "entityTag": "team-frontend", "timestamp": "2026-07-14T00:00:00", "value": 294.40 },
|
|
63
|
+
{ "entityTag": "team-frontend", "timestamp": "2026-07-21T00:00:00", "value": 361.20 },
|
|
64
|
+
{ "entityTag": "team-frontend", "timestamp": "2026-07-28T00:00:00", "value": 380.20 },
|
|
65
|
+
|
|
66
|
+
{ "entityTag": "team-data", "timestamp": "2026-06-09T00:00:00", "value": 46.20 },
|
|
67
|
+
{ "entityTag": "team-data", "timestamp": "2026-06-16T00:00:00", "value": 62.90 },
|
|
68
|
+
{ "entityTag": "team-data", "timestamp": "2026-06-23T00:00:00", "value": 107.40 },
|
|
69
|
+
{ "entityTag": "team-data", "timestamp": "2026-06-30T00:00:00", "value": 149.80 },
|
|
70
|
+
{ "entityTag": "team-data", "timestamp": "2026-07-07T00:00:00", "value": 231.60 },
|
|
71
|
+
{ "entityTag": "team-data", "timestamp": "2026-07-14T00:00:00", "value": 279.30 },
|
|
72
|
+
{ "entityTag": "team-data", "timestamp": "2026-07-21T00:00:00", "value": 341.20 },
|
|
73
|
+
{ "entityTag": "team-data", "timestamp": "2026-07-28T00:00:00", "value": 358.70 },
|
|
74
|
+
|
|
75
|
+
{ "entityTag": "team-engineering", "timestamp": "2026-06-09T00:00:00", "value": 150.60 },
|
|
76
|
+
{ "entityTag": "team-engineering", "timestamp": "2026-06-16T00:00:00", "value": 225.50 },
|
|
77
|
+
{ "entityTag": "team-engineering", "timestamp": "2026-06-23T00:00:00", "value": 349.40 },
|
|
78
|
+
{ "entityTag": "team-engineering", "timestamp": "2026-06-30T00:00:00", "value": 516.50 },
|
|
79
|
+
{ "entityTag": "team-engineering", "timestamp": "2026-07-07T00:00:00", "value": 776.10 },
|
|
80
|
+
{ "entityTag": "team-engineering", "timestamp": "2026-07-14T00:00:00", "value": 950.20 },
|
|
81
|
+
{ "entityTag": "team-engineering", "timestamp": "2026-07-21T00:00:00", "value": 1145.20 },
|
|
82
|
+
{ "entityTag": "team-engineering", "timestamp": "2026-07-28T00:00:00", "value": 1211.90 }
|
|
83
|
+
]
|
|
84
|
+
}
|
cortexapps_cli-1.29.0/cortexapps_cli/solutions/ai-spend/entity-relationship-types/team-member.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tag": "team-member",
|
|
3
|
+
"name": "Team Member",
|
|
4
|
+
"description": "Links a team to its direct members, which can be sub-teams or individual employees. Use this single relationship type to walk the full org hierarchy in the catalog.",
|
|
5
|
+
"definitionLocation": "SOURCE",
|
|
6
|
+
"isSingleSource": false,
|
|
7
|
+
"isSingleDestination": false,
|
|
8
|
+
"allowCycles": false,
|
|
9
|
+
"sourcesFilter": {
|
|
10
|
+
"include": true,
|
|
11
|
+
"types": ["team"],
|
|
12
|
+
"providers": []
|
|
13
|
+
},
|
|
14
|
+
"destinationsFilter": {
|
|
15
|
+
"include": true,
|
|
16
|
+
"types": ["team", "employee"],
|
|
17
|
+
"providers": []
|
|
18
|
+
},
|
|
19
|
+
"inheritances": []
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tag": "team-ai-spend",
|
|
3
|
+
"name": "Team AI Spend",
|
|
4
|
+
"description": "Visualizes per-member Claude AI spend for the team with a weekly total and per-member breakdown chart.",
|
|
5
|
+
"isDraft": false,
|
|
6
|
+
"minimumRoleRequired": "VIEWER",
|
|
7
|
+
"contexts": [
|
|
8
|
+
{ "type": "ENTITY", "entityFilter": { "type": "CQL_FILTER", "category": "Team", "query": "hasGroup(\"ai-spend-demo\")" } }
|
|
9
|
+
],
|
|
10
|
+
"proxyTag": null,
|
|
11
|
+
"iconTag": null,
|
|
12
|
+
"version": null,
|
|
13
|
+
"blob": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Team AI Spend</title>\n <script src=\"https://cdn.jsdelivr.net/npm/@iframe-resizer/child@5/index.umd.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js\"></script>\n <style>\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: #fff;\n color: #111827;\n padding: 16px;\n font-size: 14px;\n }\n #loading { text-align: center; padding: 40px 16px; color: #9ca3af; }\n #error {\n background: #fef2f2;\n border: 1px solid #fca5a5;\n border-radius: 8px;\n padding: 12px 16px;\n color: #dc2626;\n display: none;\n }\n .total-card {\n background: linear-gradient(135deg, #6366f1 0%, #7c3aed 100%);\n border-radius: 12px;\n padding: 20px 24px;\n color: #fff;\n margin-bottom: 20px;\n }\n .total-label {\n font-size: 12px;\n font-weight: 500;\n opacity: 0.8;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n margin-bottom: 6px;\n }\n .total-amount {\n font-size: 40px;\n font-weight: 700;\n line-height: 1;\n margin-bottom: 4px;\n }\n .total-period { font-size: 12px; opacity: 0.65; }\n .section-label {\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: #6b7280;\n margin-bottom: 12px;\n }\n #content { display: none; }\n </style>\n</head>\n<body>\n <div id=\"loading\">Loading AI spend data\u2026</div>\n <div id=\"error\"></div>\n <div id=\"content\">\n <div class=\"total-card\" id=\"total-card\">\n <div class=\"total-label\">Team Weekly AI Spend</div>\n <div class=\"total-amount\" id=\"total-amount\">$0</div>\n <div class=\"total-period\" id=\"total-period\"></div>\n </div>\n <div class=\"section-label\">Per-Member Breakdown</div>\n <canvas id=\"chart\"></canvas>\n </div>\n\n <script>\n function tagToName(tag) {\n return tag.replace(/^employee-/, '').split('-')\n .map(function(w) { return w.charAt(0).toUpperCase() + w.slice(1); })\n .join(' ');\n }\n\n function getContext() {\n return new Promise(function(resolve, reject) {\n var ch = new MessageChannel();\n ch.port1.onmessage = function(e) {\n if (e.data.ok) resolve(e.data.data);\n else reject(e.data);\n };\n parent.postMessage({ type: 'context' }, '*', [ch.port2]);\n });\n }\n\n function proxyFetch(url, init) {\n return new Promise(function(resolve, reject) {\n var ch = new MessageChannel();\n ch.port1.onmessage = function(e) {\n if (e.data.ok) resolve(e.data.data);\n else reject(new Error('fetch failed: ' + JSON.stringify(e.data)));\n };\n parent.postMessage({ type: 'fetch', input: url, init: init || {} }, '*', [ch.port2]);\n });\n }\n\n function showError(msg) {\n document.getElementById('loading').style.display = 'none';\n var el = document.getElementById('error');\n el.textContent = msg;\n el.style.display = 'block';\n }\n\n async function main() {\n try {\n var pluginCtx = await getContext();\n var apiBaseUrl = pluginCtx.apiBaseUrl;\n var teamTag = (pluginCtx.entity && pluginCtx.entity.tag) || pluginCtx.tag;\n\n if (!teamTag) { showError('No team entity found in plugin context.'); return; }\n\n var relRes = await proxyFetch(apiBaseUrl + '/relationships/team-member?pageSize=500');\n if (!relRes.ok) throw new Error('Failed to fetch team relationships: ' + relRes.status);\n var relData = JSON.parse(relRes.body);\n\n var adjacency = {};\n (relData.relationships || []).forEach(function(r) {\n if (!r.sourceEntity || !r.destinationEntity) return;\n var src = r.sourceEntity.tag;\n if (!adjacency[src]) adjacency[src] = [];\n adjacency[src].push(r.destinationEntity);\n });\n\n function collectEmployees(tag, visited) {\n if (visited[tag]) return [];\n visited[tag] = true;\n var employees = [];\n (adjacency[tag] || []).forEach(function(dst) {\n if (adjacency[dst.tag]) {\n employees = employees.concat(collectEmployees(dst.tag, visited));\n } else {\n employees.push({ tag: dst.tag, name: dst.name || tagToName(dst.tag) });\n }\n });\n return employees;\n }\n\n var members = collectEmployees(teamTag, {});\n if (!members.length) {\n showError('No team members found. Make sure team-member relationships are set up.');\n return;\n }\n\n var budget = null;\n try {\n var budgetRes = await proxyFetch(apiBaseUrl + '/catalog/' + encodeURIComponent(teamTag) + '/custom-data/ai-budget-weekly');\n if (budgetRes.ok) {\n var budgetData = JSON.parse(budgetRes.body);\n if (budgetData.value != null) budget = parseFloat(budgetData.value);\n }\n } catch (e) {}\n\n var spendResults = await Promise.all(members.map(async function(m) {\n try {\n var r = await proxyFetch(apiBaseUrl + '/eng-intel/custom-metrics/ai-spend/entity/' + encodeURIComponent(m.tag) + '?pageSize=100');\n if (!r.ok) return Object.assign({}, m, { spend: 0, timestamp: null });\n var d = JSON.parse(r.body);\n var values = d.data || d.values || d.series || [];\n if (!values.length) return Object.assign({}, m, { spend: 0, timestamp: null });\n values.sort(function(a, b) { return new Date(b.timestamp) - new Date(a.timestamp); });\n return Object.assign({}, m, { spend: values[0].value || 0, timestamp: values[0].timestamp });\n } catch (e) {\n return Object.assign({}, m, { spend: 0, timestamp: null });\n }\n }));\n\n var withSpend = spendResults.filter(function(m) { return m.spend > 0; });\n if (!withSpend.length) {\n showError('No ai-spend metric data found. Make sure the ai-spend custom metric has been pushed for team members.');\n return;\n }\n\n var total = withSpend.reduce(function(s, m) { return s + m.spend; }, 0);\n var latestDate = withSpend.map(function(m) { return m.timestamp; }).filter(Boolean).sort().pop();\n\n document.getElementById('loading').style.display = 'none';\n document.getElementById('content').style.display = 'block';\n document.getElementById('total-amount').textContent = '$' + total.toFixed(2);\n if (latestDate) {\n var d = new Date(latestDate);\n document.getElementById('total-period').textContent =\n 'Week of ' + d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });\n }\n\n if (budget !== null) {\n var card = document.getElementById('total-card');\n card.style.background = total <= budget\n ? 'linear-gradient(135deg, #16a34a 0%, #15803d 100%)'\n : 'linear-gradient(135deg, #dc2626 0%, #b91c1c 100%)';\n }\n\n // Chart entries: Team Total bar first, then members sorted by spend desc\n var sorted = withSpend.slice().sort(function(a, b) { return b.spend - a.spend; });\n var entries = [{ name: 'Team Total', spend: total, isTotal: true }].concat(sorted);\n\n var palette = ['#6366f1', '#7c3aed', '#8b5cf6', '#a78bfa', '#c4b5fd'];\n\n // Budget line plugin drawn over the Total bar only\n var budgetLinePlugin = {\n id: 'budgetLine',\n afterDatasetsDraw: function(chart) {\n if (budget == null) return;\n var c = chart.ctx;\n var xScale = chart.scales.x;\n var yScale = chart.scales.y;\n var x = xScale.getPixelForValue(budget);\n // Constrain to the Total bar row only (index 0)\n var meta = chart.getDatasetMeta(0);\n if (!meta.data || !meta.data[0]) return;\n var barTop = meta.data[0].y - meta.data[0].height / 2;\n var barBottom = meta.data[0].y + meta.data[0].height / 2;\n c.save();\n c.beginPath();\n c.moveTo(x, barTop - 18);\n c.lineTo(x, yScale.bottom);\n c.strokeStyle = '#111827';\n c.lineWidth = 2;\n c.setLineDash([6, 4]);\n c.stroke();\n c.setLineDash([]);\n c.font = 'bold 11px -apple-system, BlinkMacSystemFont, sans-serif';\n c.fillStyle = '#111827';\n c.textAlign = 'center';\n c.fillText('Budget: $' + budget, x, barTop - 20);\n c.restore();\n }\n };\n\n // Dataset 1: green for total bar (up to budget), purple for members\n // Dataset 2: red for total bar overflow only\n var greenData = entries.map(function(e, i) {\n if (e.isTotal) return budget !== null ? Math.min(e.spend, budget) : e.spend;\n return e.spend;\n });\n var redData = entries.map(function(e) {\n return (e.isTotal && budget !== null) ? Math.max(0, e.spend - budget) : 0;\n });\n var barColors = entries.map(function(e, i) {\n if (e.isTotal) return budget !== null ? '#16a34a' : '#6366f1';\n return palette[(i - 1) % palette.length];\n });\n\n new Chart(document.getElementById('chart').getContext('2d'), {\n type: 'bar',\n plugins: budget !== null ? [budgetLinePlugin] : [],\n data: {\n labels: entries.map(function(e) { return e.name; }),\n datasets: [\n {\n label: 'Spend',\n data: greenData,\n backgroundColor: barColors,\n borderRadius: 0,\n borderSkipped: false\n },\n {\n label: 'Over Budget',\n data: redData,\n backgroundColor: '#dc2626',\n borderRadius: 4,\n borderSkipped: false\n }\n ]\n },\n options: {\n indexAxis: 'y',\n responsive: true,\n layout: { padding: { top: budget !== null ? 24 : 0 } },\n interaction: { mode: 'nearest', axis: 'x', intersect: true },\n plugins: {\n legend: { display: false },\n tooltip: {\n filter: function(item) { return item.raw > 0; },\n callbacks: {\n label: function(item) {\n var entry = entries[item.dataIndex];\n if (entry.isTotal && budget !== null) {\n if (item.datasetIndex === 1) return ' Over by: +$' + item.raw.toFixed(2);\n return ' Budget: $' + budget.toFixed(2) + '/wk';\n }\n return ' $' + entry.spend.toFixed(2) + '/wk';\n }\n }\n }\n },\n scales: {\n x: {\n stacked: true,\n beginAtZero: true,\n suggestedMax: budget !== null ? Math.max(budget, total) * 1.1 : undefined,\n ticks: { callback: function(v) { return '$' + v; } },\n grid: { color: '#f3f4f6' }\n },\n y: { stacked: true, grid: { display: false } }\n }\n }\n });\n\n } catch (err) {\n showError('Error: ' + err.message);\n }\n }\n\n main();\n </script>\n</body>\n</html>"
|
|
14
|
+
}
|