- 0 minutes to read

Multi-User Collaboration – Real-Time Editing & Team Workflows

New .7.x

Multi-User Collaboration in Nodinite Mapify enables distributed teams to work concurrently on integration landscapes with confidence. Multiple users can view, edit, and annotate the same Repository Model entities simultaneously, with real-time updates ensuring everyone sees the latest changes instantly. Whether your team is conducting root-cause analysis, planning new integrations, or documenting existing architectures, Mapify's collaboration features eliminate version conflicts and "stepped-on-each-other's-toes" scenarios.

With built-in presence indicators, conflict resolution strategies, change tracking, and collaborative annotations, Mapify transforms integration management from a solo activity into a team sport.

Multi-User Collaboration Example
Example of multiple users editing the same integration landscape with real-time presence indicators and change notifications.

Why Multi-User Collaboration?

Multi-user collaboration delivers significant productivity and quality benefits for distributed teams:

  • 70% faster incident resolution – Multiple team members can investigate and document issues concurrently
  • Zero data loss – Automatic conflict resolution prevents overwrites and preserves all changes
  • Global team coordination – Real-time updates keep distributed teams synchronized across timezones
  • Complete audit trails – Track who changed what, when, and why for compliance and governance
  • Contextual discussions – Comment directly on entities to resolve questions without meetings
  • Onboarding acceleration – New team members learn by observing live edits and commenting with questions

Note: Multi-User Collaboration requires Mapify Premium licensing and Nodinite Web API 7.x. Contact Nodinite Sales to enable this feature.


Collaboration Principles

Mapify's multi-user collaboration is built on four core principles:

1. Transparency – Know Who's Working Where

User presence indicators show who's currently viewing or editing each entity in real-time. See active users, their current focus area, and recent changes at a glance.

Visual indicators:

  • Avatar badges on nodes currently being viewed by other users
  • Highlighted borders for entities actively being edited
  • Activity panel showing recent edits by team members
  • Cursor tracking for real-time collaborative editing (optional)

2. Real-Time Synchronization – Always See Latest Data

Changes made by any user are instantly propagated to all connected clients. No page refreshes, no stale data, no confusion.

Update mechanisms:

  • WebSocket/SignalR real-time push notifications
  • Automatic entity refresh when remote changes detected
  • Visual toast notifications for relevant updates ("John updated Integration #42")
  • Conflict warnings before you overwrite someone's changes

3. Conflict Prevention – Protect Everyone's Work

Optimistic locking allows concurrent editing while detecting and resolving conflicts gracefully. You'll never lose work due to simultaneous edits.

Conflict resolution strategies:

  • Last-write-wins with notification and undo option
  • Field-level merge for non-overlapping changes
  • Manual merge dialog for complex conflicts
  • Auto-save drafts to prevent data loss

4. Complete Audit Trail – Know What Changed and Why

Every change is tracked with full metadata: who, what, when, and optionally why (via commit messages or comments).

Audit capabilities:

  • Change history log per entity (last 90 days or configurable)
  • Filter entities by recent modifications ("Show all changes in last 7 days")
  • Compliance reports for regulatory reviews (SOX, GDPR, HIPAA)
  • Rollback capability to restore previous versions

Concurrent Editing Scenarios

Scenario 1: Non-Conflicting Edits (Ideal Case)

Situation:

  • User A (Alice) edits Description field of Integration #123
  • User B (Bob) edits Owner field of Integration #123 at the same time

Behavior:

  • Both users save their changes successfully
  • Mapify merges field-level changes automatically
  • Both users see updated entity with all changes applied
  • Notification: "Integration #123 updated by Alice and Bob (merged successfully)"

Technical implementation: Field-level optimistic locking with automatic merge.


Scenario 2: Conflicting Edits (Same Field)

Situation:

  • User A (Alice) changes Status from "Active" to "Deprecated" for Integration #123
  • User B (Bob) changes Status from "Active" to "In Review" for Integration #123 at the same time
  • Both users click Save within 2 seconds of each other

Behavior:

  1. Alice saves first → Status changes to "Deprecated" (timestamp: 10:00:00)
  2. Bob saves second → Conflict detected (timestamp: 10:00:02)
  3. Conflict dialog appears for Bob:
┌─────────────────────────────────────────────────────────────┐
│   Conflict Detected                │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Integration #123 was modified by Alice while you          │
│  were editing. The Status field has conflicting values:    │
│                                                             │
│  Your change:       "In Review"                            │
│  Alice's change:    "Deprecated"                           │
│                                                             │
│  Choose an option:                                         │
│                                                             │
│  ○ Keep Alice's change ("Deprecated")                      │
│  ● Overwrite with my change ("In Review")                  │
│  ○ Cancel and review manually                              │
│                                                             │
│  [View Full Change History]  [Cancel]  [Save Choice]       │
│                                                             │
└─────────────────────────────────────────────────────────────┘
  1. Bob selects "Overwrite with my change" → Status becomes "In Review"
  2. Alice receives notification: "Bob changed Status from Deprecated to In Review (overwrite)"
  3. Alice can undo → Click notification to view change and optionally revert

Technical implementation: Last-write-wins with full transparency and undo capability.


Scenario 3: High-Frequency Edits (Rapid Fire)

Situation:

  • 5 users edit different fields of Integration #123 within 10 seconds
  • Changes include: Owner, Description, Tags, Custom Metadata, Status

Behavior:

  • All changes applied in order of arrival to server
  • Each user sees incremental updates in real-time
  • WebSocket pushes change notifications to all connected clients
  • Activity panel shows: "5 users edited this integration in the last 10 seconds"
  • Entity displays "Recently edited" badge for 30 seconds

Technical implementation: Event sourcing with ordered change stream and real-time broadcast.


Scenario 4: Offline Editing & Sync-on-Reconnect

Situation:

  • User A (Alice) loses internet connection while editing Integration #123
  • Alice continues editing offline (browser caches changes)
  • After 5 minutes, Alice's connection is restored

Behavior:

  1. During offline period:

    • Mapify detects disconnection and shows Offline Mode banner
    • Alice can continue editing; changes saved to browser localStorage
    • Warning: "You are offline. Changes will sync when connection is restored."
  2. Upon reconnection:

    • Mapify automatically syncs local changes to server
    • If no conflicts: Changes applied successfully, notification displayed
    • If conflicts: Conflict resolution dialog appears (same as Scenario 2)
    • "Reconnected – 3 changes synced successfully"
  3. If remote changes occurred while offline:

    • Mapify fetches latest server state
    • Compares with local edits
    • Shows diff dialog: "These entities were modified while you were offline"
    • User reviews and chooses: merge, overwrite, or discard local changes

Technical implementation: Offline-first architecture with localStorage queue and conflict detection on sync.


Architecture & Real-Time Updates

System Architecture

Mapify's multi-user collaboration uses a client-server-database architecture with real-time bidirectional communication:

graph TB subgraph "Client Layer" C1[Browser Client 1
Alice] C2[Browser Client 2
Bob] C3[Browser Client 3
Charlie] end subgraph "Server Layer" API[Nodinite Web API
REST + SignalR] SignalR[SignalR Hub
Real-Time Events] Cache[Redis Cache
Presence & Session Data] end subgraph "Data Layer" DB[(SQL Server
Repository Model)] AuditDB[(Audit Database
Change History)] Queue[Message Queue
Change Events] end C1 <-->|WebSocket| SignalR C2 <-->|WebSocket| SignalR C3 <-->|WebSocket| SignalR C1 <-->|HTTPS REST| API C2 <-->|HTTPS REST| API C3 <-->|HTTPS REST| API SignalR --> Cache API --> Cache API <-->|Read/Write| DB API -->|Log Changes| AuditDB API -->|Publish Events| Queue Queue -->|Subscribe| SignalR SignalR -.->|Broadcast| C1 SignalR -.->|Broadcast| C2 SignalR -.->|Broadcast| C3 style C1 fill:#e3f2fd style C2 fill:#e3f2fd style C3 fill:#e3f2fd style SignalR fill:#fff3e0 style Cache fill:#f3e5f5 style DB fill:#e8f5e9 style AuditDB fill:#e8f5e9

Architecture components:

Component Technology Purpose Scalability
Browser Clients JavaScript, WebSocket, IndexedDB User interface, offline editing, local caching Unlimited clients
Web API ASP.NET Core, REST, SignalR Business logic, authentication, data validation Horizontal scaling (load balancer + multiple instances)
SignalR Hub ASP.NET Core SignalR (WebSocket/Server-Sent Events) Real-time bidirectional communication Scaled-out with Redis backplane
Redis Cache Redis 6.x Presence data, session state, pub/sub messaging Master-replica for high availability
SQL Server SQL Server 2019+ Repository Model data (Integrations, Resources, etc.) AlwaysOn Availability Groups
Audit Database SQL Server 2019+ Change history, audit logs (partitioned by date) Read replicas for reporting
Message Queue Azure Service Bus or RabbitMQ Asynchronous event processing, guaranteed delivery Partitioned queues for scale

Real-Time Update Mechanism

Mapify uses SignalR over WebSocket for real-time updates. SignalR provides:

  • Bidirectional communication – Server can push updates to clients without polling
  • Automatic fallback – If WebSocket unavailable, falls back to Server-Sent Events or Long Polling
  • Connection resilience – Automatic reconnection with exponential backoff
  • Scalability – Redis backplane enables horizontal scaling across multiple Web API instances

Update Flow (Step-by-Step)

When User A saves a change:

  1. Client sends HTTPS POST to /api/integrations/{id} with updated entity data
  2. Web API validates data (authentication, authorization, business rules)
  3. Web API writes to SQL Server using optimistic locking (checks RowVersion field)
  4. If optimistic lock succeeds:
    • Transaction commits
    • Web API logs change to Audit Database
    • Web API publishes IntegrationUpdated event to Message Queue
  5. SignalR Hub subscribes to Message Queue and receives event
  6. SignalR broadcasts to all connected clients: "IntegrationUpdated", integrationId, changedFields, userId, timestamp
  7. Connected clients receive WebSocket message:
    • If entity currently displayed: Auto-refresh entity on screen
    • If entity not displayed: Show toast notification "Integration #123 updated by Alice"
    • If user currently editing same entity: Show conflict warning before save

Latency targets:

  • Local network: <50ms from save to broadcast
  • Cloud deployment: <200ms from save to broadcast
  • Intercontinental: <500ms from save to broadcast

Optimistic vs Pessimistic Locking

Mapify uses optimistic locking for most scenarios, with pessimistic locking available for critical operations.

Aspect Optimistic Locking Pessimistic Locking
Philosophy Assume conflicts are rare; detect and resolve when they occur Prevent conflicts by locking entities before editing
Implementation RowVersion/ETag field; increment on each save; reject if version mismatch Exclusive lock in database or distributed lock in Redis
User Experience Free editing; conflict dialog only if simultaneous edits Must acquire lock before editing; "Entity locked by Alice" message if unavailable
When to Use Integrations, Resources, Services, Custom Metadata (low conflict probability) Critical configurations (Web API settings, license keys, global settings)
Scalability Excellent (no locking overhead, horizontal scaling friendly) Moderate (distributed locks require coordination, potential bottleneck)
Conflict Rate 1-3% of saves (empirical data from 100+ users) 0% (by design – only one user can edit at a time)
Failure Mode User sees conflict dialog and chooses resolution User cannot acquire lock; must wait for lock release or admin override

Recommendation: Use optimistic locking for 95% of Mapify entities. Reserve pessimistic locking for system-critical configurations where conflicts would cause service disruption.

Optimistic Locking Implementation Example

Database schema:

CREATE TABLE Integrations (
    IntegrationId UNIQUEIDENTIFIER PRIMARY KEY,
    Name NVARCHAR(255) NOT NULL,
    Description NVARCHAR(MAX),
    Owner NVARCHAR(100),
    Status NVARCHAR(50),
    RowVersion ROWVERSION NOT NULL,  -- SQL Server auto-increments on each UPDATE
    CreatedBy NVARCHAR(100),
    CreatedDate DATETIME2,
    LastModifiedBy NVARCHAR(100),
    LastModifiedDate DATETIME2
);

C# update logic:

public async Task<bool> UpdateIntegrationAsync(Integration updatedEntity, byte[] clientRowVersion)
{
    // Step 1: Fetch current entity from database
    var currentEntity = await _context.Integrations
        .FirstOrDefaultAsync(i => i.IntegrationId == updatedEntity.IntegrationId);
    
    if (currentEntity == null)
        throw new NotFoundException("Integration not found");
    
    // Step 2: Compare RowVersion (optimistic lock check)
    if (!currentEntity.RowVersion.SequenceEqual(clientRowVersion))
    {
        // Conflict detected – database was modified since client loaded it
        throw new ConcurrencyException(
            "This integration was modified by another user. Please refresh and try again.",
            currentEntity,  // Current server state
            updatedEntity   // Client's attempted changes
        );
    }
    
    // Step 3: No conflict – apply changes
    _context.Entry(currentEntity).CurrentValues.SetValues(updatedEntity);
    currentEntity.LastModifiedBy = _currentUser.Username;
    currentEntity.LastModifiedDate = DateTime.UtcNow;
    
    // Step 4: Save to database (RowVersion auto-increments)
    await _context.SaveChangesAsync();
    
    // Step 5: Publish change event to SignalR
    await _signalRHub.Clients.All.SendAsync("IntegrationUpdated", 
        currentEntity.IntegrationId, 
        _currentUser.Username,
        DateTime.UtcNow
    );
    
    return true;
}

