703 lines
20 KiB
YAML
703 lines
20 KiB
YAML
workflow_version: "v003"
|
|
feature: "add label management system"
|
|
created_at: "2025-12-18T17:45:00"
|
|
updated_at: "2025-12-18T17:50:00"
|
|
approved_at: null
|
|
status: draft
|
|
revision: 2
|
|
revision_notes: "Updated to remove external model references (existing models in codebase)"
|
|
|
|
# Note: This design extends existing models (Label, Artist, Song, Album, User)
|
|
# which are already defined in prisma/schema.prisma
|
|
# The dependencies on these models are implicit (referenced by foreign key field names)
|
|
|
|
# ============================================================================
|
|
# LAYER 1: DATA MODELS
|
|
# ============================================================================
|
|
data_models:
|
|
- id: model_label_invitation
|
|
name: LabelInvitation
|
|
description: "Invitations from labels to artists to join their roster"
|
|
table_name: label_invitations
|
|
fields:
|
|
- name: id
|
|
type: uuid
|
|
constraints: [primary_key]
|
|
description: "Unique identifier"
|
|
- name: labelId
|
|
type: uuid
|
|
constraints: [foreign_key, not_null, indexed]
|
|
description: "Reference to the inviting label (FK to labels table)"
|
|
- name: artistId
|
|
type: uuid
|
|
constraints: [foreign_key, not_null, indexed]
|
|
description: "Reference to the invited artist (FK to artists table)"
|
|
- name: status
|
|
type: enum
|
|
enum_values: [pending, accepted, declined, expired]
|
|
constraints: [not_null, default]
|
|
default: pending
|
|
description: "Current status of the invitation"
|
|
- name: message
|
|
type: text
|
|
constraints: []
|
|
description: "Optional message from label to artist"
|
|
- name: expiresAt
|
|
type: datetime
|
|
constraints: [not_null]
|
|
description: "When the invitation expires"
|
|
- name: createdAt
|
|
type: datetime
|
|
constraints: [not_null]
|
|
description: "When invitation was created"
|
|
- name: updatedAt
|
|
type: datetime
|
|
constraints: [not_null]
|
|
description: "When invitation was last updated"
|
|
relations: [] # Relations are to existing models, handled by FK fields
|
|
indexes:
|
|
- fields: [labelId, artistId]
|
|
unique: true
|
|
name: label_artist_unique
|
|
- fields: [artistId, status]
|
|
unique: false
|
|
name: artist_pending_invitations
|
|
timestamps: true
|
|
soft_delete: false
|
|
validations:
|
|
- field: expiresAt
|
|
rule: "future_date"
|
|
message: "Expiration date must be in the future"
|
|
|
|
# ============================================================================
|
|
# LAYER 2: API ENDPOINTS
|
|
# ============================================================================
|
|
api_endpoints:
|
|
# === LABEL CRUD ===
|
|
- id: api_get_label
|
|
method: GET
|
|
path: /api/labels/[id]
|
|
summary: "Get label details"
|
|
description: "Retrieve label profile with artist roster and statistics"
|
|
tags: [labels]
|
|
path_params:
|
|
- name: id
|
|
type: string
|
|
description: "Label ID"
|
|
responses:
|
|
- status: 200
|
|
description: "Label found"
|
|
schema:
|
|
type: object
|
|
properties:
|
|
- name: id
|
|
type: uuid
|
|
- name: name
|
|
type: string
|
|
- name: slug
|
|
type: string
|
|
- name: description
|
|
type: string
|
|
- name: logoUrl
|
|
type: string
|
|
- name: website
|
|
type: string
|
|
- name: artists
|
|
type: array
|
|
- name: _count
|
|
type: object
|
|
example:
|
|
id: "550e8400-e29b-41d4-a716-446655440000"
|
|
name: "Sonic Records"
|
|
slug: "sonic-records"
|
|
description: "Independent music label"
|
|
artists: []
|
|
_count: { artists: 5, songs: 120 }
|
|
- status: 404
|
|
description: "Label not found"
|
|
schema:
|
|
type: object
|
|
properties:
|
|
- name: error
|
|
type: string
|
|
depends_on_models: [model_label_invitation] # New model only
|
|
depends_on_apis: []
|
|
auth:
|
|
required: false
|
|
roles: []
|
|
|
|
- id: api_update_label
|
|
method: PUT
|
|
path: /api/labels/[id]
|
|
summary: "Update label profile"
|
|
description: "Update label name, description, logo, and website"
|
|
tags: [labels]
|
|
path_params:
|
|
- name: id
|
|
type: string
|
|
description: "Label ID"
|
|
request_body:
|
|
content_type: application/json
|
|
schema:
|
|
type: object
|
|
properties:
|
|
- name: name
|
|
type: string
|
|
required: false
|
|
description: "Label name"
|
|
- name: description
|
|
type: string
|
|
required: false
|
|
description: "Label description"
|
|
- name: logoUrl
|
|
type: string
|
|
required: false
|
|
description: "Logo URL"
|
|
- name: website
|
|
type: string
|
|
required: false
|
|
description: "Website URL"
|
|
responses:
|
|
- status: 200
|
|
description: "Label updated"
|
|
schema:
|
|
type: object
|
|
properties:
|
|
- name: id
|
|
type: uuid
|
|
- name: name
|
|
type: string
|
|
- name: slug
|
|
type: string
|
|
- name: description
|
|
type: string
|
|
- status: 401
|
|
description: "Unauthorized"
|
|
- status: 403
|
|
description: "Not label owner"
|
|
- status: 404
|
|
description: "Label not found"
|
|
depends_on_models: []
|
|
depends_on_apis: []
|
|
auth:
|
|
required: true
|
|
roles: [label]
|
|
|
|
# === LABEL STATISTICS ===
|
|
- id: api_get_label_stats
|
|
method: GET
|
|
path: /api/labels/[id]/stats
|
|
summary: "Get label statistics"
|
|
description: "Get artist count, song count, album count, and total plays"
|
|
tags: [labels, statistics]
|
|
path_params:
|
|
- name: id
|
|
type: string
|
|
description: "Label ID"
|
|
responses:
|
|
- status: 200
|
|
description: "Statistics retrieved"
|
|
schema:
|
|
type: object
|
|
properties:
|
|
- name: artistCount
|
|
type: integer
|
|
- name: songCount
|
|
type: integer
|
|
- name: albumCount
|
|
type: integer
|
|
- name: totalPlays
|
|
type: integer
|
|
example:
|
|
artistCount: 5
|
|
songCount: 120
|
|
albumCount: 15
|
|
totalPlays: 45000
|
|
- status: 404
|
|
description: "Label not found"
|
|
depends_on_models: []
|
|
depends_on_apis: []
|
|
auth:
|
|
required: false
|
|
roles: []
|
|
|
|
# === LABEL INVITATIONS ===
|
|
- id: api_list_label_invitations
|
|
method: GET
|
|
path: /api/labels/[id]/invitations
|
|
summary: "List label invitations"
|
|
description: "Get all invitations sent by this label"
|
|
tags: [labels, invitations]
|
|
path_params:
|
|
- name: id
|
|
type: string
|
|
description: "Label ID"
|
|
query_params:
|
|
- name: status
|
|
type: string
|
|
required: false
|
|
description: "Filter by status (pending, accepted, declined, expired)"
|
|
responses:
|
|
- status: 200
|
|
description: "Invitations list"
|
|
schema:
|
|
type: array
|
|
properties:
|
|
- name: id
|
|
type: uuid
|
|
- name: artist
|
|
type: object
|
|
- name: status
|
|
type: string
|
|
- name: message
|
|
type: string
|
|
- name: expiresAt
|
|
type: datetime
|
|
- status: 401
|
|
description: "Unauthorized"
|
|
- status: 403
|
|
description: "Not label owner"
|
|
depends_on_models: [model_label_invitation]
|
|
depends_on_apis: []
|
|
auth:
|
|
required: true
|
|
roles: [label]
|
|
|
|
- id: api_create_label_invitation
|
|
method: POST
|
|
path: /api/labels/[id]/invitations
|
|
summary: "Create artist invitation"
|
|
description: "Send invitation to an artist to join the label"
|
|
tags: [labels, invitations]
|
|
path_params:
|
|
- name: id
|
|
type: string
|
|
description: "Label ID"
|
|
request_body:
|
|
content_type: application/json
|
|
schema:
|
|
type: object
|
|
properties:
|
|
- name: artistId
|
|
type: string
|
|
required: true
|
|
description: "ID of artist to invite"
|
|
- name: message
|
|
type: string
|
|
required: false
|
|
description: "Optional invitation message"
|
|
responses:
|
|
- status: 201
|
|
description: "Invitation created"
|
|
schema:
|
|
type: object
|
|
properties:
|
|
- name: id
|
|
type: uuid
|
|
- name: artistId
|
|
type: uuid
|
|
- name: status
|
|
type: string
|
|
- name: expiresAt
|
|
type: datetime
|
|
- status: 400
|
|
description: "Artist already has label or pending invitation"
|
|
- status: 401
|
|
description: "Unauthorized"
|
|
- status: 403
|
|
description: "Not label owner"
|
|
- status: 404
|
|
description: "Artist not found"
|
|
depends_on_models: [model_label_invitation]
|
|
depends_on_apis: []
|
|
auth:
|
|
required: true
|
|
roles: [label]
|
|
|
|
- id: api_delete_label_invitation
|
|
method: DELETE
|
|
path: /api/labels/[id]/invitations/[invitationId]
|
|
summary: "Cancel invitation"
|
|
description: "Cancel a pending invitation"
|
|
tags: [labels, invitations]
|
|
path_params:
|
|
- name: id
|
|
type: string
|
|
description: "Label ID"
|
|
- name: invitationId
|
|
type: string
|
|
description: "Invitation ID"
|
|
responses:
|
|
- status: 200
|
|
description: "Invitation cancelled"
|
|
- status: 401
|
|
description: "Unauthorized"
|
|
- status: 403
|
|
description: "Not label owner"
|
|
- status: 404
|
|
description: "Invitation not found"
|
|
depends_on_models: [model_label_invitation]
|
|
depends_on_apis: []
|
|
auth:
|
|
required: true
|
|
roles: [label]
|
|
|
|
# === ARTIST INVITATION HANDLING ===
|
|
- id: api_list_artist_invitations
|
|
method: GET
|
|
path: /api/artists/[id]/invitations
|
|
summary: "List artist invitations"
|
|
description: "Get all invitations received by this artist"
|
|
tags: [artists, invitations]
|
|
path_params:
|
|
- name: id
|
|
type: string
|
|
description: "Artist ID"
|
|
responses:
|
|
- status: 200
|
|
description: "Invitations list"
|
|
schema:
|
|
type: array
|
|
properties:
|
|
- name: id
|
|
type: uuid
|
|
- name: label
|
|
type: object
|
|
- name: status
|
|
type: string
|
|
- name: message
|
|
type: string
|
|
- name: expiresAt
|
|
type: datetime
|
|
- status: 401
|
|
description: "Unauthorized"
|
|
- status: 403
|
|
description: "Not the artist"
|
|
depends_on_models: [model_label_invitation]
|
|
depends_on_apis: []
|
|
auth:
|
|
required: true
|
|
roles: [artist]
|
|
|
|
- id: api_respond_to_invitation
|
|
method: POST
|
|
path: /api/artists/[id]/invitations/[invitationId]/respond
|
|
summary: "Respond to invitation"
|
|
description: "Accept or decline a label invitation"
|
|
tags: [artists, invitations]
|
|
path_params:
|
|
- name: id
|
|
type: string
|
|
description: "Artist ID"
|
|
- name: invitationId
|
|
type: string
|
|
description: "Invitation ID"
|
|
request_body:
|
|
content_type: application/json
|
|
schema:
|
|
type: object
|
|
properties:
|
|
- name: response
|
|
type: string
|
|
required: true
|
|
description: "Accept or decline (accept/decline)"
|
|
responses:
|
|
- status: 200
|
|
description: "Response recorded"
|
|
schema:
|
|
type: object
|
|
properties:
|
|
- name: status
|
|
type: string
|
|
- name: label
|
|
type: object
|
|
- status: 400
|
|
description: "Invalid response or invitation already processed"
|
|
- status: 401
|
|
description: "Unauthorized"
|
|
- status: 403
|
|
description: "Not the invited artist"
|
|
- status: 404
|
|
description: "Invitation not found"
|
|
depends_on_models: [model_label_invitation]
|
|
depends_on_apis: []
|
|
auth:
|
|
required: true
|
|
roles: [artist]
|
|
|
|
# === ARTIST MANAGEMENT ===
|
|
- id: api_remove_artist_from_label
|
|
method: DELETE
|
|
path: /api/labels/[id]/artists/[artistId]
|
|
summary: "Remove artist from label"
|
|
description: "Remove an artist from the label roster"
|
|
tags: [labels, artists]
|
|
path_params:
|
|
- name: id
|
|
type: string
|
|
description: "Label ID"
|
|
- name: artistId
|
|
type: string
|
|
description: "Artist ID"
|
|
responses:
|
|
- status: 200
|
|
description: "Artist removed from label"
|
|
- status: 401
|
|
description: "Unauthorized"
|
|
- status: 403
|
|
description: "Not label owner"
|
|
- status: 404
|
|
description: "Artist not in label"
|
|
depends_on_models: []
|
|
depends_on_apis: []
|
|
auth:
|
|
required: true
|
|
roles: [label]
|
|
|
|
# ============================================================================
|
|
# LAYER 3: UI PAGES
|
|
# ============================================================================
|
|
pages:
|
|
- id: page_label_profile
|
|
name: "Label Profile"
|
|
path: /label/[id]
|
|
layout: layout_main
|
|
data_needs:
|
|
- api_id: api_get_label
|
|
purpose: "Display label info and artist roster"
|
|
on_load: true
|
|
- api_id: api_get_label_stats
|
|
purpose: "Display label statistics"
|
|
on_load: true
|
|
components:
|
|
- component_label_header
|
|
- component_label_stats
|
|
- component_artist_roster
|
|
seo:
|
|
title: "{{label.name}} | Sonic Cloud"
|
|
description: "{{label.description}}"
|
|
auth:
|
|
required: false
|
|
roles: []
|
|
redirect: null
|
|
|
|
- id: page_label_dashboard
|
|
name: "Label Dashboard"
|
|
path: /label/dashboard
|
|
layout: layout_main
|
|
data_needs:
|
|
- api_id: api_get_label
|
|
purpose: "Display label info"
|
|
on_load: true
|
|
- api_id: api_get_label_stats
|
|
purpose: "Display statistics"
|
|
on_load: true
|
|
- api_id: api_list_label_invitations
|
|
purpose: "Show pending invitations"
|
|
on_load: true
|
|
components:
|
|
- component_label_stats
|
|
- component_artist_roster
|
|
- component_invitation_card
|
|
- component_invite_artist_modal
|
|
seo:
|
|
title: "Label Dashboard | Sonic Cloud"
|
|
description: "Manage your label"
|
|
auth:
|
|
required: true
|
|
roles: [label]
|
|
redirect: /login
|
|
|
|
- id: page_label_settings
|
|
name: "Label Settings"
|
|
path: /label/settings
|
|
layout: layout_main
|
|
data_needs:
|
|
- api_id: api_get_label
|
|
purpose: "Load current label data for editing"
|
|
on_load: true
|
|
components:
|
|
- component_label_profile_form
|
|
seo:
|
|
title: "Label Settings | Sonic Cloud"
|
|
description: "Edit your label profile"
|
|
auth:
|
|
required: true
|
|
roles: [label]
|
|
redirect: /login
|
|
|
|
- id: page_label_invitations
|
|
name: "Label Invitations"
|
|
path: /label/invitations
|
|
layout: layout_main
|
|
data_needs:
|
|
- api_id: api_list_label_invitations
|
|
purpose: "List all invitations"
|
|
on_load: true
|
|
components:
|
|
- component_invitation_card
|
|
- component_invite_artist_modal
|
|
seo:
|
|
title: "Invitations | Sonic Cloud"
|
|
description: "Manage artist invitations"
|
|
auth:
|
|
required: true
|
|
roles: [label]
|
|
redirect: /login
|
|
|
|
# ============================================================================
|
|
# LAYER 3: UI COMPONENTS
|
|
# ============================================================================
|
|
components:
|
|
- id: component_label_card
|
|
name: LabelCard
|
|
props:
|
|
- name: label
|
|
type: "Label"
|
|
required: true
|
|
description: "Label object to display"
|
|
- name: showArtistCount
|
|
type: boolean
|
|
required: false
|
|
default: true
|
|
description: "Show number of artists"
|
|
events:
|
|
- name: onClick
|
|
payload: "string"
|
|
description: "Fires when card clicked, payload is label ID"
|
|
uses_apis: []
|
|
uses_components: []
|
|
internal_state: []
|
|
variants: [default, compact]
|
|
|
|
- id: component_label_header
|
|
name: LabelHeader
|
|
props:
|
|
- name: label
|
|
type: "Label"
|
|
required: true
|
|
description: "Label with full details"
|
|
- name: isOwner
|
|
type: boolean
|
|
required: false
|
|
default: false
|
|
description: "Is current user the label owner"
|
|
events:
|
|
- name: onEdit
|
|
payload: "void"
|
|
description: "Fires when edit button clicked"
|
|
uses_apis: []
|
|
uses_components: []
|
|
internal_state: []
|
|
variants: [default]
|
|
|
|
- id: component_label_stats
|
|
name: LabelStats
|
|
props:
|
|
- name: stats
|
|
type: "LabelStats"
|
|
required: true
|
|
description: "Statistics object"
|
|
events: []
|
|
uses_apis: []
|
|
uses_components: []
|
|
internal_state: []
|
|
variants: [default, compact]
|
|
|
|
- id: component_artist_roster
|
|
name: ArtistRoster
|
|
props:
|
|
- name: artists
|
|
type: "Artist[]"
|
|
required: true
|
|
description: "List of signed artists"
|
|
- name: isOwner
|
|
type: boolean
|
|
required: false
|
|
default: false
|
|
description: "Show management controls"
|
|
- name: emptyMessage
|
|
type: string
|
|
required: false
|
|
default: "No artists signed yet"
|
|
description: "Message when roster is empty"
|
|
events:
|
|
- name: onRemoveArtist
|
|
payload: "string"
|
|
description: "Fires when remove clicked, payload is artist ID"
|
|
- name: onArtistClick
|
|
payload: "string"
|
|
description: "Fires when artist clicked"
|
|
uses_apis: []
|
|
uses_components: [] # Uses existing ArtistCard component
|
|
internal_state: [removingArtistId]
|
|
variants: [grid, list]
|
|
|
|
- id: component_invitation_card
|
|
name: InvitationCard
|
|
props:
|
|
- name: invitation
|
|
type: "LabelInvitation"
|
|
required: true
|
|
description: "Invitation object"
|
|
- name: viewType
|
|
type: string
|
|
required: true
|
|
description: "Are we viewing as label or artist (label/artist)"
|
|
events:
|
|
- name: onAccept
|
|
payload: "string"
|
|
description: "Fires when accept clicked (artist view)"
|
|
- name: onDecline
|
|
payload: "string"
|
|
description: "Fires when decline clicked (artist view)"
|
|
- name: onCancel
|
|
payload: "string"
|
|
description: "Fires when cancel clicked (label view)"
|
|
uses_apis: []
|
|
uses_components: []
|
|
internal_state: [isProcessing]
|
|
variants: [default]
|
|
|
|
- id: component_invite_artist_modal
|
|
name: InviteArtistModal
|
|
props:
|
|
- name: isOpen
|
|
type: boolean
|
|
required: true
|
|
description: "Whether modal is open"
|
|
- name: labelId
|
|
type: string
|
|
required: true
|
|
description: "Label ID sending invitation"
|
|
events:
|
|
- name: onClose
|
|
payload: "void"
|
|
description: "Fires when modal should close"
|
|
- name: onInviteSent
|
|
payload: "LabelInvitation"
|
|
description: "Fires when invitation successfully sent"
|
|
uses_apis: [api_create_label_invitation]
|
|
uses_components: []
|
|
internal_state: [searchQuery, selectedArtist, message, isSubmitting]
|
|
variants: [default]
|
|
|
|
- id: component_label_profile_form
|
|
name: LabelProfileForm
|
|
props:
|
|
- name: label
|
|
type: "Label"
|
|
required: true
|
|
description: "Current label data"
|
|
events:
|
|
- name: onSave
|
|
payload: "Label"
|
|
description: "Fires when form saved successfully"
|
|
- name: onCancel
|
|
payload: "void"
|
|
description: "Fires when cancel clicked"
|
|
uses_apis: [api_update_label]
|
|
uses_components: []
|
|
internal_state: [formData, isSubmitting, errors]
|
|
variants: [default]
|