Transform meetings page into context page with tabs

Add structured organization context with 5 markdown sections (Product, Architecture, Team, Processes, Customers) editable inline. Meetings are now a tab within the context page. Moved all GraphQL queries from hooks/graph/MeetingGraph.ts into colocated components following new best practices. Updated database schema, backend services, GraphQL resolvers, and MCP API to support the new context fields and structure.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2026-03-19 17:42:14 +01:00
parent c2a1843c67
commit a9ebeef0fa
25 changed files with 1156 additions and 380 deletions

View File

@@ -0,0 +1,34 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package context
import (
"github.com/spf13/cobra"
"go.probo.inc/probo/pkg/cmd/cmdutil"
"go.probo.inc/probo/pkg/cmd/context/get"
"go.probo.inc/probo/pkg/cmd/context/update"
)
func NewCmdContext(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "context <command>",
Short: "Manage organization context",
}
cmd.AddCommand(get.NewCmdGet(f))
cmd.AddCommand(update.NewCmdUpdate(f))
return cmd
}

155
pkg/cmd/context/get/get.go Normal file
View File

@@ -0,0 +1,155 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package get
import (
"encoding/json"
"fmt"
"github.com/charmbracelet/lipgloss"
"github.com/spf13/cobra"
"go.probo.inc/probo/pkg/cli/api"
"go.probo.inc/probo/pkg/cmd/cmdutil"
)
const getQuery = `
query($id: ID!) {
node(id: $id) {
... on Organization {
id
name
context {
product
architecture
team
processes
customers
}
}
}
}
`
type getResponse struct {
Node *struct {
ID string `json:"id"`
Name string `json:"name"`
Context *struct {
Product *string `json:"product"`
Architecture *string `json:"architecture"`
Team *string `json:"team"`
Processes *string `json:"processes"`
Customers *string `json:"customers"`
} `json:"context"`
} `json:"node"`
}
func NewCmdGet(f *cmdutil.Factory) *cobra.Command {
var (
flagOrg string
flagOutput *string
)
cmd := &cobra.Command{
Use: "get",
Short: "Get organization context",
Example: ` prb context get --org <org-id>`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil {
return err
}
cfg, err := f.Config()
if err != nil {
return err
}
host, hc, err := cfg.DefaultHost()
if err != nil {
return err
}
orgID := flagOrg
if orgID == "" {
orgID = hc.Organization
}
if orgID == "" {
return fmt.Errorf("organization ID is required: pass --org or run `prb auth login`")
}
client := api.NewClient(
host,
hc.Token,
"/api/console/v1/graphql",
cfg.HTTPTimeoutDuration(),
)
data, err := client.Do(
getQuery,
map[string]any{"id": orgID},
)
if err != nil {
return err
}
var resp getResponse
if err := json.Unmarshal(data, &resp); err != nil {
return fmt.Errorf("cannot parse response: %w", err)
}
if resp.Node == nil {
return fmt.Errorf("organization %s not found", orgID)
}
if *flagOutput == cmdutil.OutputJSON {
return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node.Context)
}
ctx := resp.Node.Context
out := f.IOStreams.Out
bold := lipgloss.NewStyle().Bold(true)
label := lipgloss.NewStyle().Foreground(lipgloss.Color("242"))
sections := []struct {
title string
value *string
}{
{"Product", ctx.Product},
{"Architecture", ctx.Architecture},
{"Team", ctx.Team},
{"Processes", ctx.Processes},
{"Customers", ctx.Customers},
}
for _, s := range sections {
_, _ = fmt.Fprintf(out, "%s\n", bold.Render(s.title))
if s.value != nil && *s.value != "" {
_, _ = fmt.Fprintf(out, "%s\n\n", *s.value)
} else {
_, _ = fmt.Fprintf(out, "%s\n\n", label.Render("(empty)"))
}
}
return nil
},
}
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
flagOutput = cmdutil.AddOutputFlag(cmd)
return cmd
}