Client-side conflict handling:

async function saveIntegration(integration) {
    try {
        const response = await fetch(`/api/integrations/${integration.id}`, {
            method: 'PUT',
            headers: { 
                'Content-Type': 'application/json',
                'If-Match': integration.rowVersion  // Send current RowVersion
            },
            body: JSON.stringify(integration)
        });
        
        if (response.status === 409) {  // 409 Conflict
            const conflict = await response.json();
            showConflictDialog(conflict.current, conflict.attempted, integration);
        } else {
            showToast('Integration saved successfully', 'success');
        }
    } catch (error) {
        showToast('Failed to save integration: ' + error.message, 'error');
    }
}

function showConflictDialog(serverState, clientState, originalState) {
    // Display conflict resolution UI
    const dialog = `
        <div class="conflict-dialog" role="dialog" aria-labelledby="conflict-title">
            <h2 id="conflict-title">
                <i class="fas fa-triangle-exclamation" aria-hidden="true"></i>
                Conflict Detected
            </h2>
            <p>This integration was modified by <strong>${serverState.lastModifiedBy}</strong> 
               while you were editing.</p>
            
            <h3>Conflicting Fields:</h3>
            <ul>
                ${getConflictingFields(serverState, clientState, originalState)}
            </ul>
            
            <fieldset>
                <legend>Choose an option:</legend>
                <label>
                    <input type="radio" name="conflict-resolution" value="keep-server" checked>
                    Keep server changes (discard my changes)
                </label>
                <label>
                    <input type="radio" name="conflict-resolution" value="overwrite">
                    Overwrite with my changes
                </label>
                <label>
                    <input type="radio" name="conflict-resolution" value="merge">
                    Review and merge manually
                </label>
            </fieldset>
            
            <button onclick="resolveConflict()">Save Choice</button>
            <button onclick="closeDialog()">Cancel</button>
        </div>
    `;
    document.body.insertAdjacentHTML('beforeend', dialog);
}

User Presence Indicators

Presence indicators show who's currently viewing or editing each entity, fostering awareness and preventing conflicts.

Presence Information Tracked

For each connected user, Mapify tracks:

Presence Data Example Value Purpose
User ID alice@contoso.com Identify the user
User Display Name Alice Johnson Human-readable name
Avatar URL https://cdn.nodinite.com/avatars/alice.jpg Profile picture
Connection Status online, idle, offline Current availability
Current View Integration #123 Which entity they're viewing
Current Action viewing, editing, commenting What they're doing
Last Activity 2026-01-19 10:30:45 UTC When last active
Session Duration 25 minutes How long they've been connected

Visual Presence Indicators

On Graph Nodes

Active users viewing/editing an entity appear as avatar badges overlaid on the node:

┌─────────────────────────────────────┐
│  Integration: SAP to Salesforce    │  ← Node
│  ┌──────────────────────────────┐  │
│  │  SAP → Salesforce            │  │
│  │  Status: Active              │  │
│  │                              │  │
│  └──────────────────────────────┘  │
│                                     │
│  👤 AJ  👤 BT                       │  ← Avatar badges (Alice Johnson, Bob Taylor)
│  └─ Editing  └─ Viewing             │
└─────────────────────────────────────┘

HTML implementation:

<div class="graph-node" data-entity-id="123">
    <div class="node-content">
        <h3>Integration: SAP to Salesforce</h3>
        <p>Status: Active</p>
    </div>
    
    <!-- Presence badges container -->
    <div class="presence-badges" role="list" aria-label="Users viewing this entity">
        <!-- Alice is editing -->
        <div class="presence-badge presence-editing" 
             role="listitem" 
             data-user-id="alice@contoso.com"
             aria-label="Alice Johnson is currently editing this integration">
            <img src="https://cdn.nodinite.com/avatars/alice.jpg" 
                 alt="Alice Johnson" 
                 class="avatar">
            <span class="presence-indicator editing" aria-hidden="true">
                <i class="fas fa-pencil"></i>
            </span>
            <span class="presence-tooltip">Alice Johnson (Editing)</span>
        </div>
        
        <!-- Bob is viewing -->
        <div class="presence-badge presence-viewing" 
             role="listitem" 
             data-user-id="bob@contoso.com"
             aria-label="Bob Taylor is currently viewing this integration">
            <img src="https://cdn.nodinite.com/avatars/bob.jpg" 
                 alt="Bob Taylor" 
                 class="avatar">
            <span class="presence-indicator viewing" aria-hidden="true">
                <i class="fas fa-eye"></i>
            </span>
            <span class="presence-tooltip">Bob Taylor (Viewing)</span>
        </div>
    </div>
</div>

CSS styling:

/* Presence badges container */
.presence-badges {
    position: absolute;
    bottom: 8px;
    right: 8px;
    display: flex;
    gap: 4px;
}

/* Individual badge */
.presence-badge {
    position: relative;
    width: 32px;
    height: 32px;
    border-radius: 50%;
    border: 2px solid #fff;
    box-shadow: 0 2px 4px rgba(0,0,0,0.2);
    cursor: pointer;
}

/* Avatar image */
.presence-badge .avatar {
    width: 100%;
    height: 100%;
    border-radius: 50%;
    object-fit: cover;
}

/* Presence indicator (editing/viewing icon) */
.presence-indicator {
    position: absolute;
    bottom: -2px;
    right: -2px;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 8px;
    border: 2px solid #fff;
}

/* Editing indicator (blue) */
.presence-indicator.editing {
    background-color: #2196F3;
    color: #fff;
}

/* Viewing indicator (green) */
.presence-indicator.viewing {
    background-color: #4CAF50;
    color: #fff;
}

/* Tooltip on hover */
.presence-tooltip {
    position: absolute;
    bottom: 120%;
    left: 50%;
    transform: translateX(-50%);
    background-color: #333;
    color: #fff;
    padding: 4px 8px;
    border-radius: 4px;
    font-size: 12px;
    white-space: nowrap;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s;
}

.presence-badge:hover .presence-tooltip {
    opacity: 1;
}

In Activity Panel

The Activity Panel (side panel or bottom drawer) shows all active users and their current focus:

┌─────────────────────────────────────────────────────────┐
│   Active Users (3)                            │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  👤 Alice Johnson                           │
│      Editing: Integration #123                         │
│      Last activity: Just now                           │
│                                                         │
│  👤 Bob Taylor                                 │
│      Viewing: Integration #123                         │
│      Last activity: 2 minutes ago                      │
│                                                         │
│  👤 Charlie Davis                          │
│      Commenting on: Resource #456                      │
│      Last activity: 5 minutes ago                      │
│                                                         │
└─────────────────────────────────────────────────────────┘

HTML implementation:

<aside class="activity-panel" aria-label="Active users panel">
    <h2>
        <i class="fas fa-users" aria-hidden="true"></i>
        Active Users (3)
    </h2>
    
    <ul class="active-users-list" role="list">
        <!-- Alice is editing -->
        <li class="active-user" role="listitem">
            <img src="https://cdn.nodinite.com/avatars/alice.jpg" 
                 alt="Alice Johnson" 
                 class="user-avatar">
            <div class="user-info">
                <div class="user-name">Alice Johnson</div>
                <div class="user-activity">
                    <i class="fas fa-pencil activity-icon editing" aria-hidden="true"></i>
                    <span>Editing: <a href="#integration-123">Integration #123</a></span>
                </div>
                <div class="user-last-activity" aria-label="Last activity: Just now">
                    Last activity: <time datetime="2026-01-19T10:30:45Z">Just now</time>
                </div>
            </div>
        </li>
        
        <!-- Bob is viewing -->
        <li class="active-user" role="listitem">
            <img src="https://cdn.nodinite.com/avatars/bob.jpg" 
                 alt="Bob Taylor" 
                 class="user-avatar">
            <div class="user-info">
                <div class="user-name">Bob Taylor</div>
                <div class="user-activity">
                    <i class="fas fa-eye activity-icon viewing" aria-hidden="true"></i>
                    <span>Viewing: <a href="#integration-123">Integration #123</a></span>
                </div>
                <div class="user-last-activity" aria-label="Last activity: 2 minutes ago">
                    Last activity: <time datetime="2026-01-19T10:28:45Z">2 minutes ago</time>
                </div>
            </div>
        </li>
        
        <!-- Charlie is commenting -->
        <li class="active-user" role="listitem">
            <img src="https://cdn.nodinite.com/avatars/charlie.jpg" 
                 alt="Charlie Davis" 
                 class="user-avatar">
            <div class="user-info">
                <div class="user-name">Charlie Davis</div>
                <div class="user-activity">
                    <i class="fas fa-comment activity-icon commenting" aria-hidden="true"></i>
                    <span>Commenting on: <a href="#resource-456">Resource #456</a></span>
                </div>
                <div class="user-last-activity" aria-label="Last activity: 5 minutes ago">
                    Last activity: <time datetime="2026-01-19T10:25:45Z">5 minutes ago</time>
                </div>
            </div>
        </li>
    </ul>
</aside>

Presence State Management

Connection lifecycle:

  1. User opens Mapify → JavaScript establishes WebSocket connection to SignalR Hub
  2. SignalR Hub registers user → Stores presence data in Redis Cache
  3. User navigates to entity → Client sends "ViewingEntity", entityId message
  4. SignalR broadcasts presence → All clients update their UI to show avatar badge
  5. User starts editing → Client sends "EditingEntity", entityId message
  6. Presence indicator changes → Badge updates from to
  7. User navigates away → Client sends "LeftEntity", entityId message
  8. Badge removed → Other clients remove avatar badge from node
  9. User disconnects → SignalR Hub removes presence data from Redis

Idle timeout:

  • After 5 minutes of inactivity (no mouse/keyboard events), user marked as idle
  • Avatar badge changes to semi-transparent (opacity: 0.5)
  • After 15 minutes idle, user removed from active users list

Scalability:

  • Presence data stored in Redis Cache (not database) for sub-10ms lookup
  • SignalR uses Redis backplane to sync presence across multiple Web API instances
  • Presence updates batched (max 1 update per 2 seconds per user) to reduce network traffic
  • Target: 1,000 concurrent users with <200ms presence update latency

Scalability Considerations

Mapify's collaboration features are designed for enterprise scale: 100+ concurrent users and 10,000+ entities.

Performance Targets

Metric Target Measurement Method
Concurrent Users 100+ simultaneous editors Load testing with SignalR test clients
Entity Count 10,000+ Integrations/Resources Realistic production dataset
WebSocket Latency <200ms save-to-broadcast (cloud) SignalR telemetry
Conflict Rate <3% of concurrent edits Application Insights logs
Presence Update Latency <500ms user-action-to-indicator Client-side performance marks
Database Query Time <100ms for entity CRUD operations SQL Server query stats
Redis Lookup Time <10ms for presence data Redis SLOWLOG analysis
SignalR Connection Limit 10,000 connections per Web API instance Azure SignalR Service capacity planning

Scalability Strategies

1. Horizontal Scaling (Scale-Out)

Architecture:

  • Deploy multiple Web API instances behind load balancer (Azure App Service, Kubernetes)
  • Use Azure SignalR Service or Redis backplane to sync SignalR across instances
  • Stateless Web API design enables unlimited horizontal scaling

Configuration example (Azure SignalR Service):

// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddSignalR()
        .AddAzureSignalR(options =>
        {
            options.ConnectionString = Configuration["AzureSignalR:ConnectionString"];
            options.ServerStickyMode = ServerStickyMode.Required;  // Sticky sessions for connection stability
        });
    
    services.AddStackExchangeRedisCache(options =>
    {
        options.Configuration = Configuration["Redis:ConnectionString"];
        options.InstanceName = "Mapify_";
    });
}

2. Database Optimization

Indexes for optimistic locking and audit queries:

-- Optimistic locking: fast RowVersion lookups
CREATE NONCLUSTERED INDEX IX_Integrations_RowVersion
ON Integrations (IntegrationId, RowVersion);

-- Audit queries: find recent changes
CREATE NONCLUSTERED INDEX IX_Integrations_LastModified
ON Integrations (LastModifiedDate DESC, LastModifiedBy)
INCLUDE (IntegrationId, Name, Status);

-- Filter by owner
CREATE NONCLUSTERED INDEX IX_Integrations_Owner
ON Integrations (Owner)
INCLUDE (IntegrationId, Name, Status);

Partitioning for audit logs:

