diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..381bea7 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,32 @@ +{ + "permissions": { + "allow": [ + "Bash(npm run *:*)", + "Bash(npm install*)", + "Bash(npm test*)", + "Bash(dotnet build*)", + "Bash(dotnet restore*)", + "Bash(dotnet run*)", + "Bash(dotnet format*)", + "Bash(git status)", + "Bash(git diff*)", + "Bash(git log*)", + "Bash(git branch*)", + "Bash(az login)", + "Bash(az account*)" + ], + "deny": [ + "Bash(rm -rf*)", + "Bash(git push --force* main)", + "Bash(git push --force* master)", + "Bash(git reset --hard*)", + "Bash(npm publish*)", + "Bash(dotnet publish*)" + ], + "ask": [ + "Bash(git push*)", + "Bash(git merge*)", + "Bash(git rebase*)" + ] + } +} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..edfd211 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,176 @@ +# CLAUDE.md - Data Model Viewer Monorepo + +This file provides guidance to Claude Code when working with the Data Model Viewer monorepo. + +## Monorepo Overview + +Data Model Viewer is a hybrid C#/.NET and Next.js application for visualizing Microsoft Dataverse metadata. The monorepo contains three main projects: + +``` +DataModelViewer/ +├── Generator/ # C# .NET 8 - Extracts metadata from Dataverse +├── Website/ # Next.js 15 - Interactive web application +└── Infrastructure/ # Azure Bicep - IaC for deployment +``` + +## Project Relationships + +**Data Flow**: +``` +Dataverse → Generator → Website/generated/Data.ts → Website Frontend +``` + +**Development Workflow**: +1. Generator connects to Dataverse and extracts metadata +2. Generator outputs TypeScript file to `Website/generated/Data.ts` +3. Website imports and visualizes the generated data +4. Infrastructure deploys Website to Azure App Service + +**Cross-Project Dependencies**: +- Website depends on Generator output (`generated/Data.ts`) +- Both projects share common type structures (entities, attributes, relationships) +- Infrastructure configurations must match Website environment requirements + +## When to Work on Each Project + +### Work on Generator when: +- Adding support for new Dataverse metadata types +- Changing data extraction logic or queries +- Modifying TypeScript output format +- Adding new analyzers (plugins, flows, web resources) + +### Work on Website when: +- Building UI components or pages +- Implementing diagram features +- Adding API endpoints +- Modifying authentication or session management +- Updating styling or user interactions + +### Work on Infrastructure when: +- Changing Azure resources or configuration +- Updating deployment parameters +- Modifying App Service settings +- Changing managed identity permissions + +### Work on Multiple Projects when: +- Adding new entity/attribute types (Generator DTOs + Website types) +- Changing serialization format (Generator output + Website import) +- Updating environment variables (Generator config + Infrastructure parameters) + +## Monorepo Conventions + +### Code Style +- **C# (Generator)**: Follow .NET conventions, use `nullable` reference types +- **TypeScript (Website)**: Strict mode enabled, use type-safe patterns +- **All**: Descriptive variable names, avoid abbreviations unless domain-specific + +### Commit Messages +Use conventional commits format: +``` +feat(generator): add support for virtual attributes +fix(website): resolve diagram zoom reset issue +chore(infra): update bicep API version +docs: update setup instructions in README +``` + +Scope options: `generator`, `website`, `infra`, `pipeline`, `root` + +### Branch Naming +- `features/description` - New features +- `fix/description` - Bug fixes +- `patches/description` - Hot fixes for production + +### Pull Requests +- Run tests in both projects before creating PR +- Ensure Generator output is up to date if metadata types changed +- Target `main` branch (see gitStatus for current state) +- Include description of changes and affected projects + +## Environment Setup + +### Prerequisites +- .NET 8.0 SDK +- Node.js 24 LTS +- Azure CLI (for local Generator authentication) +- Git + +## Testing Strategy + +### Generator Testing +- Run against test Dataverse environment +- Verify generated TypeScript is valid +- Check console output for warnings + +### Website Testing +- Run `npm run lint` to check for errors +- Test in browser at http://localhost:3000 +- Verify diagram interactions work correctly manally + +### Integration Testing +- Generate data from actual environment +- Load in local Website instance +- Test end-to-end workflows (diagram creation, saving, loading) + +## Common Cross-Project Tasks + +### Adding a New Attribute Type + +1. **Generator**: Create DTO in `Generator/DTO/Attributes/NewAttributeType.cs` +2. **Generator**: Update mapping in `DataverseService.cs:MapAttribute()` +3. **Website**: Add type to `lib/Types.ts` AttributeType union +4. **Website**: Create renderer in `components/datamodelview/attributes/NewAttributeRenderer.tsx` +5. **Test**: Generate data and verify display in Website + +### Changing Environment Variables + +1. **Generator**: Update `appsettings.local.json` structure if needed +2. **Website**: Add to `.env.local` and document in Website/CLAUDE.md +3. **Infrastructure**: Add parameter to `main.bicep` if required for deployment +4. **Pipeline**: Update `azure-pipelines-deploy-jobs.yml` to pass variable +5. **Documentation**: Update README.md setup instructions + +### Updating Dependencies + +- **Generator**: `dotnet list package --outdated` then update `.csproj` +- **Website**: `npm outdated` then update `package.json` +- **Test**: Run both projects after updates to ensure compatibility + +## Project-Specific Documentation + +For detailed architecture, development commands, and project-specific guidance: + +- **Generator**: See [Generator/CLAUDE.md](Generator/CLAUDE.md) +- **Website**: See [Website/CLAUDE.md](Website/CLAUDE.md) +- **Infrastructure**: See [Infrastructure/CLAUDE.md](Infrastructure/CLAUDE.md) +- **Architecture Deep Dive**: See [PROJECT_CONTEXT.md](PROJECT_CONTEXT.md) + +## Quick Reference + +### Run All Linters +```bash +# Generator (C# format check) +cd Generator && dotnet format --verify-no-changes + +# Website +cd Website && npm run lint +``` + +### Full Build Pipeline +```bash +# From repository root +cd Generator +dotnet build --configuration Release +dotnet run --OutputFolder ../Website/generated +cd ../Website +npm install +npm run prepipeline # Copy stub files if needed +npm run build +``` + +## Important Reminders + +- Always regenerate `Website/generated/Data.ts` after Generator changes +- Check that Generator has access to Dataverse (use `az login` locally) +- Never commit `.env.local` or `appsettings.local.json` files +- Website depends on generated data - Generator must run first +- Azure deployment requires Managed Identity permissions setup diff --git a/Generator/.claude/settings.json b/Generator/.claude/settings.json new file mode 100644 index 0000000..163d208 --- /dev/null +++ b/Generator/.claude/settings.json @@ -0,0 +1,21 @@ +{ + "permissions": { + "allow": [ + "Bash(dotnet build*)", + "Bash(dotnet restore*)", + "Bash(dotnet run*)", + "Bash(dotnet clean*)", + "Bash(dotnet format*)", + "Bash(dotnet list package*)", + "Bash(az login)", + "Bash(az account*)", + "Bash(git status)", + "Bash(git diff*)" + ], + "deny": [ + "Bash(rm -rf*)", + "Bash(dotnet publish*)" + ], + "ask": [] + } +} diff --git a/Generator/CLAUDE.md b/Generator/CLAUDE.md new file mode 100644 index 0000000..d10271b --- /dev/null +++ b/Generator/CLAUDE.md @@ -0,0 +1,414 @@ +# CLAUDE.md - Generator Project + +This file provides guidance to Claude Code when working with the Generator project. + +## Project Overview + +The Generator is a C# .NET 8 console application that extracts metadata from Microsoft Dataverse and generates TypeScript definitions for the Website project. It analyzes entities, attributes, relationships, security roles, plugins, flows, and web resources. + +## Technology Stack + +- **.NET 8.0** with C# 13 +- **Microsoft.PowerPlatform.Dataverse.Client** v1.2.2 - Dataverse SDK +- **Azure.Identity** v1.13.1 - Authentication via DefaultAzureCredential +- **System.Linq.Dynamic.Core** v1.6.7 - Dynamic LINQ queries +- **Microsoft.Extensions.Configuration** - Configuration management +- **Microsoft.Extensions.Logging** - Console logging + +## Architecture + +### Entry Point: Program.cs + +Simple flow: +1. Load configuration from environment variables and `appsettings.local.json` +2. Create DataverseService with logger +3. Call `GetFilteredMetadata()` to extract data +4. Pass to WebsiteBuilder to generate TypeScript output + +### Core Services + +#### DataverseService.cs +**Responsibility**: Connect to Dataverse and extract all metadata + +**Key Methods**: +- `GetFilteredMetadata()` - Main orchestrator, filters by solutions +- `GetEntities()` - Query all EntityMetadata +- `MapEntity()` - Convert Dataverse entity to DTO +- `MapAttribute()` - Convert attributes to polymorphic type-safe DTOs +- `GetRelationships()` - Extract N:1, 1:N, N:N relationships + +**Authentication**: Uses `DefaultAzureCredential` which tries in order: +1. Environment variables (for pipelines) +2. Azure CLI credentials (for local dev) +3. Managed Identity (for Azure deployment) + +**Filtering**: Only includes entities from specified solutions in `DataverseSolutionNames` config + +#### WebsiteBuilder.cs +**Responsibility**: Generate TypeScript file from metadata DTOs + +**Output**: `Website/generated/Data.ts` containing: +```typescript +export const EntityGroups: Map +export const Warnings: SolutionWarningsList[] +export const SolutionComponents: SolutionComponentList[] +export const timestamp: string +export const logo: string | null +``` + +**Key Methods**: +- `AddData()` - Orchestrates file generation +- Groups entities by category (from entity descriptions or config) +- Converts C# DTOs to TypeScript type syntax + +#### MetadataExtensions.cs +**Responsibility**: Detect customizations to standard fields + +**Key Logic**: +- `StandardFieldHasChanged()` - Detects if OOB fields were modified +- Compares display names and descriptions against defaults (English, Danish) +- Checks if StatusCode/StateCode options have custom (unmanaged) values +- Returns true if any customization detected + +**Use Case**: Website can hide/show standard vs. customized fields + +### Directory Structure + +``` +Generator/ +├── DTO/ # Data Transfer Objects +│ ├── Attributes/ # Attribute type DTOs (polymorphic) +│ ├── Entity.cs # Entity DTO +│ ├── Relationship.cs # Relationship DTO +│ ├── SecurityRole.cs # Security role DTO +│ └── Solution.cs # Solution component DTOs +│ +├── Services/ # Analyzers for special components +│ ├── PluginAnalyzer.cs # Extracts plugin steps and dependencies +│ ├── FlowAnalyzer.cs # Parses Power Automate flows +│ └── WebResourceAnalyzer.cs # Analyzes JavaScript web resources +│ +├── Queries/ # FetchXML and query helpers +│ └── (Query definition files) +│ +├── Program.cs # Entry point +├── DataverseService.cs # Main Dataverse interaction +├── WebsiteBuilder.cs # TypeScript generator +├── MetadataExtensions.cs # Customization detection +├── ClientExtensions.cs # ServiceClient helpers +├── UtilityExtensions.cs # General utilities +└── Generator.csproj # Project file +``` + +## Configuration + +### appsettings.local.json (required for local development) + +```json +{ + "DataverseUrl": "https://org.crm4.dynamics.com", + "DataverseSolutionNames": "SolutionUniqueName1,SolutionUniqueName2", + "OutputFolder": "../Website/generated", + "Verbosity": "Information" +} +``` + +**Configuration Keys**: +- `DataverseUrl` - Full URL to Dataverse environment (required) +- `DataverseSolutionNames` - Comma-separated list of solution unique names (required) +- `OutputFolder` - Path to output directory (default: `../Website/generated`) +- `Verbosity` - Logging level: `Trace`, `Debug`, `Information`, `Warning`, `Error` (default: `Information`) +- `TableGroups` - Optional semi-colon separated group definitions (format: `Group Name: table1, table2; Other Group: table3`) + +### Environment Variables (for pipelines) + +- `DataverseUrl` - Same as config +- `DataverseSolutionNames` - Same as config +- `OutputFolder` - Same as config +- `AZURE_CLIENT_ID` - Azure App Registration Client ID +- `AZURE_CLIENT_SECRET` - Azure App Registration Secret +- `AZURE_TENANT_ID` - Azure Tenant ID + +## Development Commands + +### Setup +```bash +cd Generator +dotnet restore +``` + +### Build +```bash +dotnet build +dotnet build --configuration Release +``` + +### Run (Generate Metadata) +```bash +# Default output to ../Website/generated +dotnet run + +# Custom output folder +dotnet run --OutputFolder /path/to/output + +# With specific configuration +dotnet run --DataverseUrl https://org.crm4.dynamics.com --OutputFolder ../Website/generated +``` + +### Format Code +```bash +dotnet format +dotnet format --verify-no-changes # Check only, no modifications +``` + +### Clean Build +```bash +dotnet clean +dotnet build +``` + +## Authentication Setup (Local Development) + +The Generator uses `DefaultAzureCredential` which requires Azure CLI authentication locally: + +```bash +# Login with Azure CLI +az login + +# Select correct subscription if needed +az account list +az account set --subscription "Subscription Name or ID" + +# Verify access +az account show +``` + +**Required Permissions**: The authenticated user must have at least `Basic` read access to the Dataverse environment. + +## DTO Type System + +### Entity DTO (`DTO/Entity.cs`) + +```csharp +public class Entity +{ + public string DisplayName { get; set; } + public string SchemaName { get; set; } + public string? Group { get; set; } + public string Ownership { get; set; } + public bool IsActivity { get; set; } + public bool IsCustom { get; set; } + public List Attributes { get; set; } + public List Relationships { get; set; } + public List SecurityRoles { get; set; } + public List Keys { get; set; } +} +``` + +### Polymorphic Attribute Types (`DTO/Attributes/`) + +**Base**: `Attribute` (abstract) + +**Concrete Types**: +- `ChoiceAttribute` - Picklist with options +- `LookupAttribute` - Foreign key (Targets list) +- `StringAttribute` - Text (MaxLength, Format) +- `DateTimeAttribute` - Date/Time (Behavior, Format) +- `IntegerAttribute` - Whole number +- `DecimalAttribute` - Decimal/Money +- `BooleanAttribute` - Two-option (TrueLabel, FalseLabel) +- `StatusAttribute` - Status with linked State +- `FileAttribute` - File/Image (MaxSize) +- `GenericAttribute` - Fallback for unsupported types + +**Pattern**: Each attribute type has properties specific to its Dataverse counterpart. + +### Relationship DTO (`DTO/Relationship.cs`) + +```csharp +public class Relationship +{ + public string Name { get; set; } + public string RelationshipSchema { get; set; } + public string TableSchema { get; set; } + public bool IsManyToMany { get; set; } + public CascadeConfiguration CascadeConfiguration { get; set; } +} +``` + +## Common Tasks + +### Add Support for New Attribute Type + +1. **Create DTO**: Add new class in `DTO/Attributes/` inheriting from `Attribute` + ```csharp + public class NewAttribute : Attribute + { + public string SpecialProperty { get; set; } + } + ``` + +2. **Update Mapper**: Add case in `DataverseService.cs:MapAttribute()` + ```csharp + case NewAttributeMetadata newAttr: + return new NewAttribute + { + // Map properties + SpecialProperty = newAttr.SpecialProperty + }; + ``` + +3. **Update Website**: Add corresponding TypeScript type in `Website/lib/Types.ts` + +### Add New Analyzer + +1. **Create Service**: Add new analyzer in `Services/` directory +2. **Extract Data**: Query relevant Dataverse records +3. **Add to Output**: Update `WebsiteBuilder.cs` to include new data +4. **Update Website**: Create UI to display the new data + +### Modify Query Logic + +1. **Find Query**: Locate in `Queries/` or in `DataverseService.cs` methods +2. **Update FetchXML/QueryExpression**: Modify query structure +3. **Test Locally**: Run generator and verify output +4. **Validate Output**: Check `Website/generated/Data.ts` is valid TypeScript + +### Debug Connection Issues + +**Problem**: Cannot connect to Dataverse + +**Solutions**: +1. Verify `DataverseUrl` is correct in config +2. Check Azure CLI is logged in: `az account show` +3. Verify user has Dataverse access +4. Check network connectivity +5. Try different authentication methods (environment variables vs. CLI) + +**Enable Detailed Logging**: +```json +{ + "Verbosity": "Trace" +} +``` + +### Handle Missing Entities + +**Problem**: Expected entities not in output + +**Check**: +1. Verify entity exists in specified solutions +2. Check `DataverseSolutionNames` includes correct solutions +3. Review console warnings for excluded entities +4. Verify solution publisher prefix matches + +## Output Format + +The generated `Data.ts` file structure: + +```typescript +// Entity groups mapped by category +export const EntityGroups = new Map([ + ["Core", [/* entities */]], + ["Custom", [/* entities */]], + // ... +]); + +// Validation warnings for solutions +export const Warnings: SolutionWarningsList[] = [ + { + SolutionName: "MySolution", + Warnings: [/* warning objects */] + } +]; + +// Solution component breakdown +export const SolutionComponents: SolutionComponentList[] = [ + { + SolutionName: "MySolution", + Components: [/* component objects */] + } +]; + +// Generation timestamp +export const timestamp = "2025-11-10T15:30:00Z"; + +// Optional base64 encoded logo +export const logo = "data:image/png;base64,..."; +``` + +## Integration with Website + +**Critical**: Website depends on this output file. Always regenerate after Generator changes. + +**Workflow**: +1. Modify Generator code or configuration +2. Run `dotnet run` to regenerate `Data.ts` +3. Restart Website dev server to pick up changes +4. Test Website functionality + +**Type Safety**: Keep C# DTOs in sync with TypeScript types in `Website/lib/Types.ts` + +## Performance Considerations + +**Large Environments**: +- Fetching 500+ entities can take 2-5 minutes +- Relationship queries are expensive +- Plugin/Flow analysis adds overhead + +**Optimization**: +- Filter by specific solutions only +- Reduce verbosity for production runs +- Consider caching metadata for incremental updates + +## Testing Strategy + +### Manual Testing +1. Run against test environment +2. Check console output for errors/warnings +3. Verify generated TypeScript file: + - Valid syntax (no compile errors) + - Expected entities present + - Attributes have correct types + - Relationships are bidirectional + +### Validation Checklist +- [ ] Generator runs without exceptions +- [ ] Output file created at correct location +- [ ] No TypeScript compilation errors in output +- [ ] Entity count matches expectation +- [ ] Custom entities identified correctly +- [ ] Relationships include both directions +- [ ] Security roles populated +- [ ] Warning list shows known issues + +## Troubleshooting + +### "Unable to authenticate" Error +- Run `az login` and authenticate +- Verify subscription has Dataverse access +- Check if using correct Azure tenant + +### "Invalid solution name" Warning +- Verify solution unique names (not display names) +- Check solutions exist in target environment +- Ensure solutions have same publisher prefix + +### Large Output File +- Filter by more specific solutions +- Consider splitting into multiple generation runs +- Website may have performance issues with 1000+ entities + +### Missing Attributes/Relationships +- Check if filtered by solution components +- Verify attribute is not system-only +- Ensure relationship is managed or in solutions + +## Important Reminders + +- Always run Generator before starting Website development session +- Keep `appsettings.local.json` out of source control (already in .gitignore) +- DefaultAzureCredential requires Azure CLI for local dev +- Output folder must exist before running (Generator will not create it) +- TypeScript output must be valid - Generator will not validate syntax +- Generator runs synchronously - expect 2-5 minute execution time for large environments diff --git a/Infrastructure/.claude/settings.json b/Infrastructure/.claude/settings.json new file mode 100644 index 0000000..00612ba --- /dev/null +++ b/Infrastructure/.claude/settings.json @@ -0,0 +1,24 @@ +{ + "permissions": { + "allow": [ + "Bash(az login)", + "Bash(az account*)", + "Bash(az group*)", + "Bash(az deployment group validate*)", + "Bash(az deployment group what-if*)", + "Bash(az bicep build*)", + "Bash(az webapp*)", + "Bash(git status)", + "Bash(git diff*)" + ], + "deny": [ + "Bash(az deployment group create*)", + "Bash(az deployment group delete*)", + "Bash(az group delete*)", + "Bash(rm -rf*)" + ], + "ask": [ + "Bash(az deployment*)" + ] + } +} diff --git a/Infrastructure/CLAUDE.md b/Infrastructure/CLAUDE.md new file mode 100644 index 0000000..d224806 --- /dev/null +++ b/Infrastructure/CLAUDE.md @@ -0,0 +1,440 @@ +# CLAUDE.md - Infrastructure Project + +This file provides guidance to Claude Code when working with the Infrastructure project. + +## Project Overview + +The Infrastructure project contains Azure Bicep templates for deploying the Data Model Viewer website to Azure App Service. It provisions all necessary cloud resources and configurations. + +## Technology Stack + +- **Azure Bicep** - Infrastructure as Code (IaC) +- **Azure App Service** - Web hosting platform +- **Azure App Service Plan** - Compute resources +- **System Assigned Managed Identity** - Authentication for Azure services + +## Architecture + +### Infrastructure Components + +``` +Infrastructure/ +└── main.bicep # Main Bicep template +``` + +### Deployed Azure Resources + +1. **App Service Plan** (`asp-{solutionId}`) + - SKU: F1 (Free tier) by default + - Platform: Linux + - Purpose: Compute capacity for web app + +2. **Web App** (`wa-{solutionId}`) + - Runtime: Node.js 24 LTS + - HTTPS Only: Enabled + - System Assigned Managed Identity: Enabled + - Environment Variables: Configured for Website project + +3. **Managed Identity** + - Type: System Assigned + - Purpose: Authenticate to Dataverse and Azure DevOps without credentials + - Permissions: Must be configured manually after deployment + +## Bicep Template Structure + +### Parameters + +```bicep +@description('Unique identifier for naming resources') +param solutionId string + +@description('Password for website login') +@secure() +param websitePassword string + +@description('Secret key for session encryption') +@secure() +param sessionSecret string + +@description('Azure DevOps organization URL') +param adoOrganizationUrl string = '' + +@description('Azure DevOps project name') +param adoProjectName string = '' + +@description('Azure DevOps repository name for diagram storage') +param adoRepositoryName string = '' +``` + +### Resource Naming Convention + +- App Service Plan: `asp-{solutionId}` +- Web App: `wa-{solutionId}` +- Full URL: `https://wa-{solutionId}.azurewebsites.net/` + +**Important**: `solutionId` must be globally unique across all Azure customers. + +### Environment Variables + +The template configures these environment variables for the Website: + +| Variable | Source | Purpose | +|----------|--------|---------| +| `WebsitePassword` | Parameter | User login password | +| `WebsiteSessionSecret` | Parameter | JWT encryption key | +| `WEBSITE_NODE_DEFAULT_VERSION` | Template | Node.js version hint | +| `ADO_ORGANIZATION_URL` | Parameter | Azure DevOps org URL | +| `ADO_PROJECT_NAME` | Parameter | ADO project name | +| `ADO_REPOSITORY_NAME` | Parameter | Diagram storage repo | + +## Deployment + +### Prerequisites + +- Azure subscription with appropriate permissions +- Azure CLI or Azure PowerShell +- Resource group (can be created by template) +- Globally unique `solutionId` value + +### Manual Deployment + +```bash +# Login to Azure +az login + +# Create resource group (if needed) +az group create --name rg-datamodelviewer --location westeurope + +# Deploy Bicep template +az deployment group create \ + --resource-group rg-datamodelviewer \ + --template-file main.bicep \ + --parameters solutionId=myorg-dmv \ + websitePassword='SecurePassword123!' \ + sessionSecret='<32-byte-random-string>' \ + adoOrganizationUrl='https://dev.azure.com/myorg' \ + adoProjectName='MyProject' \ + adoRepositoryName='DataModelViewer' +``` + +### Pipeline Deployment + +The Azure Pipeline (`azure-pipelines-deploy-jobs.yml`) deploys using: + +```yaml +- task: AzureResourceManagerTemplateDeployment@3 + inputs: + deploymentScope: 'Resource Group' + azureResourceManagerConnection: $(AzureServiceConnectionName) + subscriptionId: $(AzureSubscriptionId) + action: 'Create Or Update Resource Group' + resourceGroupName: $(AzureResourceGroupName) + location: $(AzureLocation) + templateLocation: 'Linked artifact' + csmFile: 'Infrastructure/main.bicep' + overrideParameters: | + -solutionId $(WebsiteName) + -websitePassword $(WebsitePassword) + -sessionSecret $(WebsiteSessionSecret) + -adoOrganizationUrl $(ADO_ORGANIZATION_URL) + -adoProjectName $(ADO_PROJECT_NAME) + -adoRepositoryName $(AdoRepositoryName) +``` + +## Post-Deployment Configuration + +### 1. Configure Startup Command + +**Important**: Azure App Service requires explicit startup command for Next.js standalone mode. + +1. Navigate to Azure Portal → App Service → Configuration → General settings +2. Set **Startup Command** to: `node server.js` +3. Save and restart app service + +Without this, the app will not start correctly. + +### 2. Configure Managed Identity Permissions + +The deployed Managed Identity needs two sets of permissions: + +#### Dataverse Access (for data generation) +1. Navigate to Power Platform Admin Center +2. Select your Dataverse environment +3. Go to Settings → Users + permissions → Users +4. Add the Managed Identity (`wa-{solutionId}`) as a user +5. Assign **Environment Maker** security role (or custom role with read access) + +#### Azure DevOps Access (for diagram storage) +1. Navigate to Azure DevOps → Organization Settings → Users +2. Click "Add users" +3. Search for `wa-{solutionId}` (the managed identity) +4. Select the service principal from results +5. Grant **Basic** access level +6. Add to appropriate project +7. **Uncheck** "Send email invites" +8. Navigate to Project Settings → Repositories → DataModelViewer repo → Security +9. Add Managed Identity with **Contributor** permissions (or least-privilege custom role with read/write) + +**Note**: Contributor grants more permissions than strictly necessary. Consider creating a custom role with only: +- Read repository +- Create branch +- Commit changes +- Create pull request (if using PR workflow) + +### 3. Verify Deployment + +```bash +# Get web app URL +az webapp show --name wa-{solutionId} --resource-group rg-datamodelviewer --query "defaultHostName" -o tsv + +# Check deployment logs +az webapp log tail --name wa-{solutionId} --resource-group rg-datamodelviewer + +# Verify managed identity +az webapp identity show --name wa-{solutionId} --resource-group rg-datamodelviewer +``` + +## Common Modifications + +### Change App Service SKU + +**Default**: F1 (Free tier) - limited compute, no custom domains, no scaling + +**Upgrade for production**: + +```bicep +resource appServicePlan 'Microsoft.Web/serverfarms@2021-02-01' = { + name: 'asp-${solutionId}' + location: location + sku: { + name: 'B1' // Basic tier + tier: 'Basic' + } + properties: { + reserved: true + } +} +``` + +**SKU Options**: +- `F1` - Free (shared compute, 60 min/day) +- `B1` - Basic (dedicated, 1 core, 1.75GB RAM) +- `S1` - Standard (auto-scale, custom domains, SSL) +- `P1V2` - Premium (better performance, more features) + +### Add Custom Domain + +1. Update Bicep to use S1 or higher SKU +2. Add custom hostname binding: + +```bicep +resource hostname 'Microsoft.Web/sites/hostNameBindings@2021-02-01' = { + parent: webApp + name: 'dmv.mycompany.com' + properties: { + siteName: webApp.name + hostNameType: 'Verified' + } +} +``` + +3. Configure DNS CNAME record +4. Add SSL certificate + +### Add Application Insights + +```bicep +resource appInsights 'Microsoft.Insights/components@2020-02-02' = { + name: 'ai-${solutionId}' + location: location + kind: 'web' + properties: { + Application_Type: 'Node.JS' + } +} + +// Add to web app properties +{ + name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' + value: appInsights.properties.ConnectionString +} +``` + +### Add Key Vault for Secrets + +```bicep +resource keyVault 'Microsoft.KeyVault/vaults@2021-06-01-preview' = { + name: 'kv-${solutionId}' + location: location + properties: { + tenantId: subscription().tenantId + sku: { family: 'A', name: 'standard' } + accessPolicies: [ + { + tenantId: subscription().tenantId + objectId: webApp.identity.principalId + permissions: { + secrets: ['get', 'list'] + } + } + ] + } +} + +// Reference in web app +{ + name: 'WebsitePassword' + value: '@Microsoft.KeyVault(VaultName=kv-${solutionId};SecretName=WebsitePassword)' +} +``` + +## Pipeline Integration + +### Required Pipeline Variables + +Set these in Azure DevOps Variable Group (typically named `DataModel`): + +| Variable | Description | Secret | +|----------|-------------|--------| +| `AzureServiceConnectionName` | ARM service connection name | No | +| `AzureLocation` | Azure region (e.g., `westeurope`) | No | +| `AzureResourceGroupName` | Resource group name | No | +| `WebsiteName` | Unique site identifier (solutionId) | No | +| `WebsitePassword` | Login password | Yes | +| `WebsiteSessionSecret` | JWT encryption key (32 chars) | Yes | +| `ADO_ORGANIZATION_URL` | ADO org URL | No | +| `ADO_PROJECT_NAME` | ADO project name | No | +| `AdoRepositoryName` | Diagram storage repo | No | + +### Pipeline Flow + +1. **Build Stage**: Website built into `.next/standalone` folder +2. **Infrastructure Stage**: Bicep template deploys/updates resources +3. **Deploy Stage**: Website files uploaded to App Service +4. **Post-Deploy**: Manual startup command configuration (first time only) + +## Troubleshooting + +### Deployment Fails: "Name already exists" + +**Problem**: `solutionId` is not globally unique + +**Solution**: +- Choose a different `solutionId` value +- Check existing App Services: `az webapp list --query "[].name"` +- Use format like: `{company}-{environment}-dmv` (e.g., `contoso-prod-dmv`) + +### Web App Not Starting + +**Problem**: Startup command not configured or incorrect + +**Solution**: +1. Check Configuration → General settings → Startup Command +2. Should be: `node server.js` +3. Check logs: `az webapp log tail --name wa-{solutionId} --resource-group {rg}` + +### Managed Identity Not Working + +**Problem**: MI doesn't have proper permissions + +**Solution**: +1. Verify MI exists: `az webapp identity show` +2. Check Dataverse role assignments in Power Platform +3. Check ADO permissions in Organization Settings → Users +4. Wait 5-10 minutes for permission propagation + +### Environment Variables Not Set + +**Problem**: Website can't read configuration + +**Solution**: +1. Check App Service → Configuration → Application settings +2. Verify all required variables present +3. Restart web app after adding variables +4. Check for typos in variable names + +## Infrastructure Testing + +### Validate Bicep Template + +```bash +# Check syntax +az bicep build --file main.bicep + +# Validate against Azure +az deployment group validate \ + --resource-group rg-datamodelviewer \ + --template-file main.bicep \ + --parameters solutionId=test-dmv websitePassword=Test123! sessionSecret=test123 +``` + +### What-If Deployment + +```bash +# See what would change without deploying +az deployment group what-if \ + --resource-group rg-datamodelviewer \ + --template-file main.bicep \ + --parameters solutionId=myorg-dmv websitePassword=Pass123! sessionSecret=secret123 +``` + +### Cost Estimation + +- **F1 Free Tier**: $0/month (1 app per subscription) +- **B1 Basic**: ~$13/month +- **S1 Standard**: ~$70/month +- **P1V2 Premium**: ~$150/month + +Plus data transfer costs (minimal for typical usage). + +## Security Considerations + +### Secure Parameters + +Always mark sensitive parameters with `@secure()` decorator: + +```bicep +@secure() +param websitePassword string +``` + +This prevents values from appearing in logs or deployment history. + +### HTTPS Only + +Template enforces HTTPS: + +```bicep +properties: { + httpsOnly: true +} +``` + +Never disable this in production. + +### Managed Identity vs. Connection Strings + +**Always prefer** Managed Identity over connection strings/PAT tokens for: +- Azure service authentication +- Azure DevOps API access +- Dataverse connectivity + +Managed Identity eliminates secret management and rotation. + +### Network Security + +For production, consider: +- **Private Endpoints**: Connect App Service to VNet +- **Access Restrictions**: Limit inbound traffic by IP +- **VNet Integration**: Access private Dataverse instances + +## Important Reminders + +- `solutionId` parameter must be globally unique +- Always set startup command to `node server.js` after first deployment +- Managed Identity requires manual permission configuration +- Free tier (F1) has limitations - upgrade for production +- Template deploys to same region as resource group +- Environment variables are configured in template - no manual portal configuration needed +- Website deployment happens separately from infrastructure deployment +- Test with `what-if` command before production changes diff --git a/Website/.claude/hooks/stop-audio.py b/Website/.claude/hooks/stop-audio.py new file mode 100644 index 0000000..538905c --- /dev/null +++ b/Website/.claude/hooks/stop-audio.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +""" +play_system_sound.py + +Cross-platform "play a short system sound" helper. +- Windows: uses winsound + known system .wav +- macOS: uses afplay on a known .aiff; falls back to `say` +- Linux: tries paplay/aplay/ffplay and common sound theme files; falls back to terminal bell + +Usage: + python play_system_sound.py # play default for this OS + python play_system_sound.py /path/to/file.wav # play a specific file if you prefer + +Exit codes: + 0 = played successfully (or reasonable fallback like bell) + 1 = hard failure +""" + +import os +import sys +import platform +import subprocess +import shutil +from pathlib import Path + +def which(cmd: str) -> str | None: + return shutil.which(cmd) + +def try_run(cmd: list[str]) -> bool: + try: + # Use DEVNULL to keep CLI quiet + with open(os.devnull, "wb") as devnull: + subprocess.check_call(cmd, stdout=devnull, stderr=devnull) + return True + except Exception: + return False + +def pick_default_sound(system: str) -> Path | None: + p = Path + + if system == "Windows": + candidates = [ + p(r"C:\Windows\Media\Windows Notify.wav"), + p(r"C:\Windows\Media\Windows Balloon.wav"), + p(r"C:\Windows\Media\Alarm01.wav"), + ] + for c in candidates: + if c.exists(): + return c + return None + + if system == "Darwin": # macOS + candidates = [ + p("/System/Library/Sounds/Glass.aiff"), + p("/System/Library/Sounds/Submarine.aiff"), + p("/System/Library/Sounds/Pop.aiff"), + ] + for c in candidates: + if c.exists(): + return c + return None + + # Linux & everything else POSIX-like + linux_candidates = [ + # Freedesktop/Ubuntu themes (paplay likes .oga/.ogg) + p("/usr/share/sounds/freedesktop/stereo/complete.oga"), + p("/usr/share/sounds/freedesktop/stereo/message.oga"), + p("/usr/share/sounds/ubuntu/stereo/dialog-information.ogg"), + # ALSA samples for aplay (wav) + p("/usr/share/sounds/alsa/Front_Center.wav"), + # speech-dispatcher test (wav) + p("/usr/share/sounds/speech-dispatcher/test.wav"), + ] + for c in linux_candidates: + if c.exists(): + return c + return None + +def play_file(path: Path) -> bool: + """Try a few players appropriate to the file extension/OS.""" + system = platform.system() + ext = path.suffix.lower() + + if system == "Windows": + try: + import winsound + winsound.PlaySound(str(path), winsound.SND_FILENAME) + return True + except Exception: + pass + # Fallback to start (may open a UI player) + return try_run(["powershell", "-NoProfile", "-Command", f'Start-Process -FilePath "{path}"']) + + if system == "Darwin": # macOS + if which("afplay"): + return try_run(["afplay", str(path)]) + # fallback: QuickTime via 'open' (will show UI), or 'say' if no file + if which("open"): + return try_run(["open", str(path)]) + return False + + # Linux / other POSIX + # Prefer paplay (PulseAudio/PipeWire), then aplay (ALSA), then ffplay + if which("paplay"): + return try_run(["paplay", str(path)]) + if which("aplay"): + return try_run(["aplay", str(path)]) + if which("ffplay"): + # -autoexit to quit when done, -nodisp to avoid window + return try_run(["ffplay", "-autoexit", "-nodisp", str(path)]) + if which("play"): # SoX + return try_run(["play", "-q", str(path)]) + return False + +def fallback_tone() -> bool: + """Last-ditch: try to emit a short beep on the current platform.""" + system = platform.system() + try: + if system == "Windows": + import winsound + winsound.MessageBeep() + return True + elif system == "Darwin": + # Speak a short chirp if no audio players are available + if which("say"): + return try_run(["say", "-v", "Boing", "boop"]) + else: + # ANSI bell (works in some terminals) + sys.stdout.write("\a") + sys.stdout.flush() + return True + except Exception: + pass + return False + +def main(): + # If a path is provided, try that first. + if len(sys.argv) > 1: + user_path = Path(sys.argv[1]).expanduser() + if user_path.exists(): + if play_file(user_path): + return 0 + if fallback_tone(): + return 0 + return 1 + else: + print(f"File not found: {user_path}", file=sys.stderr) + return 1 + + system = platform.system() + sound = pick_default_sound(system) + + # If we found a system sound, try to play it + if sound and play_file(sound): + return 0 + + # If we couldn't play a file, try a platform-specific fallback + if system == "Darwin" and which("say"): + # make a tiny chirp without needing a file + if try_run(["say", "ding"]): + return 0 + + if fallback_tone(): + return 0 + + print("Unable to play any sound on this system.", file=sys.stderr) + return 1 + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Website/.claude/settings.json b/Website/.claude/settings.json new file mode 100644 index 0000000..3ea3dcc --- /dev/null +++ b/Website/.claude/settings.json @@ -0,0 +1,32 @@ +{ + "permissions": { + "allow": [ + "Bash(npm run lint:*)", + "Bash(npm install:*)", + "Bash(npm run build:*)", + "Bash(npx tsc:*)", + "Bash(npm test:*)", + "Bash(git status)", + "Bash(git diff*)", + "Bash(python:*)" + ], + "deny": [ + "Bash(rm -rf*)", + "Bash(git push --force*)" + ], + "ask": [] + }, + "hooks": { + "Stop": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "python .claude/hooks/stop-audio.py" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/Website/.claude/settings.local.json b/Website/.claude/settings.local.json deleted file mode 100644 index 47115b5..0000000 --- a/Website/.claude/settings.local.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(npm run lint:*)", - "Bash(npm install:*)", - "Bash(npm run build:*)", - "Bash(npx tsc:*)" - ], - "deny": [], - "ask": [] - } -} diff --git a/Website/CLAUDE.md b/Website/CLAUDE.md index 1414484..ce11501 100644 --- a/Website/CLAUDE.md +++ b/Website/CLAUDE.md @@ -8,20 +8,6 @@ Data Model Viewer is a Next.js 15 application for visualizing Dataverse data mod ## Development Commands -### Setup -```bash -npm i -``` - -Required environment variables in `.env.local`: -- `WebsitePassword` - Basic auth password -- `WebsiteSessionSecret` - Session encryption secret -- `ADO_PROJECT_NAME` - Azure DevOps project name -- `ADO_ORGANIZATION_URL` - Azure DevOps organization URL -- `ADO_REPOSITORY_NAME` - Repository name for diagram storage -- `AZURE_CLI_AUTHENTICATION_ENABLED` - Set to `true` for local dev -- `ADO_PAT` - Personal Access Token for Azure DevOps (generate at DevOps settings) - ### Development ```bash npm run dev # Start development server @@ -33,6 +19,16 @@ npm run prepipeline # Copy stub files (runs before pipeline build) Note: The build process includes a `postbuild` script that creates required standalone directories for Next.js 15 deployment compatibility. +### MUI MCP Server Setup + +**REQUIRED** for working with MUI components: + +```bash +claude mcp add mui-mcp -- npx -y @mui/mcp@latest +``` + +This provides access to MUI v7 documentation and component APIs through the MCP server. + ## Architecture ### Core Technology Stack @@ -53,7 +49,7 @@ Application uses nested context providers in this order: ### Diagram Architecture -**Key Pattern**: Diagram uses JointJS for rendering with a Web Worker for routing calculations. +**Key Pattern**: Diagram uses JointJS for rendering with a Web Worker for libavoid routing calculations. #### DiagramViewContext (`contexts/DiagramViewContext.tsx`) - Central state management for the diagram canvas @@ -198,6 +194,50 @@ Authentication uses either: File operations always specify branch (default: 'main') and commit messages. +## Styling Guidelines + +**CRITICAL**: This project uses a specific MUI + Tailwind integration pattern. + +### MUI Component Usage Rules + +1. **Use MUI for interactive components**: Button, TextField, Dialog, Select, etc. +2. **Use Tailwind for ALL visual styling**: No `sx` prop except for theme access +3. **Query MUI MCP server** before implementing unfamiliar components +4. **See the MUI Guidelines Skill**: `.claude/skills/mui-guidelines/SKILL.md` + +### Examples + +**WRONG** (using `sx` prop): +```tsx + +``` + +**CORRECT** (Tailwind classes): +```tsx + +``` + +**Exception** (theme access only): +```tsx + + Responsive with theme breakpoints + +``` + +### Workflow for Styling Tasks + +1. Identify need (button, form, modal, etc.) +2. Choose appropriate MUI component +3. Query MUI MCP server if unfamiliar: `mcp__mui-mcp__useMuiDocs` +4. Implement with MUI component + Tailwind classes +5. Verify no `sx` props (except theme access) + +**Reference**: See `.claude/skills/mui-guidelines/SKILL.md` for comprehensive guidelines. + ## Important Notes - **Path aliases**: `@/*` maps to root directory (see `tsconfig.json`) @@ -206,3 +246,4 @@ File operations always specify branch (default: 'main') and commit messages. - **Selection transformations**: Must be calculated relative to paper transformation matrix (see `Selection.ts:applyTransformation`) - **Entity deduplication**: Always check `diagramContext.isEntityInDiagram()` before adding - **JointJS integration**: Custom elements defined with `dia.Element.define()`, custom views with `dia.ElementView.extend()` +- **MUI Styling**: NEVER use `sx` prop for styling (use Tailwind), see MUI Guidelines Skill diff --git a/Website/middleware.ts b/Website/middleware.ts index 4ecc0c6..0bc6b8a 100644 --- a/Website/middleware.ts +++ b/Website/middleware.ts @@ -10,7 +10,21 @@ export async function middleware(request: NextRequest) { return NextResponse.next() } - // Redirect to login page if not authenticated + // Allow access to public API endpoints without authentication + const publicApiEndpoints = ['/api/auth/login', '/api/version']; + if (publicApiEndpoints.includes(request.nextUrl.pathname)) { + return NextResponse.next() + } + + // For API routes, return 401 Unauthorized + if (request.nextUrl.pathname.startsWith('/api')) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ) + } + + // For page routes, redirect to login page return NextResponse.redirect(new URL('/login', request.url)) } @@ -18,13 +32,12 @@ export const config = { matcher: [ /* * Match all request paths except for the ones starting with: - * - api (API routes) * - _next/static (static files) * - _next/image (image optimization files) * - favicon.ico (favicon file) * - login (login page) * - public assets (images, SVGs, etc.) */ - '/((?!api|_next/static|_next/image|favicon.ico|login|.*\\.).*)', + '/((?!_next/static|_next/image|favicon.ico|login|.*\\.).*)', ] } \ No newline at end of file