View File

@@ -0,0 +1,151 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package update
import (
"encoding/json"
"fmt"
"github.com/spf13/cobra"
"go.probo.inc/probo/pkg/cli/api"
"go.probo.inc/probo/pkg/cmd/cmdutil"
)
const updateMutation = `
mutation($input: UpdateOrganizationContextInput!) {
updateOrganizationContext(input: $input) {
context {
organizationId
product
architecture
team
processes
customers
}
}
}
`
type updateResponse struct {
UpdateOrganizationContext struct {
Context struct {
OrganizationID string `json:"organizationId"`
Product *string `json:"product"`
Architecture *string `json:"architecture"`
Team *string `json:"team"`
Processes *string `json:"processes"`
Customers *string `json:"customers"`
} `json:"context"`
} `json:"updateOrganizationContext"`
}
func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
var (
flagOrg string
flagProduct string
flagArchitecture string
flagTeam string
flagProcesses string
flagCustomers string
)
cmd := &cobra.Command{
Use: "update",
Short: "Update organization context",
Example: ` prb context update --org <org-id> --product "We build compliance software"
prb context update --org <org-id> --architecture "Monolith deployed on AWS ECS"`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := f.Config()
if err != nil {
return err
}
host, hc, err := cfg.DefaultHost()
if err != nil {
return err
}
orgID := flagOrg
if orgID == "" {
orgID = hc.Organization
}
if orgID == "" {
return fmt.Errorf("organization ID is required: pass --org or run `prb auth login`")
}
input := map[string]any{
"organizationId": orgID,
}
if cmd.Flags().Changed("product") {
input["product"] = flagProduct
}
if cmd.Flags().Changed("architecture") {
input["architecture"] = flagArchitecture
}
if cmd.Flags().Changed("team") {
input["team"] = flagTeam
}
if cmd.Flags().Changed("processes") {
input["processes"] = flagProcesses
}
if cmd.Flags().Changed("customers") {
input["customers"] = flagCustomers
}
if len(input) == 1 {
return fmt.Errorf("at least one section flag is required (--product, --architecture, --team, --processes, --customers)")
}
client := api.NewClient(
host,
hc.Token,
"/api/console/v1/graphql",
cfg.HTTPTimeoutDuration(),
)
data, err := client.Do(
updateMutation,
map[string]any{"input": input},
)
if err != nil {
return err
}
var resp updateResponse
if err := json.Unmarshal(data, &resp); err != nil {
return fmt.Errorf("cannot parse response: %w", err)
}
_, _ = fmt.Fprintf(
f.IOStreams.Out,
"Updated context for organization %s\n",
resp.UpdateOrganizationContext.Context.OrganizationID,
)
return nil
},
}
cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID")
cmd.Flags().StringVar(&flagProduct, "product", "", "Product description (markdown)")
cmd.Flags().StringVar(&flagArchitecture, "architecture", "", "Architecture description (markdown)")
cmd.Flags().StringVar(&flagTeam, "team", "", "Team description (markdown)")
cmd.Flags().StringVar(&flagProcesses, "processes", "", "Processes description (markdown)")
cmd.Flags().StringVar(&flagCustomers, "customers", "", "Customers description (markdown)")
return cmd
}

View File