-- Partition audit logs by month for performance and archival
CREATE PARTITION FUNCTION PF_AuditByMonth (DATETIME2)
AS RANGE RIGHT FOR VALUES (
    '2026-01-01', '2026-02-01', '2026-03-01', '2026-04-01', 
    '2026-05-01', '2026-06-01', '2026-07-01', '2026-08-01',
    '2026-09-01', '2026-10-01', '2026-11-01', '2026-12-01'
);

CREATE PARTITION SCHEME PS_AuditByMonth
AS PARTITION PF_AuditByMonth
ALL TO ([PRIMARY]);

CREATE TABLE AuditLog (
    AuditId BIGINT IDENTITY(1,1),
    EntityType NVARCHAR(50),
    EntityId UNIQUEIDENTIFIER,
    Action NVARCHAR(50),  -- 'Created', 'Updated', 'Deleted'
    ChangedBy NVARCHAR(100),
    ChangedDate DATETIME2 NOT NULL,
    OldValues NVARCHAR(MAX),
    NewValues NVARCHAR(MAX),
    CONSTRAINT PK_AuditLog PRIMARY KEY (AuditId, ChangedDate)
) ON PS_AuditByMonth(ChangedDate);

3. Caching Strategy

Three-tier caching:

  1. Client-side (Browser):

    • IndexedDB for offline editing
    • SessionStorage for current view state
    • Cache entities for 5 minutes to reduce API calls
  2. Server-side (Redis):

    • Cache frequently accessed entities (TTL: 5 minutes)
    • Cache presence data (TTL: 15 minutes, auto-refresh)
    • Invalidate cache on entity update (pub/sub pattern)
  3. Database (SQL Server):

    • Query result caching for read-heavy queries
    • Indexed views for complex joins

Redis caching example:

public async Task<Integration> GetIntegrationAsync(Guid integrationId)
{
    string cacheKey = $"integration:{integrationId}";
    
    // Try cache first
    var cachedData = await _cache.GetStringAsync(cacheKey);
    if (cachedData != null)
    {
        return JsonSerializer.Deserialize<Integration>(cachedData);
    }
    
    // Cache miss – fetch from database
    var integration = await _context.Integrations
        .FirstOrDefaultAsync(i => i.IntegrationId == integrationId);
    
    if (integration != null)
    {
        // Cache for 5 minutes
        var cacheOptions = new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
        };
        await _cache.SetStringAsync(cacheKey, 
            JsonSerializer.Serialize(integration), 
            cacheOptions);
    }
    
    return integration;
}

public async Task UpdateIntegrationAsync(Integration integration)
{
    // Update database
    _context.Integrations.Update(integration);
    await _context.SaveChangesAsync();
    
    // Invalidate cache
    string cacheKey = $"integration:{integration.IntegrationId}";
    await _cache.RemoveAsync(cacheKey);
    
    // Broadcast change to all clients
    await _signalRHub.Clients.All.SendAsync("IntegrationUpdated", integration.IntegrationId);
}

4. Message Queue for Asynchronous Processing

Use cases for message queues:

  • Audit log writes – Don't block save operations waiting for audit inserts
  • Email notifications – Send @mention emails asynchronously
  • Search index updates – Update Elasticsearch/Azure Search without blocking
  • Webhook triggers – External system integrations

Azure Service Bus example:

public async Task UpdateIntegrationAsync(Integration integration)
{
    // Step 1: Save to database (synchronous – user waits)
    _context.Integrations.Update(integration);
    await _context.SaveChangesAsync();
    
    // Step 2: Publish event to queue (asynchronous – fire-and-forget)
    var message = new ServiceBusMessage(JsonSerializer.Serialize(new
    {
        EventType = "IntegrationUpdated",
        IntegrationId = integration.IntegrationId,
        ChangedBy = _currentUser.Username,
        ChangedDate = DateTime.UtcNow,
        Changes = GetChangedFields(integration)
    }));
    
    await _serviceBusClient.SendMessageAsync(message);
    
    // Step 3: Return success to user (don't wait for queue processing)
    return integration;
}

// Background worker processes queue messages
public class AuditLogWorker : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (ServiceBusReceivedMessage message in _receiver.ReceiveMessagesAsync(stoppingToken))
        {
            var eventData = JsonSerializer.Deserialize<IntegrationUpdatedEvent>(message.Body);
            
            // Write to audit log database
            await _auditRepository.LogChangeAsync(eventData);
            
            // Complete message
            await _receiver.CompleteMessageAsync(message);
        }
    }
}

5. SignalR Scalability

Azure SignalR Service tiers:

Tier Max Connections Max Concurrent Messages/sec Use Case
Free 20 connections 20 messages/sec Development, proof-of-concept
Standard 1,000 connections per unit 1,000 messages/sec per unit Small-medium deployments (1-10 units)
Premium 1,000 connections per unit 1,000 messages/sec per unit Enterprise (HA, geo-replication, 100+ units)

Scaling formula:

  • 100 concurrent users = 100 connections
  • Average 1 edit per user per minute = 100 messages/min ≈ 2 messages/sec
  • Broadcast to 100 users = 200 messages/sec total
  • Required capacity: 1 Standard unit (1,000 messages/sec)

Cost optimization:

  • Use connection throttling to limit inactive connections (auto-disconnect after 15min idle)
  • Use message batching (send presence updates every 2 seconds, not real-time)
  • Use selective broadcasting (only send to users viewing the same entity)

Offline Editing & Sync-on-Reconnect

Mapify supports offline editing for scenarios where network connectivity is unstable (remote sites, VPN disconnects, mobile users).

Offline Capabilities

What works offline:

  • ✅ View previously loaded entities (cached in IndexedDB)
  • ✅ Edit entities (changes saved to local queue)
  • ✅ Create new entities (assigned temporary client-side IDs)
  • ✅ Delete entities (soft delete marked in local queue)
  • ✅ Search cached entities
  • ✅ Export current view to Excel

What doesn't work offline:

  • ❌ Fetch new entities from server
  • ❌ Real-time presence indicators
  • ❌ Receive updates from other users
  • ❌ Validate against server-side business rules
  • ❌ Upload attachments or images

Offline Mode UI

When Mapify detects disconnection (WebSocket closed, HTTP requests failing), it displays a prominent banner:

<div class="offline-banner" role="alert" aria-live="assertive">
    <i class="fas fa-wifi-slash" aria-hidden="true"></i>
    <strong>You are offline.</strong> 
    Changes will be saved locally and synced when your connection is restored.
    <button onclick="retryConnection()" aria-label="Retry connection">
        <i class="fas fa-rotate" aria-hidden="true"></i> Retry
    </button>
</div>

CSS styling:

.offline-banner {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    background-color: #ff9800;  /* Orange warning */
    color: #000;
    padding: 12px 16px;
    display: flex;
    align-items: center;
    gap: 12px;
    font-weight: 500;
    z-index: 10000;
    box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}

.offline-banner button {
    margin-left: auto;
    padding: 6px 12px;
    background-color: #fff;
    border: 1px solid #ccc;
    border-radius: 4px;
    cursor: pointer;
    font-weight: 500;
}

.offline-banner button:hover {
    background-color: #f5f5f5;
}

Local Change Queue

IndexedDB schema for offline edits:

// IndexedDB database: MapifyOfflineQueue
const dbSchema = {
    name: 'MapifyOfflineQueue',
    version: 1,
    stores: [
        {
            name: 'pendingChanges',
            keyPath: 'changeId',
            indexes: [
                { name: 'entityId', keyPath: 'entityId' },
                { name: 'timestamp', keyPath: 'timestamp' }
            ]
        },
        {
            name: 'cachedEntities',
            keyPath: 'entityId',
            indexes: [
                { name: 'entityType', keyPath: 'entityType' },
                { name: 'cachedDate', keyPath: 'cachedDate' }
            ]
        }
    ]
};

// Example pending change record
const pendingChange = {
    changeId: 'change_1234567890',  // Client-generated unique ID
    entityType: 'Integration',
    entityId: '550e8400-e29b-41d4-a716-446655440000',
    action: 'UPDATE',  // 'CREATE', 'UPDATE', 'DELETE'
    changes: {
        Description: 'New description added offline',
        Owner: 'alice@contoso.com'
    },
    originalRowVersion: 'AAAAAAAAB9E=',  // For conflict detection
    timestamp: '2026-01-19T10:30:45Z',
    syncStatus: 'pending',  // 'pending', 'syncing', 'synced', 'conflict'
    userId: 'alice@contoso.com'
};

Sync-on-Reconnect Flow

When connection is restored:

  1. Detect reconnection → WebSocket establishes, HTTP requests succeed
  2. Display reconnecting banner:
     Reconnecting... Syncing your offline changes.
    
  3. Fetch server state for all entities in pending change queue
  4. Compare local vs server state:
    • No server changes: Apply local changes directly → Success
    • Server changed different fields: Merge automatically → Success
    • Server changed same fields: Show conflict dialog → Manual resolution
  5. Apply changes in order (sorted by timestamp)
  6. Display sync summary:
    ✅ Reconnected – 3 changes synced successfully, 1 conflict requires review
    

JavaScript sync logic:

async function syncOfflineChanges() {
    const db = await openIndexedDB('MapifyOfflineQueue');
    const pendingChanges = await db.getAll('pendingChanges');
    
    if (pendingChanges.length === 0) {
        showToast('No offline changes to sync', 'info');
        return;
    }
    
    showToast(`Syncing ${pendingChanges.length} offline changes...`, 'info');
    
    let successCount = 0;
    let conflictCount = 0;
    const conflicts = [];
    
    for (const change of pendingChanges) {
        try {
            // Fetch current server state
            const serverState = await fetch(`/api/${change.entityType}s/${change.entityId}`)
                .then(r => r.json());
            
            // Check for conflicts (compare RowVersion)
            if (serverState.rowVersion !== change.originalRowVersion) {
                // Conflict detected
                const hasConflict = detectFieldConflicts(
                    change.changes, 
                    serverState, 
                    change.originalRowVersion
                );
                
                if (hasConflict) {
                    conflictCount++;
                    conflicts.push({ change, serverState });
                    continue;  // Skip for now, handle in conflict dialog
                }
            }
            
            // No conflict or auto-mergeable – apply change
            const response = await fetch(`/api/${change.entityType}s/${change.entityId}`, {
                method: 'PUT',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ ...serverState, ...change.changes })
            });
            
            if (response.ok) {
                successCount++;
                await db.delete('pendingChanges', change.changeId);
            } else {
                throw new Error(`Server rejected change: ${response.statusText}`);
            }
        } catch (error) {
            console.error('Sync failed for change:', change, error);
            conflictCount++;
            conflicts.push({ change, error });
        }
    }
    
    // Display results
    if (conflictCount > 0) {
        showConflictResolutionDialog(conflicts);
    }
    
    showToast(
        `Synced ${successCount} changes successfully. ${conflictCount} conflicts require review.`, 
        conflictCount > 0 ? 'warning' : 'success'
    );
}

Conflict Resolution for Offline Edits

Offline conflict resolution UI:

<div class="offline-sync-conflicts" role="dialog" aria-labelledby="sync-conflicts-title">
    <h2 id="sync-conflicts-title">
        <i class="fas fa-triangle-exclamation" aria-hidden="true"></i>
        Offline Sync Conflicts (3)
    </h2>
    
    <p>The following entities were modified by other users while you were offline:</p>
    
    <ul class="conflict-list">
        <!-- Conflict 1 -->
        <li class="conflict-item">
            <h3>Integration: SAP to Salesforce (#123)</h3>
            <p>Modified by <strong>Bob Taylor</strong> on 2026-01-19 at 10:25 AM</p>
            
            <table role="table" aria-label="Field conflicts for Integration 123">
                <thead>
                    <tr>
                        <th scope="col">Field</th>
                        <th scope="col">Your Offline Change</th>
                        <th scope="col">Server Value</th>
                        <th scope="col">Keep</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td><strong>Description</strong></td>
                        <td>Updated offline description</td>
                        <td>Bob's new description</td>
                        <td>
                            <select aria-label="Choose value for Description field">
                                <option value="local">My change</option>
                                <option value="server" selected>Server change</option>
                                <option value="merge">Merge both</option>
                            </select>
                        </td>
                    </tr>
                    <tr>
                        <td><strong>Status</strong></td>
                        <td>Active</td>
                        <td>In Review</td>
                        <td>
                            <select aria-label="Choose value for Status field">
                                <option value="local">My change</option>
                                <option value="server" selected>Server change</option>
                            </select>
                        </td>
                    </tr>
                </tbody>
            </table>
            
            <button onclick="resolveConflict(123)" class="btn-primary">Apply Resolution</button>
        </li>
    </ul>
    
    <div class="dialog-actions">
        <button onclick="acceptAllServerChanges()" class="btn-secondary">
            Accept All Server Changes
        </button>
        <button onclick="closeDialog()" class="btn-secondary">
            Review Later
        </button>
    </div>
</div>

Change Tracking & Audit Trail

Change tracking provides a complete audit trail of all modifications to Repository Model entities, answering the critical questions: Who changed what, when, and why? This is essential for enterprise governance, compliance audits (SOX, GDPR, HIPAA), root-cause analysis, and accountability in multi-user environments.

Every entity modification is captured with full metadata and stored in a queryable audit log. Administrators can generate compliance reports, users can review recent changes, and teams can track the evolution of their integration landscapes over time.

Change Tracking Example
Example of change history panel showing recent modifications with user information and timestamps.

Why Change Tracking?

Change tracking delivers critical business value for governance and operations:

  • Compliance audit support – SOX, GDPR, HIPAA require "who changed what" audit trails
  • Root-cause analysis – Trace back configuration changes that caused incidents
  • Accountability – Clear ownership of changes prevents finger-pointing during outages
  • Rollback capability – Restore previous configurations if changes cause issues
  • Trend analysis – Identify frequently changed entities (instability hotspots)
  • Knowledge retention – Understand historical decisions when original team members leave

Note: Change tracking is enabled by default in Nodinite 7.x. Retention policies are configurable per environment.


Entity Audit Metadata

Every Repository Model entity (Integration, System, Service, Resource) tracks the following metadata fields:

Field Name Data Type Description Example Value
CreatedBy String (email/username) User who created the entity alice.johnson@contoso.com
CreatedDate DateTime (UTC) Timestamp when entity was created 2025-12-15T09:30:00Z
LastModifiedBy String (email/username) User who last modified the entity bob.taylor@contoso.com
LastModifiedDate DateTime (UTC) Timestamp of most recent modification 2026-01-19T14:22:35Z
RowVersion Byte[] (concurrency token) Auto-incremented version for optimistic locking AAAAAAAAB9E= (Base64 encoded)
ModificationCount Integer Total number of times entity has been modified 42

Display in Mapify UI:

These metadata fields are displayed in the entity detail panel:

<div class="entity-audit-metadata" aria-label="Entity audit information">
    <h3>
        <i class="fas fa-clock-rotate-left" aria-hidden="true"></i>
        Audit Information
    </h3>
    
    <dl class="metadata-list">
        <dt>Created By:</dt>
        <dd>
            <a href="mailto:alice.johnson@contoso.com" class="user-link">
                Alice Johnson
            </a>
            on <time datetime="2025-12-15T09:30:00Z">Dec 15, 2025 at 9:30 AM</time>
        </dd>
        
        <dt>Last Modified By:</dt>
        <dd>
            <a href="mailto:bob.taylor@contoso.com" class="user-link">
                Bob Taylor
            </a>
            on <time datetime="2026-01-19T14:22:35Z">Jan 19, 2026 at 2:22 PM</time>
            <span class="modification-count" title="Total modifications">
                (42 edits)
            </span>
        </dd>
        
        <dt>Actions:</dt>
        <dd>
            <button onclick="showChangeHistory()" class="btn-link" aria-label="View full change history">
                <i class="fas fa-list" aria-hidden="true"></i>
                View Change History
            </button>
        </dd>
    </dl>
</div>

CSS for audit metadata display:

.entity-audit-metadata {
    background-color: #f8f9fa;
    border-left: 3px solid #0056b3;
    padding: 16px;
    margin-top: 24px;
    border-radius: 4px;
}

.entity-audit-metadata h3 {
    font-size: 16px;
    font-weight: 600;
    color: #212529;
    margin-bottom: 12px;
}

.metadata-list {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 8px 16px;
    font-size: 14px;
}

.metadata-list dt {
    font-weight: 600;
    color: #495057;
}

.metadata-list dd {
    color: #212529;
    margin: 0;
}

.user-link {
    color: #0056b3;
    text-decoration: none;
}

.user-link:hover {
    text-decoration: underline;
}

.modification-count {
    color: #6c757d;
    font-size: 13px;
}

.btn-link {
    background: none;
    border: none;
    color: #0056b3;
    cursor: pointer;
    padding: 4px 0;
    font-size: 14px;
}

.btn-link:hover {
    text-decoration: underline;
}

.btn-link i {
    margin-right: 6px;
}

Change History Log Structure

Detailed change history is stored in a dedicated EntityChangeLog table with field-level granularity. Each record captures what changed, who changed it, and when.

Database Schema (SQL Server):

CREATE TABLE [dbo].[EntityChangeLog]
(
    [ChangeId] UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
    [EntityType] NVARCHAR(50) NOT NULL,  -- 'Integration', 'System', 'Service', 'Resource'
    [EntityId] UNIQUEIDENTIFIER NOT NULL,
    [EntityName] NVARCHAR(255) NULL,  -- Denormalized for reporting
    [ChangeType] NVARCHAR(20) NOT NULL,  -- 'CREATE', 'UPDATE', 'DELETE'
    [FieldName] NVARCHAR(100) NULL,  -- NULL for CREATE/DELETE, specific field for UPDATE
    [OldValue] NVARCHAR(MAX) NULL,  -- Previous value (NULL for CREATE)
    [NewValue] NVARCHAR(MAX) NULL,  -- New value (NULL for DELETE)
    [ChangedBy] NVARCHAR(255) NOT NULL,  -- Email or username
    [ChangedDate] DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
    [ChangeReason] NVARCHAR(500) NULL,  -- Optional commit message or comment
    [SessionId] NVARCHAR(100) NULL,  -- Group related changes from same save operation
    [IPAddress] NVARCHAR(45) NULL,  -- For security audits (IPv4/IPv6)
    [UserAgent] NVARCHAR(500) NULL,  -- Browser/client information
    
    INDEX [IX_EntityChangeLog_EntityId] ([EntityId], [ChangedDate] DESC),
    INDEX [IX_EntityChangeLog_ChangedBy] ([ChangedBy], [ChangedDate] DESC),
    INDEX [IX_EntityChangeLog_ChangedDate] ([ChangedDate] DESC),
    INDEX [IX_EntityChangeLog_EntityType] ([EntityType], [ChangedDate] DESC)
);

JSON representation for API responses:

{
    "changeId": "550e8400-e29b-41d4-a716-446655440000",
    "entityType": "Integration",
    "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "entityName": "SAP to Salesforce Sync",
    "changeType": "UPDATE",
    "fieldName": "Description",
    "oldValue": "Legacy description from 2024",
    "newValue": "Updated description with GDPR compliance notes",
    "changedBy": "alice.johnson@contoso.com",
    "changedDate": "2026-01-19T14:22:35.123Z",
    "changeReason": "Added GDPR compliance documentation per legal review",
    "sessionId": "session_abc123",
    "ipAddress": "192.168.1.100",
    "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0"
}

Change history display in Mapify UI:

<div class="change-history-panel" role="dialog" aria-labelledby="change-history-title">
    <h2 id="change-history-title">
        <i class="fas fa-clock-rotate-left" aria-hidden="true"></i>
        Change History: Integration #123
    </h2>
    
    <div class="history-filters">
        <label for="user-filter">Filter by User:</label>
        <select id="user-filter" aria-label="Filter changes by user">
            <option value="">All Users</option>
            <option value="alice.johnson@contoso.com">Alice Johnson</option>
            <option value="bob.taylor@contoso.com">Bob Taylor</option>
        </select>
        
        <label for="date-filter">Date Range:</label>
        <select id="date-filter" aria-label="Filter by date range">
            <option value="7d">Last 7 days</option>
            <option value="30d" selected>Last 30 days</option>
            <option value="90d">Last 90 days</option>
            <option value="all">All history</option>
        </select>
    </div>
    
    <div class="history-timeline" role="list">
        <!-- Single change entry -->
        <div class="change-entry" role="listitem">
            <div class="change-avatar">
                <img src="/api/users/avatar/alice.johnson@contoso.com" 
                     alt="Alice Johnson" 
                     width="40" 
                     height="40">
            </div>
            <div class="change-details">
                <div class="change-header">
                    <strong>Alice Johnson</strong>
                    <time datetime="2026-01-19T14:22:35Z">Jan 19, 2026 at 2:22 PM</time>
                </div>
                <div class="change-body">
                    <span class="change-type update">Updated</span>
                    <strong>Description</strong>
                    <p class="change-reason">
                        <i class="fas fa-comment" aria-hidden="true"></i>
                        "Added GDPR compliance documentation per legal review"
                    </p>
                </div>
                <div class="change-diff">
                    <div class="diff-old">
                        <strong>Old:</strong> Legacy description from 2024
                    </div>
                    <div class="diff-new">
                        <strong>New:</strong> Updated description with GDPR compliance notes
                    </div>
                </div>
                <div class="change-actions">
                    <button onclick="revertChange('550e8400-e29b-41d4-a716-446655440000')" 
                            class="btn-sm-secondary">
                        <i class="fas fa-rotate-left" aria-hidden="true"></i>
                        Revert This Change
                    </button>
                </div>
            </div>
        </div>
        
        <!-- Another change entry -->
        <div class="change-entry" role="listitem">
            <div class="change-avatar">
                <img src="/api/users/avatar/bob.taylor@contoso.com" 
                     alt="Bob Taylor" 
                     width="40" 
                     height="40">
            </div>
            <div class="change-details">
                <div class="change-header">
                    <strong>Bob Taylor</strong>
                    <time datetime="2026-01-18T10:15:00Z">Jan 18, 2026 at 10:15 AM</time>
                </div>
                <div class="change-body">
                    <span class="change-type update">Updated</span>
                    <strong>Owner</strong>
                </div>
                <div class="change-diff">
                    <div class="diff-old">
                        <strong>Old:</strong> charlie.davis@contoso.com
                    </div>
                    <div class="diff-new">
                        <strong>New:</strong> alice.johnson@contoso.com
                    </div>
                </div>
            </div>
        </div>
        
        <!-- Create event -->
        <div class="change-entry" role="listitem">
            <div class="change-avatar">
                <img src="/api/users/avatar/charlie.davis@contoso.com" 
                     alt="Charlie Davis" 
                     width="40" 
                     height="40">
            </div>
            <div class="change-details">
                <div class="change-header">
                    <strong>Charlie Davis</strong>
                    <time datetime="2025-12-15T09:30:00Z">Dec 15, 2025 at 9:30 AM</time>
                </div>
                <div class="change-body">
                    <span class="change-type create">Created</span>
                    this integration
                </div>
            </div>
        </div>
    </div>
    
    <button onclick="closeChangeHistory()" class="btn-secondary">Close</button>
</div>

CSS for change history timeline:

.change-history-panel {
    max-width: 800px;
    margin: 0 auto;
    padding: 24px;
}

.history-filters {
    display: flex;
    gap: 16px;
    margin-bottom: 24px;
    padding-bottom: 16px;
    border-bottom: 1px solid #dee2e6;
}

.history-filters label {
    margin-right: 8px;
    font-weight: 600;
}

.history-timeline {
    position: relative;
}

/* Timeline line */
.history-timeline::before {
    content: '';
    position: absolute;
    left: 20px;
    top: 0;
    bottom: 0;
    width: 2px;
    background-color: #dee2e6;
}

.change-entry {
    display: flex;
    gap: 16px;
    margin-bottom: 24px;
    position: relative;
}

.change-avatar {
    flex-shrink: 0;
    z-index: 1;
}

.change-avatar img {
    border-radius: 50%;
    border: 3px solid #fff;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

.change-details {
    flex: 1;
    background-color: #f8f9fa;
    border-radius: 8px;
    padding: 16px;
}

.change-header {
    display: flex;
    justify-content: space-between;
    margin-bottom: 12px;
}

.change-header strong {
    color: #212529;
}

.change-header time {
    color: #6c757d;
    font-size: 14px;
}

.change-type {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 4px;
    font-size: 12px;
    font-weight: 600;
    text-transform: uppercase;
}

.change-type.create {
    background-color: #d4edda;
    color: #155724;
}

.change-type.update {
    background-color: #fff3cd;
    color: #856404;
}

.change-type.delete {
    background-color: #f8d7da;
    color: #721c24;
}

.change-reason {
    margin-top: 8px;
    padding: 8px;
    background-color: #e7f3ff;
    border-left: 3px solid #0056b3;
    font-style: italic;
    color: #495057;
}

.change-diff {
    margin-top: 12px;
    font-size: 14px;
}

.diff-old,
.diff-new {
    padding: 8px;
    margin: 4px 0;
    border-radius: 4px;
}

.diff-old {
    background-color: #f8d7da;
    color: #721c24;
}

.diff-new {
    background-color: #d4edda;
    color: #155724;
}

.change-actions {
    margin-top: 12px;
}

.btn-sm-secondary {
    padding: 6px 12px;
    font-size: 13px;
    background-color: #fff;
    border: 1px solid #ced4da;
    border-radius: 4px;
    cursor: pointer;
}

.btn-sm-secondary:hover {
    background-color: #e9ecef;
}

.btn-sm-secondary i {
    margin-right: 6px;
}

Filter Examples: Recent Modifications

Common filter queries for finding recently changed entities:

Filter Description Query Syntax Use Case
Show all changes in last 7 days modified:7d Weekly review of recent updates
Show entities modified by specific user lastModifiedBy:alice@contoso.com Track changes by team member
Show entities modified by anyone except me lastModifiedBy:NOT {currentUser} Review others' changes since last login
Show entities modified since specific date modified:>2026-01-15 Compliance audit for specific time period
Show frequently modified entities (>10 edits) modificationCount:>10 Identify unstable/high-churn integrations
Show entities created in last 30 days created:30d Track new integrations and growth
Show entities not modified in 90 days modified:>90d Identify stale/abandoned integrations
Combine filters: Modified by Alice in last 7 days lastModifiedBy:alice@contoso.com AND modified:7d Targeted review of specific user's recent work

Filter UI in Mapify:

<div class="recent-changes-filter" role="search" aria-label="Filter by recent modifications">
    <h3>
        <i class="fas fa-filter" aria-hidden="true"></i>
        Filter by Recent Changes
    </h3>
    
    <form id="change-filter-form">
        <div class="form-row">
            <label for="date-range">Modified In:</label>
            <select id="date-range" name="dateRange">
                <option value="1d">Last 24 hours</option>
                <option value="7d" selected>Last 7 days</option>
                <option value="30d">Last 30 days</option>
                <option value="90d">Last 90 days</option>
                <option value="custom">Custom date range...</option>
            </select>
        </div>
        
        <div class="form-row">
            <label for="modified-by">Modified By:</label>
            <select id="modified-by" name="modifiedBy" multiple aria-label="Select users">
                <option value="">All users</option>
                <option value="alice.johnson@contoso.com">Alice Johnson</option>
                <option value="bob.taylor@contoso.com">Bob Taylor</option>
                <option value="charlie.davis@contoso.com">Charlie Davis</option>
            </select>
        </div>
        
        <div class="form-row">
            <label for="modification-count">Modification Count:</label>
            <select id="modification-count" name="modificationCount">
                <option value="">Any</option>
                <option value=">5">More than 5 edits</option>
                <option value=">10">More than 10 edits</option>
                <option value=">20">More than 20 edits</option>
            </select>
        </div>
        
        <div class="form-actions">
            <button type="submit" class="btn-primary">
                <i class="fas fa-search" aria-hidden="true"></i>
                Apply Filter
            </button>
            <button type="reset" class="btn-secondary">
                Clear Filters
            </button>
        </div>
    </form>
    
    <div class="filter-results" role="status" aria-live="polite">
        <!-- Results populated dynamically -->
        <p><strong>42 entities</strong> modified in last 7 days</p>
    </div>
</div>

Audit Report Formats for Compliance

Pre-built audit reports for common compliance scenarios (SOX, GDPR, HIPAA):

1. Change Activity Report (All Entities)

Purpose: Comprehensive report of all changes for a given time period.

Report Fields:

  • Entity Type (Integration, System, Service, Resource)
  • Entity Name
  • Changed By (user email/name)
  • Changed Date (timestamp)
  • Field Name (what was changed)
  • Old Value / New Value
  • Change Reason (if provided)

Output Formats:

  • Excel (.xlsx) – Full detail with filtering/sorting
  • CSV (.csv) – Raw data for import into other systems
  • PDF (.pdf) – Executive summary with charts

Sample Excel report structure:

| Entity Type | Entity Name           | Field Name   | Old Value    | New Value      | Changed By              | Changed Date        | Change Reason             |
|-------------|-----------------------|--------------|--------------|----------------|-------------------------|---------------------|---------------------------|
| Integration | SAP to Salesforce     | Description  | Old desc     | New desc       | alice.johnson@...       | 2026-01-19 14:22:35 | GDPR compliance update    |
| Integration | SAP to Salesforce     | Owner        | charlie@...  | alice@...      | bob.taylor@...          | 2026-01-18 10:15:00 | Ownership transfer        |
| System      | SAP ERP Production    | Status       | Active       | In Review      | admin@...               | 2026-01-17 09:00:00 | Quarterly review          |

C# code to generate Excel report:

public async Task<byte[]> GenerateChangeActivityReportAsync(
    DateTime startDate, 
    DateTime endDate, 
    string entityType = null)
{
    using (var package = new ExcelPackage())
    {
        var worksheet = package.Workbook.Worksheets.Add("Change Activity");
        
        // Header row
        worksheet.Cells[1, 1].Value = "Entity Type";
        worksheet.Cells[1, 2].Value = "Entity Name";
        worksheet.Cells[1, 3].Value = "Field Name";
        worksheet.Cells[1, 4].Value = "Old Value";
        worksheet.Cells[1, 5].Value = "New Value";
        worksheet.Cells[1, 6].Value = "Changed By";
        worksheet.Cells[1, 7].Value = "Changed Date";
        worksheet.Cells[1, 8].Value = "Change Reason";
        
        // Style header
        using (var range = worksheet.Cells[1, 1, 1, 8])
        {
            range.Style.Font.Bold = true;
            range.Style.Fill.PatternType = ExcelFillStyle.Solid;
            range.Style.Fill.BackgroundColor.SetColor(Color.FromArgb(0, 112, 192));
            range.Style.Font.Color.SetColor(Color.White);
        }
        
        // Fetch data
        var query = _context.EntityChangeLogs
            .Where(c => c.ChangedDate >= startDate && c.ChangedDate <= endDate);
        
        if (!string.IsNullOrEmpty(entityType))
        {
            query = query.Where(c => c.EntityType == entityType);
        }
        
        var changes = await query
            .OrderByDescending(c => c.ChangedDate)
            .ToListAsync();
        
        // Populate data rows
        int row = 2;
        foreach (var change in changes)
        {
            worksheet.Cells[row, 1].Value = change.EntityType;
            worksheet.Cells[row, 2].Value = change.EntityName;
            worksheet.Cells[row, 3].Value = change.FieldName ?? "(Entity Created)";
            worksheet.Cells[row, 4].Value = change.OldValue ?? "";
            worksheet.Cells[row, 5].Value = change.NewValue ?? "";
            worksheet.Cells[row, 6].Value = change.ChangedBy;
            worksheet.Cells[row, 7].Value = change.ChangedDate;
            worksheet.Cells[row, 7].Style.Numberformat.Format = "yyyy-mm-dd hh:mm:ss";
            worksheet.Cells[row, 8].Value = change.ChangeReason ?? "";
            
            row++;
        }
        
        // Auto-fit columns
        worksheet.Cells.AutoFitColumns();
        
        return package.GetAsByteArray();
    }
}

2. User Activity Report (By Person)

Purpose: Track all changes made by specific users (useful for access reviews, offboarding).

Report Fields:

  • User Name
  • User Email
  • Total Changes Made
  • Entity Types Modified (breakdown)
  • Date Range
  • Most Frequently Modified Entities

Sample report:

User Activity Report
Date Range: 2026-01-01 to 2026-01-31

| User Name       | Email                  | Total Changes | Integrations | Systems | Services | Most Edited Entity       |
|-----------------|------------------------|---------------|--------------|---------|----------|--------------------------|
| Alice Johnson   | alice.johnson@...      | 142           | 98           | 32      | 12       | SAP to Salesforce (23x)  |
| Bob Taylor      | bob.taylor@...         | 87            | 45           | 28      | 14       | Dynamics CRM API (18x)   |
| Charlie Davis   | charlie.davis@...      | 63            | 40           | 15      | 8        | Legacy EDI Bridge (15x)  |

3. Compliance Audit Report (Filtered by Tag/Domain)

Purpose: Generate audit trail for specific compliance scope (e.g., all GDPR-regulated systems).

Report Filters:

  • Domain (Finance, HR, Sales, etc.)
  • Compliance Tags (GDPR, HIPAA, SOX, PCI-DSS)
  • Entity Type
  • Date Range

Sample SQL query:

-- Compliance Audit Report: All changes to GDPR-regulated integrations in Q1 2026
SELECT 
    ecl.EntityType,
    ecl.EntityName,
    ecl.FieldName,
    ecl.OldValue,
    ecl.NewValue,
    ecl.ChangedBy,
    ecl.ChangedDate,
    ecl.ChangeReason,
    cm.MetadataValue AS ComplianceTag
FROM EntityChangeLog ecl
INNER JOIN Entities e ON ecl.EntityId = e.EntityId
INNER JOIN CustomMetadata cm ON e.EntityId = cm.EntityId
WHERE cm.MetadataKey = 'ComplianceTag'
  AND cm.MetadataValue LIKE '%GDPR%'
  AND ecl.ChangedDate >= '2026-01-01'
  AND ecl.ChangedDate < '2026-04-01'
ORDER BY ecl.ChangedDate DESC;

4. High-Risk Change Report

Purpose: Identify changes to critical/production systems for extra scrutiny.

Report Criteria:

  • Changes to production environments only
  • Changes to entities tagged as "Critical"
  • Changes made outside business hours (off-hours deployments)
  • Changes without change reason documentation

Sample report:

High-Risk Changes Report
Period: Last 30 days

| Entity Name              | Environment | Changed By        | Changed Date        | Field Changed | Change Reason        | Risk Level |
|--------------------------|-------------|-------------------|---------------------|---------------|----------------------|------------|
| Payment Gateway API      | Production  | admin@...         | 2026-01-19 23:45:00 | Endpoint URL  | (none provided)      | HIGH       |
| SAP ERP Production       | Production  | alice@...         | 2026-01-18 02:30:00 | Credentials   | Emergency fix #1234  | MEDIUM     |
| Customer Data Sync       | Production  | bob@...           | 2026-01-15 18:00:00 | Status        | Planned maintenance  | LOW        |

Retention Policy & Data Management

Change history retention balances compliance requirements with database storage costs:

Environment Default Retention Configurable Range Compliance Notes
Production 365 days (1 year) 90 days – Indefinite SOX requires 7 years for financial systems; GDPR allows deletion after purpose fulfilled
Test/QA 90 days 30 days – 180 days Limited compliance requirements; focus on performance testing data
Development 30 days 7 days – 90 days No compliance requirements; short retention minimizes storage

Retention configuration in appsettings.json:

{
    "ChangeTracking": {
        "RetentionPolicy": {
            "DefaultRetentionDays": 365,
            "AutoDeleteEnabled": true,
            "AutoDeleteSchedule": "0 2 * * 0",  // Weekly on Sunday at 2 AM
            "ArchiveBeforeDelete": true,
            "ArchiveLocation": "\\\\fileserver\\audit-archives\\",
            "ComplianceOverrides": {
                "FinancialSystemsRetentionDays": 2555,  // 7 years for SOX
                "HIPAARetentionDays": 2190,  // 6 years for HIPAA
                "GDPRRetentionDays": 365  // 1 year default, deletable on request
            }
        }
    }
}

Automated cleanup job (C# background service):

public class ChangeLogCleanupService : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ILogger<ChangeLogCleanupService> _logger;
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                using var scope = _scopeFactory.CreateScope();
                var context = scope.ServiceProvider.GetRequiredService<NodiniteDbContext>();
                var config = scope.ServiceProvider.GetRequiredService<IConfiguration>();
                
                var retentionDays = config.GetValue<int>("ChangeTracking:RetentionPolicy:DefaultRetentionDays", 365);
                var cutoffDate = DateTime.UtcNow.AddDays(-retentionDays);
                
                // Archive before deleting (if enabled)
                if (config.GetValue<bool>("ChangeTracking:RetentionPolicy:ArchiveBeforeDelete", true))
                {
                    await ArchiveOldChangesAsync(context, cutoffDate);
                }
                
                // Delete old records
                var deletedCount = await context.EntityChangeLogs
                    .Where(c => c.ChangedDate < cutoffDate)
                    .ExecuteDeleteAsync();
                
                _logger.LogInformation("Deleted {Count} change log records older than {CutoffDate}", deletedCount, cutoffDate);
                
                // Wait 7 days before next run
                await Task.Delay(TimeSpan.FromDays(7), stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error during change log cleanup");
                await Task.Delay(TimeSpan.FromHours(1), stoppingToken);  // Retry in 1 hour
            }
        }
    }
    
    private async Task ArchiveOldChangesAsync(NodiniteDbContext context, DateTime cutoffDate)
    {
        var archivePath = Path.Combine(
            _config["ChangeTracking:RetentionPolicy:ArchiveLocation"],
            $"ChangeLog_Archive_{DateTime.UtcNow:yyyyMMdd}.csv"
        );
        
        var oldChanges = await context.EntityChangeLogs
            .Where(c => c.ChangedDate < cutoffDate)
            .OrderBy(c => c.ChangedDate)
            .ToListAsync();
        
        // Write to CSV archive
        using var writer = new StreamWriter(archivePath);
        using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
        await csv.WriteRecordsAsync(oldChanges);
        
        _logger.LogInformation("Archived {Count} records to {Path}", oldChanges.Count, archivePath);
    }
}