@@ -22,6 +22,7 @@ import (
"go.probo.inc/probo/pkg/cmd/cmdutil"
"go.probo.inc/probo/pkg/cmd/completion"
cmdconfig "go.probo.inc/probo/pkg/cmd/config"
cmdcontext "go.probo.inc/probo/pkg/cmd/context"
"go.probo.inc/probo/pkg/cmd/control"
"go.probo.inc/probo/pkg/cmd/finding"
"go.probo.inc/probo/pkg/cmd/framework"
@@ -68,6 +69,7 @@ func NewCmdRoot(f *cmdutil.Factory) *cobra.Command {
cmd.AddCommand(browse.NewCmdBrowse(f))
cmd.AddCommand(completion.NewCmdCompletion(f))
cmd.AddCommand(cmdconfig.NewCmdConfig(f))
cmd.AddCommand(cmdcontext.NewCmdContext(f))
cmd.AddCommand(control.NewCmdControl(f))
cmd.AddCommand(finding.NewCmdFinding(f))
cmd.AddCommand(framework.NewCmdFramework(f))

View File

@@ -0,0 +1,6 @@
ALTER TABLE organization_contexts ADD COLUMN product TEXT;
ALTER TABLE organization_contexts ADD COLUMN architecture TEXT;
ALTER TABLE organization_contexts ADD COLUMN team TEXT;
ALTER TABLE organization_contexts ADD COLUMN processes TEXT;
ALTER TABLE organization_contexts ADD COLUMN customers TEXT;
ALTER TABLE organization_contexts DROP COLUMN summary;

View File

@@ -29,7 +29,11 @@ import (
type (
OrganizationContext struct {
OrganizationID gid.GID `db:"organization_id"`
Summary *string `db:"summary"`
Product *string `db:"product"`
Architecture *string `db:"architecture"`
Team *string `db:"team"`
Processes *string `db:"processes"`
Customers *string `db:"customers"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
@@ -44,7 +48,11 @@ func (oc *OrganizationContext) LoadByOrganizationID(
q := `
SELECT
organization_id,
summary,
product,
architecture,
team,
processes,
customers,
created_at,
updated_at
FROM
@@ -88,13 +96,21 @@ func (oc *OrganizationContext) Insert(
INSERT INTO organization_contexts (
organization_id,
tenant_id,
summary,
product,
architecture,
team,
processes,
customers,
created_at,
updated_at
) VALUES (
@organization_id,
@tenant_id,
@summary,
@product,
@architecture,
@team,
@processes,
@customers,
@created_at,
@updated_at
)
@@ -103,7 +119,11 @@ INSERT INTO organization_contexts (
args := pgx.StrictNamedArgs{
"organization_id": oc.OrganizationID,
"tenant_id": scope.GetTenantID(),
"summary": oc.Summary,
"product": oc.Product,
"architecture": oc.Architecture,
"team": oc.Team,
"processes": oc.Processes,
"customers": oc.Customers,
"created_at": oc.CreatedAt,
"updated_at": oc.UpdatedAt,
}
@@ -124,7 +144,11 @@ func (oc *OrganizationContext) Update(
q := `
UPDATE organization_contexts
SET
summary = @summary,
product = @product,
architecture = @architecture,
team = @team,
processes = @processes,
customers = @customers,
updated_at = @updated_at
WHERE
%s
@@ -135,7 +159,11 @@ WHERE
args := pgx.StrictNamedArgs{
"organization_id": oc.OrganizationID,
"summary": oc.Summary,
"product": oc.Product,
"architecture": oc.Architecture,
"team": oc.Team,
"processes": oc.Processes,
"customers": oc.Customers,
"updated_at": oc.UpdatedAt,
}

View File

@@ -49,7 +49,11 @@ type (
UpdateOrganizationContextRequest struct {
OrganizationID gid.GID
Summary **string
Product **string
Architecture **string
Team **string
Processes **string
Customers **string
}
)
@@ -72,7 +76,11 @@ func (uocr *UpdateOrganizationContextRequest) Validate() error {
v := validator.New()
v.Check(uocr.OrganizationID, "organization_id", validator.Required(), validator.GID(coredata.OrganizationEntityType))
v.Check(uocr.Summary, "summary", validator.SafeText(30_000))
v.Check(uocr.Product, "product", validator.SafeText(30_000))
v.Check(uocr.Architecture, "architecture", validator.SafeText(30_000))
v.Check(uocr.Team, "team", validator.SafeText(30_000))
v.Check(uocr.Processes, "processes", validator.SafeText(30_000))
v.Check(uocr.Customers, "customers", validator.SafeText(30_000))
return v.Error()
}
@@ -102,7 +110,7 @@ func (s OrganizationService) Get(
return organization, nil
}
func (s OrganizationService) GetContextSummary(
func (s OrganizationService) GetContext(
ctx context.Context,
organizationID gid.GID,
) (*coredata.OrganizationContext, error) {
@@ -154,8 +162,34 @@ func (s OrganizationService) UpdateContext(
return fmt.Errorf("cannot load organization context: %w", err)
}
if req.Summary != nil {
organizationContext.Summary = *req.Summary
updated := false
if req.Product != nil {
organizationContext.Product = *req.Product
updated = true
}
if req.Architecture != nil {
organizationContext.Architecture = *req.Architecture
updated = true
}
if req.Team != nil {
organizationContext.Team = *req.Team
updated = true
}
if req.Processes != nil {
organizationContext.Processes = *req.Processes
updated = true
}
if req.Customers != nil {
organizationContext.Customers = *req.Customers
updated = true
}
if updated {
organizationContext.UpdatedAt = time.Now()
if err := organizationContext.Update(ctx, tx, s.svc.scope); err != nil {

View File

@@ -3846,7 +3846,11 @@ type Mutation {
# Input Types
input UpdateOrganizationContextInput {
organizationId: ID!
summary: String @goField(omittable: true)
product: String @goField(omittable: true)
architecture: String @goField(omittable: true)
team: String @goField(omittable: true)
processes: String @goField(omittable: true)
customers: String @goField(omittable: true)
}
input UpdateTrustCenterInput {
@@ -4753,7 +4757,11 @@ type UpdateOrganizationContextPayload {
type OrganizationContext {
organizationId: ID!
summary: String
product: String
architecture: String
team: String
processes: String
customers: String
}
type UpdateTrustCenterPayload {

View File

@@ -21,6 +21,10 @@ import (
func NewOrganizationContext(oc *coredata.OrganizationContext) *OrganizationContext {
return &OrganizationContext{
OrganizationID: oc.OrganizationID,
Summary: oc.Summary,
Product: oc.Product,
Architecture: oc.Architecture,
Team: oc.Team,
Processes: oc.Processes,
Customers: oc.Customers,
}
}

View File

@@ -2048,7 +2048,11 @@ func (r *mutationResolver) UpdateOrganizationContext(ctx context.Context, input
req := probo.UpdateOrganizationContextRequest{
OrganizationID: input.OrganizationID,
Summary: gqlutils.UnwrapOmittable(input.Summary),
Product: gqlutils.UnwrapOmittable(input.Product),
Architecture: gqlutils.UnwrapOmittable(input.Architecture),
Team: gqlutils.UnwrapOmittable(input.Team),
Processes: gqlutils.UnwrapOmittable(input.Processes),
Customers: gqlutils.UnwrapOmittable(input.Customers),
}
organizationContext, err := prb.Organizations.UpdateContext(ctx, req)
@@ -6387,7 +6391,7 @@ func (r *organizationResolver) Context(ctx context.Context, obj *types.Organizat
prb := r.ProboService(ctx, obj.ID.TenantID())
orgContext, err := prb.Organizations.GetContextSummary(ctx, obj.ID)
orgContext, err := prb.Organizations.GetContext(ctx, obj.ID)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot load organization context", log.Error(err))
return nil, gqlutils.Internal(ctx)

View File

@@ -3241,3 +3241,43 @@ func (r *Resolver) UnarchiveDocumentTool(ctx context.Context, req *mcp.CallToolR
Document: types.NewDocument(document, profileIDs(approverPage)),
}, nil
}
func (r *Resolver) GetOrganizationContextTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetOrganizationContextInput) (*mcp.CallToolResult, types.GetOrganizationContextOutput, error) {
r.MustAuthorize(ctx, input.OrganizationID, probo.ActionOrganizationContextGet)
prb := r.ProboService(ctx, input.OrganizationID)
orgContext, err := prb.Organizations.GetContext(ctx, input.OrganizationID)
if err != nil {
return nil, types.GetOrganizationContextOutput{}, fmt.Errorf("cannot get organization context: %w", err)
}
return nil, types.GetOrganizationContextOutput{
OrganizationContext: types.NewOrganizationContext(orgContext),
}, nil
}
func (r *Resolver) UpdateOrganizationContextTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateOrganizationContextInput) (*mcp.CallToolResult, types.UpdateOrganizationContextOutput, error) {
r.MustAuthorize(ctx, input.OrganizationID, probo.ActionOrganizationContextUpdate)
prb := r.ProboService(ctx, input.OrganizationID)
orgContext, err := prb.Organizations.UpdateContext(
ctx,
probo.UpdateOrganizationContextRequest{
OrganizationID: input.OrganizationID,
Product: &input.Product,
Architecture: &input.Architecture,
Team: &input.Team,
Processes: &input.Processes,
Customers: &input.Customers,
},
)
if err != nil {
return nil, types.UpdateOrganizationContextOutput{}, fmt.Errorf("cannot update organization context: %w", err)
}
return nil, types.UpdateOrganizationContextOutput{
OrganizationContext: types.NewOrganizationContext(orgContext),
}, nil
}

View File

@@ -6323,6 +6323,79 @@ components:
$ref: "#/components/schemas/GID"
description: Deleted applicability statement ID
OrganizationContext:
type: object
required:
- organization_id
properties:
organization_id:
$ref: "#/components/schemas/GID"
description: Organization ID
product:
type: string
description: Product description
architecture:
type: string
description: Architecture description
team:
type: string
description: Team description
processes:
type: string
description: Processes description
customers:
type: string
description: Customers description
GetOrganizationContextInput:
type: object
required:
- organization_id
properties:
organization_id:
$ref: "#/components/schemas/GID"
description: Organization ID
GetOrganizationContextOutput:
type: object
required:
- organization_context
properties:
organization_context:
$ref: "#/components/schemas/OrganizationContext"
UpdateOrganizationContextInput:
type: object
required:
- organization_id
properties:
organization_id:
$ref: "#/components/schemas/GID"
description: Organization ID
product:
type: string
description: Product description
architecture:
type: string
description: Architecture description
team:
type: string
description: Team description
processes:
type: string
description: Processes description
customers:
type: string
description: Customers description
UpdateOrganizationContextOutput:
type: object
required:
- organization_context
properties:
organization_context:
$ref: "#/components/schemas/OrganizationContext"
tools:
- name: listOrganizations
description: List all organizations the user has access to
@@ -7441,3 +7514,20 @@ tools:
$ref: "#/components/schemas/DeleteApplicabilityStatementInput"
outputSchema:
$ref: "#/components/schemas/DeleteApplicabilityStatementOutput"
- name: getOrganizationContext
description: Get the organization context containing structured sections about the company
hints:
readonly: true
idempotent: true
inputSchema:
$ref: "#/components/schemas/GetOrganizationContextInput"
outputSchema:
$ref: "#/components/schemas/GetOrganizationContextOutput"
- name: updateOrganizationContext
description: Update the organization context sections (product, architecture, team, processes, customers)
hints:
readonly: false
inputSchema:
$ref: "#/components/schemas/UpdateOrganizationContextInput"
outputSchema:
$ref: "#/components/schemas/UpdateOrganizationContextOutput"

View File

@@ -0,0 +1,30 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package types
import (
"go.probo.inc/probo/pkg/coredata"
)
func NewOrganizationContext(oc *coredata.OrganizationContext) *OrganizationContext {
return &OrganizationContext{
OrganizationID: oc.OrganizationID,
Product: oc.Product,
Architecture: oc.Architecture,
Team: oc.Team,
Processes: oc.Processes,
Customers: oc.Customers,
}
}