Audit Query Examples

Common SQL queries for compliance audits and governance reporting:

Query 1: All changes to a specific entity

-- View complete change history for Integration #123
SELECT 
    ChangeId,
    ChangeType,
    FieldName,
    OldValue,
    NewValue,
    ChangedBy,
    ChangedDate,
    ChangeReason
FROM EntityChangeLog
WHERE EntityId = '3fa85f64-5717-4562-b3fc-2c963f66afa6'  -- Integration #123
ORDER BY ChangedDate DESC;

Query 2: Changes by specific user in date range

-- All changes made by Alice in January 2026
SELECT 
    EntityType,
    EntityName,
    FieldName,
    OldValue,
    NewValue,
    ChangedDate,
    ChangeReason
FROM EntityChangeLog
WHERE ChangedBy = 'alice.johnson@contoso.com'
  AND ChangedDate >= '2026-01-01'
  AND ChangedDate < '2026-02-01'
ORDER BY ChangedDate DESC;

Query 3: Entities modified most frequently (high-churn analysis)

-- Top 20 most frequently changed entities (instability hotspots)
SELECT TOP 20
    EntityType,
    EntityName,
    COUNT(*) AS ChangeCount,
    COUNT(DISTINCT ChangedBy) AS UniqueEditors,
    MIN(ChangedDate) AS FirstChange,
    MAX(ChangedDate) AS LastChange
FROM EntityChangeLog
WHERE ChangedDate >= DATEADD(DAY, -90, GETUTCDATE())  -- Last 90 days
GROUP BY EntityType, EntityName, EntityId
ORDER BY ChangeCount DESC;

Query 4: Changes without change reasons (governance flag)

-- Changes missing change reason documentation (governance violation)
SELECT 
    EntityType,
    EntityName,
    FieldName,
    ChangedBy,
    ChangedDate
FROM EntityChangeLog
WHERE ChangeReason IS NULL
  AND ChangedDate >= DATEADD(DAY, -30, GETUTCDATE())
  AND EntityType IN ('Integration', 'System')  -- Critical entities only
ORDER BY ChangedDate DESC;

Query 5: After-hours changes (security audit)

-- Changes made outside business hours (potential security risk)
SELECT 
    EntityType,
    EntityName,
    FieldName,
    ChangedBy,
    ChangedDate,
    DATEPART(HOUR, ChangedDate) AS HourOfDay,
    DATENAME(WEEKDAY, ChangedDate) AS DayOfWeek
FROM EntityChangeLog
WHERE (
    DATEPART(HOUR, ChangedDate) < 7 OR DATEPART(HOUR, ChangedDate) >= 19  -- Before 7 AM or after 7 PM
    OR DATENAME(WEEKDAY, ChangedDate) IN ('Saturday', 'Sunday')  -- Weekend
)
AND ChangedDate >= DATEADD(DAY, -30, GETUTCDATE())
ORDER BY ChangedDate DESC;

Query 6: Field-level change frequency analysis

-- Which fields are modified most often (UX optimization insight)
SELECT 
    EntityType,
    FieldName,
    COUNT(*) AS ChangeCount,
    COUNT(DISTINCT EntityId) AS AffectedEntities
FROM EntityChangeLog
WHERE ChangedDate >= DATEADD(DAY, -90, GETUTCDATE())
  AND FieldName IS NOT NULL  -- Exclude CREATE/DELETE events
GROUP BY EntityType, FieldName
ORDER BY ChangeCount DESC;

Query 7: Rollback audit (who reverted what)

-- Detect rollbacks (OldValue = current database value after subsequent change)
WITH ChangeSequence AS (
    SELECT 
        EntityId,
        FieldName,
        OldValue,
        NewValue,
        ChangedBy,
        ChangedDate,
        LEAD(NewValue) OVER (PARTITION BY EntityId, FieldName ORDER BY ChangedDate) AS NextValue
    FROM EntityChangeLog
    WHERE ChangedDate >= DATEADD(DAY, -30, GETUTCDATE())
)
SELECT 
    EntityId,
    FieldName,
    OldValue AS RevertedTo,
    NewValue AS TemporaryValue,
    NextValue AS FinalValue,
    ChangedBy AS UserWhoReverted,
    ChangedDate
FROM ChangeSequence
WHERE NewValue = NextValue  -- Indicates revert
ORDER BY ChangedDate DESC;

GDPR Compliance & Right to Deletion

GDPR considerations for change tracking:

Key Principle: Change logs containing personal data must respect GDPR "right to be forgotten" (Article 17), but audit trail requirements (SOX, HIPAA) may override in certain cases. Consult legal counsel for your specific regulatory context.

GDPR-compliant deletion workflow:

  1. User submits deletion request via formal GDPR request form
  2. Legal review determines if audit retention overrides apply (financial systems = 7 years SOX retention)
  3. If deletion approved:
    • Anonymize user identity in ChangedBy field → Replace with "User_DELETED_20260119"
    • Preserve change metadata (what/when) for audit trail
    • Delete or redact any personal data in OldValue/NewValue fields
  4. Generate deletion certificate for data subject

SQL script to anonymize deleted user:

-- Anonymize all changes by user requesting GDPR deletion
DECLARE @UserEmail NVARCHAR(255) = 'alice.johnson@contoso.com';
DECLARE @AnonymizedId NVARCHAR(255) = CONCAT('User_DELETED_', FORMAT(GETUTCDATE(), 'yyyyMMdd'));

BEGIN TRANSACTION;

-- Update ChangedBy field
UPDATE EntityChangeLog
SET ChangedBy = @AnonymizedId
WHERE ChangedBy = @UserEmail;

-- Redact personal data in change values (example: email addresses)
UPDATE EntityChangeLog
SET OldValue = '[REDACTED]'
WHERE OldValue LIKE '%' + @UserEmail + '%';

UPDATE EntityChangeLog
SET NewValue = '[REDACTED]'
WHERE NewValue LIKE '%' + @UserEmail + '%';

COMMIT TRANSACTION;

-- Log GDPR deletion action
INSERT INTO GDPRDeletionLog (UserEmail, DeletionDate, RecordsAffected)
SELECT @UserEmail, GETUTCDATE(), @@ROWCOUNT;

Compliance documentation link:

For full GDPR, SOX, HIPAA, and PCI-DSS compliance guidelines, see:
Compliance & Data Governance Guide


Comments & Annotations – Team Collaboration on Entities

Comments and Annotations enable distributed teams to collaborate directly on Repository Model entities without leaving Mapify. Add contextual discussions, ask questions, request approvals, and document decisions right where the work happens. No more scattered email threads, lost Slack messages, or "who said what about this integration?" confusion.

Comments support @mentions for direct notifications, threaded replies for organized discussions, and status tracking (Open → Resolved → Closed) for approval workflows and issue resolution.

Why Use Comments?

Comments transform Mapify from a visualization tool into a collaborative workspace:

  • 50% faster decision-making – Questions answered in context, no meeting scheduling needed
  • Zero missed notifications – @mentions email relevant team members instantly
  • Approval workflows – Track "Pending Review" → "Approved" → "Closed" lifecycle
  • Permanent documentation – Decision rationale preserved in audit trail
  • Knowledge transfer – New team members read historical discussions to understand "why"
  • Compliance evidence – Demonstrate review and approval for SOX/GDPR audits

Typical use cases:

  • Compliance approvals ("Legal has reviewed this GDPR integration – approved ✓")
  • Incident root-cause analysis ("Why did this integration fail last Tuesday?")
  • Architecture questions ("Should we use REST or SOAP for this System?")
  • Change coordination ("Don't modify this until Friday's deployment window")
  • Knowledge capture ("This Service requires special firewall rules – see ticket #1234")

Comment Data Structure

Comments are stored with rich metadata to enable threading, status tracking, and notifications:

Field Name Type Example Value Purpose
CommentId GUID 7c9e6679-7425-40de-944b-e07fc1f90ae7 Unique identifier for comment
EntityId GUID 3fa85f64-5717-4562-b3fc-2c963f66afa6 Entity this comment is attached to (Integration, System, etc.)
EntityType String Integration Type of entity (denormalized for reporting)
EntityName String SAP to Salesforce Entity name (denormalized for email notifications)
UserId GUID 8d2f3a4b-1c5e-6f7d-8a9b-0c1d2e3f4a5b User who created the comment
UserName String Alice Johnson Display name (denormalized)
UserEmail String alice.johnson@contoso.com Email for notifications (denormalized)
CommentText String (max 4000 chars) @bob.taylor Can you review the error handling logic before we deploy? Comment content (supports Markdown and @mentions)
ParentCommentId GUID (nullable) null (top-level) or 7c9e6679... (reply) Parent comment for threaded replies; null for top-level comments
CreatedDate DateTime (UTC) 2026-01-19T14:23:45Z When comment was created
ModifiedDate DateTime (UTC, nullable) 2026-01-19T15:10:00Z When comment was last edited (null if never edited)
Status Enum Open, Resolved, Closed Comment status for approval/issue workflows
MentionedUsers JSON array ["bob.taylor@contoso.com", "charlie.davis@contoso.com"] List of @mentioned user emails (parsed from CommentText)
Attachments JSON array [{"fileName": "architecture.png", "fileUrl": "..."}] Optional file attachments (screenshots, diagrams)
IsEdited Boolean true Indicates if comment was modified after creation
IsDeleted Boolean false Soft delete flag (preserves comment in threads)

Database schema (SQL):

CREATE TABLE EntityComments (
    CommentId UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
    EntityId UNIQUEIDENTIFIER NOT NULL,
    EntityType NVARCHAR(50) NOT NULL,  -- Denormalized for reporting
    EntityName NVARCHAR(255) NOT NULL,  -- Denormalized for email notifications
    UserId UNIQUEIDENTIFIER NOT NULL,
    UserName NVARCHAR(255) NOT NULL,  -- Denormalized
    UserEmail NVARCHAR(255) NOT NULL,  -- Denormalized
    CommentText NVARCHAR(4000) NOT NULL,
    ParentCommentId UNIQUEIDENTIFIER NULL,  -- NULL for top-level, GUID for replies
    CreatedDate DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
    ModifiedDate DATETIME2 NULL,
    Status NVARCHAR(20) NOT NULL DEFAULT 'Open',  -- Open, Resolved, Closed
    MentionedUsers NVARCHAR(MAX) NULL,  -- JSON array of mentioned user emails
    Attachments NVARCHAR(MAX) NULL,  -- JSON array of attachment metadata
    IsEdited BIT NOT NULL DEFAULT 0,
    IsDeleted BIT NOT NULL DEFAULT 0,
    
    CONSTRAINT FK_EntityComments_Entities FOREIGN KEY (EntityId) REFERENCES Entities(EntityId),
    CONSTRAINT FK_EntityComments_Users FOREIGN KEY (UserId) REFERENCES Users(UserId),
    CONSTRAINT FK_EntityComments_ParentComment FOREIGN KEY (ParentCommentId) REFERENCES EntityComments(CommentId)
);

-- Indexes for performance
CREATE INDEX IX_EntityComments_EntityId ON EntityComments(EntityId) WHERE IsDeleted = 0;
CREATE INDEX IX_EntityComments_ParentCommentId ON EntityComments(ParentCommentId) WHERE IsDeleted = 0;
CREATE INDEX IX_EntityComments_CreatedDate ON EntityComments(CreatedDate DESC);
CREATE INDEX IX_EntityComments_Status ON EntityComments(Status) WHERE IsDeleted = 0;

Example comment JSON (API response):

{
    "commentId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "entityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "entityType": "Integration",
    "entityName": "SAP to Salesforce",
    "userId": "8d2f3a4b-1c5e-6f7d-8a9b-0c1d2e3f4a5b",
    "userName": "Alice Johnson",
    "userEmail": "alice.johnson@contoso.com",
    "commentText": "@bob.taylor Can you review the error handling logic before we deploy? Also cc @charlie.davis for security review.",
    "parentCommentId": null,
    "createdDate": "2026-01-19T14:23:45Z",
    "modifiedDate": null,
    "status": "Open",
    "mentionedUsers": [
        "bob.taylor@contoso.com",
        "charlie.davis@contoso.com"
    ],
    "attachments": [
        {
            "fileName": "error-handling-diagram.png",
            "fileUrl": "https://storage.contoso.com/attachments/7c9e6679.../error-handling-diagram.png",
            "fileSize": 245678,
            "uploadedDate": "2026-01-19T14:23:40Z"
        }
    ],
    "isEdited": false,
    "isDeleted": false,
    "replies": []  // Array of nested comment objects for threaded replies
}

@Mention Functionality

@mentions notify specific users when they're referenced in a comment. Mentions are parsed from comment text using @username or @email syntax and trigger email notifications.

Mention Syntax

  • By username: @alice.johnson (automatically matched to user account)
  • By email: @alice.johnson@contoso.com (explicit email address)
  • By display name: @Alice Johnson (fuzzy matched if display name is unique)
  • Multiple mentions: @bob.taylor @charlie.davis (space-separated)

Auto-complete While Typing

When users type @ in the comment box, an autocomplete dropdown appears:

<!-- Mention autocomplete dropdown -->
<div class="mention-autocomplete" role="listbox" aria-label="Mention user suggestions">
    <div class="mention-option" role="option" aria-selected="false" tabindex="0">
        <img src="avatar-alice.png" alt="" class="mention-avatar" aria-hidden="true">
        <div class="mention-details">
            <strong>Alice Johnson</strong>
            <small>alice.johnson@contoso.com</small>
        </div>
    </div>
    <div class="mention-option" role="option" aria-selected="false" tabindex="0">
        <img src="avatar-bob.png" alt="" class="mention-avatar" aria-hidden="true">
        <div class="mention-details">
            <strong>Bob Taylor</strong>
            <small>bob.taylor@contoso.com</small>
        </div>
    </div>
    <div class="mention-option" role="option" aria-selected="false" tabindex="0">
        <img src="avatar-charlie.png" alt="" class="mention-avatar" aria-hidden="true">
        <div class="mention-details">
            <strong>Charlie Davis</strong>
            <small>charlie.davis@contoso.com</small>
        </div>
    </div>
</div>

Autocomplete behavior:

  • Appears immediately when @ is typed
  • Filters as user continues typing (@ali → shows "Alice Johnson")
  • Keyboard navigation: Arrow keys to select, Enter to insert, Escape to cancel
  • Mouse selection: Click to insert mention
  • Fuzzy matching: @alice matches "Alice Johnson" and "alice.johnson@contoso.com"
  • Max 10 suggestions shown (sorted by recent collaboration frequency)

Notification Mechanism

When a comment with mentions is saved, the system:

  1. Parses mentions from CommentText using regex: /@(\S+@\S+\.\S+|[\w.]+)/g
  2. Resolves user emails (match username or email to user account)
  3. Stores in MentionedUsers JSON array field
  4. Sends email notifications to each mentioned user
  5. Creates in-app notifications (bell icon in Mapify header)

Email notification template:

Subject: Alice Johnson mentioned you in a comment on "SAP to Salesforce"

Hi Bob,

Alice Johnson mentioned you in a comment on the Integration "SAP to Salesforce":

    @bob.taylor Can you review the error handling logic before we deploy? 
    Also cc @charlie.davis for security review.

Click here to view and reply:
https://mapify.contoso.com/integration/3fa85f64-5717-4562-b3fc-2c963f66afa6#comment-7c9e6679

---
This is an automated notification from Nodinite Mapify.
Update your notification preferences: https://mapify.contoso.com/settings/notifications

In-app notification (bell icon dropdown):

<div class="notification-item unread" role="alert">
    <img src="avatar-alice.png" alt="Alice Johnson avatar" class="notification-avatar">
    <div class="notification-content">
        <strong>Alice Johnson</strong> mentioned you in a comment on 
        <a href="/integration/3fa85f64#comment-7c9e6679">SAP to Salesforce</a>
        <small class="notification-time">2 minutes ago</small>
    </div>
    <button class="btn-mark-read" aria-label="Mark as read">
        <i class="fas fa-check" aria-hidden="true"></i>
    </button>
</div>

Notification preferences (user settings):

Users can control mention notification behavior:

  • Email notifications: On (default), Off, Digest (daily summary)
  • In-app notifications: On (default), Off
  • Mobile push notifications: On, Off (requires Nodinite mobile app)
  • Quiet hours: Disable notifications outside business hours (9 AM - 5 PM user's timezone)

Comment Threading (Nested Replies)

Comment threads organize discussions hierarchically with nested replies, making it easy to follow multi-person conversations.

Threading Structure

  • Top-level comments have ParentCommentId = NULL
  • Replies have ParentCommentId pointing to parent comment
  • Max nesting depth: 3 levels (top → reply → reply to reply)
  • Indentation: 20px per nesting level for visual hierarchy

Example Thread Structure

Comment #1 (Alice, top-level)
├── Reply #2 (Bob, depth 1)
│   └── Reply #3 (Alice, depth 2)
│       └── Reply #4 (Charlie, depth 3) ← Max depth reached
└── Reply #5 (Charlie, depth 1)

Visual representation (HTML/CSS):

<div class="comment-thread">
    <!-- Top-level comment -->
    <div class="comment" data-comment-id="comment-1" data-depth="0">
        <img src="avatar-alice.png" alt="Alice Johnson avatar" class="comment-avatar">
        <div class="comment-content">
            <div class="comment-header">
                <strong>Alice Johnson</strong>
                <span class="comment-time">2 hours ago</span>
                <span class="comment-status status-open">Open</span>
            </div>
            <div class="comment-body">
                @bob.taylor Can you review the error handling logic before we deploy?
            </div>
            <div class="comment-actions">
                <button class="btn-reply" aria-label="Reply to Alice Johnson's comment">
                    <i class="fas fa-reply" aria-hidden="true"></i> Reply
                </button>
                <button class="btn-resolve" aria-label="Mark comment as resolved">
                    <i class="fas fa-check" aria-hidden="true"></i> Resolve
                </button>
            </div>
        </div>
    </div>

    <!-- Reply (depth 1) -->
    <div class="comment comment-reply" data-comment-id="comment-2" data-depth="1" style="margin-left: 20px;">
        <img src="avatar-bob.png" alt="Bob Taylor avatar" class="comment-avatar">
        <div class="comment-content">
            <div class="comment-header">
                <strong>Bob Taylor</strong>
                <span class="comment-time">1 hour ago</span>
            </div>
            <div class="comment-body">
                Looks good! I've reviewed the try-catch blocks. One suggestion: add logging for the fallback path.
            </div>
            <div class="comment-actions">
                <button class="btn-reply" aria-label="Reply to Bob Taylor's comment">
                    <i class="fas fa-reply" aria-hidden="true"></i> Reply
                </button>
            </div>
        </div>
    </div>

    <!-- Reply to reply (depth 2) -->
    <div class="comment comment-reply" data-comment-id="comment-3" data-depth="2" style="margin-left: 40px;">
        <img src="avatar-alice.png" alt="Alice Johnson avatar" class="comment-avatar">
        <div class="comment-content">
            <div class="comment-header">
                <strong>Alice Johnson</strong>
                <span class="comment-time">30 minutes ago</span>
            </div>
            <div class="comment-body">
                Great idea! I'll add Application Insights logging. Thanks @bob.taylor!
            </div>
            <div class="comment-actions">
                <button class="btn-reply" aria-label="Reply to Alice Johnson's comment">
                    <i class="fas fa-reply" aria-hidden="true"></i> Reply
                </button>
            </div>
        </div>
    </div>
</div>

CSS for threading:

.comment {
    display: flex;
    gap: 12px;
    padding: 12px;
    border-left: 3px solid transparent;
    transition: background-color 0.2s;
}

.comment:hover {
    background-color: #f5f5f5;
}

.comment-reply {
    margin-left: 20px;  /* Indent per depth level */
    border-left-color: #e0e0e0;  /* Visual thread line */
}

.comment[data-depth="1"] { margin-left: 20px; }
.comment[data-depth="2"] { margin-left: 40px; }
.comment[data-depth="3"] { margin-left: 60px; }  /* Max depth */

.comment-avatar {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    flex-shrink: 0;
}

.comment-content {
    flex: 1;
    min-width: 0;  /* Prevent overflow */
}

.comment-header {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 8px;
}

.comment-time {
    color: #757575;
    font-size: 0.875rem;
}

.comment-body {
    margin-bottom: 8px;
    line-height: 1.6;
    white-space: pre-wrap;  /* Preserve line breaks */
}

.comment-actions {
    display: flex;
    gap: 12px;
}

.btn-reply, .btn-resolve {
    background: transparent;
    border: 1px solid #e0e0e0;
    padding: 4px 12px;
    border-radius: 4px;
    cursor: pointer;
    font-size: 0.875rem;
    transition: background-color 0.2s;
}

.btn-reply:hover, .btn-resolve:hover {
    background-color: #f5f5f5;
}

.btn-reply:focus, .btn-resolve:focus {
    outline: 2px solid #0056b3;
    outline-offset: 2px;
}

Comment Status Lifecycle

Comments support a status workflow for approval processes and issue resolution:

Status Icon Color Meaning Typical Use Case
Open Blue (#0056b3) Active discussion, question unanswered, or approval pending "Can someone review this before production deploy?"
Resolved Green (#28a745) Question answered, issue fixed, or approval granted "Reviewed and approved for deployment ✓"
Closed Gray (#6c757d) Discussion archived, no action needed, or obsolete "This integration was deprecated; closing thread."

Status Transition Rules

graph LR A[Open] -->|Resolve button| B[Resolved] A -->|Close button| C[Closed] B -->|Reopen button| A B -->|Close button| C C -->|Reopen button| A

Who can change status:

  • Comment author can change their own comment status
  • Entity owner can change status of any comment on their entities
  • Administrators can change any comment status

Status change notifications:

When comment status changes, system notifies:

  • Original comment author
  • All users who replied in the thread
  • All @mentioned users
  • Entity owner (if different from above)

Email notification example:

Subject: Comment resolved: "Can you review the error handling logic?"

Hi Alice,

Your comment on "SAP to Salesforce" was marked as Resolved by Bob Taylor:

    Original comment: @bob.taylor Can you review the error handling logic before we deploy?
    
    Bob's reply: Looks good! I've reviewed the try-catch blocks. One suggestion: add logging for the fallback path.

Status: Open → Resolved

View thread: https://mapify.contoso.com/integration/3fa85f64#comment-7c9e6679

UI Display Options

Comments can be displayed in three locations based on user workflow:

Option 1: Inline on Node (Minimalist)

Best for: Quick visibility without cluttering the graph

Visual indicator:

  • Small comment badge icon on node corner: (count: 3)
  • Badge color indicates status: Blue (open), Green (resolved), Gray (closed)
  • Hover tooltip shows most recent comment preview
┌────────────────────────┐
│  SAP to Salesforce     │ 🔵3  ← Comment badge (3 open comments)
│  Integration           │
│  Status: Active        │
└────────────────────────┘

HTML for comment badge:

<div class="node-badge comment-badge" aria-label="3 open comments on this integration">
    <i class="fas fa-comment" aria-hidden="true"></i>
    <span class="badge-count">3</span>
</div>

<!-- Hover tooltip (appears on :hover) -->
<div class="comment-tooltip" role="tooltip">
    <div class="tooltip-header">
        <strong>3 comments</strong>
        <small>Most recent:</small>
    </div>
    <div class="tooltip-body">
        <strong>Alice Johnson</strong> 2 hours ago<br>
        @bob.taylor Can you review the error handling logic...
    </div>
    <small class="tooltip-action">Click node to view all comments</small>
</div>

Option 2: Side Panel (Contextual)

Best for: Reading and replying to comments while viewing entity details

Placement: Right side panel (slides in when entity is selected)

Sections:

  1. Entity details (top): Name, type, owner, status, metadata
  2. Comments section (below): Scrollable list of all comments and replies
  3. Add comment box (bottom): Text area with @mention autocomplete
┌─────────────────────────────┐
│  Entity Details   │  ← Close button
├─────────────────────────────┤
│ Integration: SAP to Sales...│
│ Owner: Alice Johnson        │
│ Status: Active              │
│                             │
│  Comments (3)  │  ← Section header
├─────────────────────────────┤
│ ┌─ Alice Johnson (2h ago)  │
│ │ @bob.taylor Can you...    │
│ │ [Reply] [Resolve]         │
│ └─┬─ Bob Taylor (1h ago)   │
│   │ Looks good! I've...     │
│   │ [Reply]                 │
│   └─── Alice (30m ago)     │
│       Great idea! I'll...   │
│       [Reply]               │
├─────────────────────────────┤
│  Add Comment       │
│ ┌───────────────────────┐   │
│ │ Type your comment...  │   │
│ │ Use @mention to notify│   │
│ └───────────────────────┘   │
│ [Cancel]  [Post Comment]    │
└─────────────────────────────┘

HTML for side panel:

<aside class="entity-detail-panel" role="complementary" aria-label="Entity details and comments">
    <!-- Entity Details Section -->
    <div class="panel-header">
        <h2>Entity Details</h2>
        <button class="btn-close-panel" aria-label="Close entity details panel">
            <i class="fas fa-times" aria-hidden="true"></i>
        </button>
    </div>
    
    <div class="entity-summary">
        <dl>
            <dt>Integration:</dt>
            <dd>SAP to Salesforce</dd>
            <dt>Owner:</dt>
            <dd>Alice Johnson</dd>
            <dt>Status:</dt>
            <dd><span class="status-badge status-active">Active</span></dd>
        </dl>
    </div>

    <!-- Comments Section -->
    <div class="comments-section">
        <h3>
            <i class="fas fa-comments" aria-hidden="true"></i> Comments (3)
        </h3>
        
        <!-- Filter/Sort controls -->
        <div class="comment-controls">
            <select aria-label="Filter comments by status">
                <option value="all">All Comments</option>
                <option value="open">Open Only</option>
                <option value="resolved">Resolved Only</option>
            </select>
            <select aria-label="Sort comments">
                <option value="newest">Newest First</option>
                <option value="oldest">Oldest First</option>
            </select>
        </div>

        <!-- Comment threads (scrollable) -->
        <div class="comments-list" role="list">
            <!-- Comment thread inserted here (see Threading section) -->
        </div>
    </div>

    <!-- Add Comment Section -->
    <div class="add-comment-section">
        <h4>
            <i class="fas fa-plus" aria-hidden="true"></i> Add Comment
        </h4>
        <form class="comment-form">
            <label for="comment-text" class="visually-hidden">Comment text</label>
            <textarea 
                id="comment-text" 
                name="commentText" 
                rows="3" 
                placeholder="Type your comment... Use @mention to notify team members"
                aria-describedby="comment-help"
                required
            ></textarea>
            <small id="comment-help" class="form-help">
                Use @username to mention team members. Supports Markdown formatting.
            </small>
            
            <div class="comment-form-actions">
                <button type="button" class="btn-cancel">Cancel</button>
                <button type="submit" class="btn-primary">
                    <i class="fas fa-paper-plane" aria-hidden="true"></i> Post Comment
                </button>
            </div>
        </form>
    </div>
</aside>

Accessibility requirements:

  • Panel announced to screen readers when opened: aria-live="polite" on panel container
  • Focus moved to panel header when opened (keyboard navigation)
  • Escape key closes panel and returns focus to trigger element
  • All comment threads navigable by Tab key
  • Reply/Resolve buttons have descriptive aria-label attributes

Option 3: Modal Dialog (Full-Screen Focus)

Best for: Extended discussions requiring full attention (approval workflows, complex threads)

Trigger: "View Comments" button or comment badge click

Layout: Centered modal overlay with dimmed background

╔═══════════════════════════════════════════════════╗
║  Comments: SAP to Salesforce Integration    [X]  ║
╠═══════════════════════════════════════════════════╣
║   Filter: All (3)  |  Sort: Newest    ║
╟───────────────────────────────────────────────────╢
║  ┌─ Alice Johnson (2 hours ago)  [Open]          ║
║  │ @bob.taylor Can you review the error          ║
║  │ handling logic before we deploy?              ║
║  │ [Reply] [Resolve]                             ║
║  └─┬─ Bob Taylor (1 hour ago)                    ║
║    │ Looks good! I've reviewed the try-catch...  ║
║    │ [Reply]                                      ║
║    └─── Alice Johnson (30 minutes ago)           ║
║        Great idea! I'll add Application...       ║
║        [Reply]                                    ║
║                                                   ║
║  ┌─ Charlie Davis (5 hours ago)  [Resolved]      ║
║  │ Security review complete. Approved for prod.  ║
║  └─── [No replies]                               ║
║                                                   ║
╟───────────────────────────────────────────────────╢
║   Add Comment                            ║
║  ┌─────────────────────────────────────────────┐ ║
║  │ Type your comment...                        │ ║
║  └─────────────────────────────────────────────┘ ║
║  [Attach File]  [Cancel]  [Post Comment]         ║
╚═══════════════════════════════════════════════════╝

HTML for modal:

<div class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="modal-title">
    <div class="modal-container comment-modal">
        <!-- Modal Header -->
        <div class="modal-header">
            <h2 id="modal-title">Comments: SAP to Salesforce Integration</h2>
            <button class="btn-close-modal" aria-label="Close comments modal">
                <i class="fas fa-times" aria-hidden="true"></i>
            </button>
        </div>

        <!-- Filter/Sort Bar -->
        <div class="modal-toolbar">
            <div class="filter-group">
                <label for="filter-status">
                    <i class="fas fa-filter" aria-hidden="true"></i> Filter:
                </label>
                <select id="filter-status" aria-label="Filter comments by status">
                    <option value="all">All (3)</option>
                    <option value="open">Open (1)</option>
                    <option value="resolved">Resolved (2)</option>
                </select>
            </div>
            <div class="sort-group">
                <label for="sort-order">Sort:</label>
                <select id="sort-order" aria-label="Sort comments">
                    <option value="newest">Newest First</option>
                    <option value="oldest">Oldest First</option>
                    <option value="status">By Status</option>
                </select>
            </div>
        </div>

        <!-- Comment Threads (scrollable) -->
        <div class="modal-body">
            <div class="comments-list" role="list">
                <!-- Comment threads inserted here -->
            </div>
        </div>

        <!-- Add Comment Form -->
        <div class="modal-footer">
            <h3>
                <i class="fas fa-plus" aria-hidden="true"></i> Add Comment
            </h3>
            <form class="comment-form">
                <textarea 
                    id="modal-comment-text" 
                    name="commentText" 
                    rows="3" 
                    placeholder="Type your comment... Use @mention to notify team members"
                    aria-label="Comment text"
                    required
                ></textarea>
                <div class="comment-form-actions">
                    <button type="button" class="btn-attach-file">
                        <i class="fas fa-paperclip" aria-hidden="true"></i> Attach File
                    </button>
                    <button type="button" class="btn-cancel">Cancel</button>
                    <button type="submit" class="btn-primary">
                        <i class="fas fa-paper-plane" aria-hidden="true"></i> Post Comment
                    </button>
                </div>
            </form>
        </div>
    </div>
</div>

Modal accessibility:

  • Modal announced to screen readers when opened
  • Focus trapped inside modal (Tab cycles through modal elements only)
  • Escape key closes modal and restores focus to trigger button
  • Background content inert (cannot be interacted with while modal is open)
  • Close button always visible and keyboard-accessible

Comment Use Cases

Use Case 1: Compliance Approval Workflow

Scenario: Legal team must approve all GDPR-related integrations before production deployment.

Workflow:

  1. Developer (Alice) completes integration configuration and adds comment:

    "Integration ready for legal review. @legal.team Please approve for production."
    Status: Open

  2. Legal reviewer (Charlie) reviews integration, posts approval:

    "GDPR compliance verified. Data processing agreement in place. Approved for production."
    Status: Resolved (marks Alice's comment as Resolved)

  3. Developer (Alice) receives notification and proceeds with deployment
  4. Audit trail preserved: When was approval given? Who approved? What was reviewed?

Benefit: Compliance approval embedded in the tool; no separate email trails or SharePoint documents. SOX/GDPR auditors can query approval history directly from Mapify.


Use Case 2: Incident Root-Cause Analysis

Scenario: Production integration fails at 2 AM. Multiple team members investigate concurrently.

Workflow:

  1. On-call engineer (Bob) adds comment at 2:05 AM:

    "Integration failing with timeout errors. Investigating. @alice.johnson FYI"
    Status: Open

  2. Integration owner (Alice, wakes up) replies at 2:15 AM:

    "I'll check the API endpoint. @charlie.davis Can you verify firewall rules?"

  3. Network admin (Charlie) replies at 2:20 AM:

    "Firewall rules look good. No recent changes."

  4. Bob identifies root cause at 2:30 AM:

    "Found it: database connection pool exhausted. Restarting service. Adding connection pool monitoring."
    Status: Resolved

  5. Alice follows up at 9:00 AM:

    "Thanks team! Let's add this to our runbook. @bob.taylor Can you document the fix?" Status: Closed (incident resolved, documentation action item created)

Benefit: Entire investigation timeline preserved in context. No need to search Slack history or email threads. New team members can read the thread to understand how similar issues were resolved in the past.


Use Case 3: Architecture Review Question

Scenario: Developer unsure whether to use REST or SOAP for new System integration.

Workflow:

  1. Developer (Alice) adds comment on System entity:

    "@solution.architect Should we use REST or SOAP for the legacy mainframe integration? Performance is critical."
    Status: Open

  2. Solution architect (David) replies:

    "For mainframe, SOAP is better supported. Performance impact is minimal (<50ms difference). Here's a benchmark: [attach: performance-comparison.xlsx]"

  3. Alice confirms:

    "Thanks! Going with SOAP. Closing this thread."
    Status: Resolved

Benefit: Architectural decisions documented in context. Future developers working on this System can see why SOAP was chosen, avoiding re-litigation of past decisions.


Use Case 4: Change Coordination

Scenario: Critical integration undergoing maintenance window; prevent accidental edits.

Workflow:

  1. DevOps lead (Bob) adds comment:

    " MAINTENANCE WINDOW: Do not modify this integration until Friday 6 PM. Deployment in progress."
    Status: Open

  2. Other developers see comment badge on node and read warning before editing
  3. Bob updates comment Friday 7 PM:

    "Deployment complete. Safe to edit now."
    Status: Resolved

Benefit: Prevents coordination failures. Comment serves as real-time lock without blocking edit capability (soft lock, not hard lock).


Best Practices

For Comment Authors

  • Be specific – Reference field names, error codes, ticket numbers for context
  • Use @mentions sparingly – Only tag users who need to take action
  • Attach evidence – Screenshots, logs, diagrams clarify complex issues
  • Resolve when done – Mark comments as Resolved to close the loop
  • Provide context – New readers should understand the issue without external research

For Teams

  • Define SLAs for responses – "@mentions should be acknowledged within 4 business hours"
  • Use status workflow – Open = needs action, Resolved = done, Closed = archived
  • Archive old threads – Close resolved comments after 30 days to reduce clutter
  • Standardize approval language – "Approved for production" = Resolved status
  • Integrate with ticketing – Reference Jira/ServiceNow ticket numbers in comments

For Administrators

  • Monitor comment volume – High comment count may indicate unclear documentation
  • Export for compliance – Quarterly reports for audit trail requirements
  • Set retention policy – Closed comments archived after 1 year (configurable)
  • Enable notifications – Ensure email delivery for @mentions
  • Audit critical entities – Review approval comments on production integrations

Accessibility Considerations

Comments UI must meet WCAG 2.1 AA standards:

Keyboard Navigation

  • Tab key: Navigate through comments, reply buttons, resolve buttons
  • Enter/Space: Activate buttons (Reply, Resolve, Post Comment)
  • Escape: Close comment modal or side panel
  • Arrow keys: Navigate autocomplete dropdown during @mention typing

Screen Reader Support

  • Comment threads: Use role="list" and role="listitem" for semantic structure
  • Status badges: Include aria-label with full status text ("Open comment", "Resolved comment")
  • Avatars: alt attribute with user name ("Alice Johnson avatar")
  • Action buttons: Descriptive aria-label ("Reply to Alice Johnson's comment")
  • @mention autocomplete: role="listbox" and role="option" for dropdown
  • Notifications: aria-live="polite" for new comment announcements

Visual Accessibility

  • Color contrast: Status colors meet 4.5:1 ratio (Blue #0056b3, Green #28a745, Gray #6c757d)
  • Not color-only: Status indicated by icon + text + color (e.g., + "Resolved" + green)
  • Focus indicators: 2px solid outline on all interactive elements (buttons, links, textareas)
  • Font size: Minimum 14px for comment text, 12px for metadata (time, user name)
  • Touch targets: Minimum 44×44px for mobile (Reply button, Resolve button)

Reduced Motion

@media (prefers-reduced-motion: reduce) {
    .comment {
        transition: none;  /* Disable hover animations */
    }
    
    .mention-autocomplete {
        animation: none;  /* Disable slide-in animation */
    }
}

Integration with Other Features

Comments integrate seamlessly with other Mapify features:

Saved Views

  • Comment filters: Save views showing "Entities with open comments" or "Entities I'm mentioned in"
  • Notification views: Auto-filter to entities with comments created in last 24 hours

Search and Discovery

  • Comment text search: Global search includes comment content
    Example: Search "firewall rules" finds entities with firewall-related comments
  • @mention search: commented-by:alice.johnson finds all entities where Alice commented
  • Status search: comment-status:open filters to entities with unresolved comments

Change Tracking

  • Comments logged: Comment creation/edit/delete events appear in EntityChangeLog
  • Audit reports: Compliance reports include comment activity (who commented when, on what entities)

Ownership and Team Management

  • Team mentions: @integration-team mentions all team members (expands to individual emails)
  • Owner notifications: Entity owner auto-notified when first comment is added to their entity
  • Escalation: Unresolved comments on critical entities escalate to owner after 48 hours

  • Saved Views – Save and share custom graph configurations
  • Search and Discovery – Find entities with fuzzy search and filters
  • Ownership and Team Management – Assign owners and manage teams
  • Repository Model – Core data model for Mapify entities
  • Web API Documentation – REST API and authentication

Best Practices

For Administrators

  • Monitor SignalR metrics – Track connection count, message throughput, latency
  • Set up Redis caching – Improves presence lookup performance by 10x
  • Configure offline mode – Enable IndexedDB for remote workers
  • Plan for scale – Use Azure SignalR Service for >100 concurrent users
  • Review audit logs monthly – Detect unusual edit patterns or compliance issues

For Users

  • Watch for presence indicators – Check who's editing before making changes
  • Resolve conflicts promptly – Don't ignore conflict dialogs; data integrity depends on it
  • Use comments for coordination – @mention colleagues to clarify ownership or get approval
  • Save frequently – Auto-save every 30 seconds reduces conflict probability
  • Enable notifications – Get alerted when entities you care about are modified

For Developers

  • Use optimistic locking – Field-level RowVersion checks prevent lost updates
  • Handle conflicts gracefully – Show user-friendly conflict dialogs, not 409 errors
  • Test offline scenarios – Simulate network disconnects during integration testing
  • Log all conflicts – Application Insights helps identify conflict hotspots
  • Cache presence data – Redis lookup must be <10ms for smooth UX