npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nuxthealth/dicom

v0.2.0-beta.4

Published

Nuxt module for DICOM

Readme

Nuxt DICOM

npm version npm downloads License Nuxt

A comprehensive Nuxt module for working with DICOM (Digital Imaging and Communications in Medicine) files. Includes a Rust-based StoreSCP server, event-driven architecture, file management UI, and more.

Features

  • 🏥 StoreSCP Server - Rust-based DICOM receiver running alongside your Nuxt app
  • 🎯 Event-Driven - React to DICOM events with server-side handlers
  • 📁 File Management - Browse, download, and delete stored DICOM files via UI
  • 🧹 Auto Cleanup - Automatic deletion of old files with configurable retention
  • 📊 Service Management - Start/stop services and view live logs through UI
  • 🔧 Tag Extraction - Configure which DICOM tags to extract automatically
  • 📝 Log Levels - Configurable logging with runtime control
  • 🎨 Built-in UI - Beautiful admin interface powered by Nuxt UI

Installation

npm install @nuxthealth/dicom @nuxthealth/node-dicom

Quick Start

1. Add Module to Config

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxthealth/dicom'],
  
  dicom: {
    // Service configuration
    services: {
      storeScp: {
        name: 'storeScp_1',
        port: 11112,
        callingAETitle: 'STORESCP',
        outDir: './dicom-storage',
        autoStart: true,
        extractTags: [
          'PatientName',
          'PatientID',
          'StudyDate',
          'Modality'
        ]
      }
    },
    
    // Optional: Auto-delete old files
    autoDeleteAfterDays: 30, // 0 = disabled
    
    // Optional: Default log level
    logLevel: 'info' // 'debug' | 'info' | 'warn' | 'error'
  }
})

2. Create Event Handlers

Create handlers in server/dicom/ directory:

// server/dicom/storeScp.onFileStored.ts
import { defineDicomEvent } from '#imports'

export default defineDicomEvent('storeScp_onFileStored', async (payload, { logger }) => {
  logger.info('File received', {
    file: payload.file,
    patient: payload.tags?.PatientName,
    modality: payload.tags?.Modality,
    studyDate: payload.tags?.StudyDate
  })
  
  // Your custom logic here
})

3. Access the UI

Start your development server:

npm run dev

Navigate to /_dicom to access the management interface where you can:

  • View service status
  • Start/stop services
  • Browse stored files
  • View live logs
  • Manage file cleanup

Using the DICOM UI Component

Built-in Route (Recommended)

By default, the DICOM UI is available at /_dicom. To use it:

// nuxt.config.ts
export default defineNuxtConfig({
  dicom: {
    route: true,        // Enable the route (default)
    routePath: '/_dicom' // Customize the path
  }
})

Navigate to /_dicom in your browser to access the management interface.

Custom Layout

Wrap the DICOM UI with a custom layout:

// nuxt.config.ts
export default defineNuxtConfig({
  dicom: {
    route: true,
    layout: 'admin'  // Use your 'admin' layout from layouts/admin.vue
  }
})

Or use a standalone page without any layout:

export default defineNuxtConfig({
  dicom: {
    route: true,
    layout: false  // No layout wrapper (default)
  }
})

Use Component Directly

Import and use the DicomApp component in your own pages or components:

<!-- pages/admin/dicom.vue -->
<template>
  <div>
    <h1>DICOM Management</h1>
    <DicomApp />
  </div>
</template>

The component is automatically registered as DicomApp and ready to use.

Custom Integration with Composables

For more control, use the provided composables:

<!-- pages/custom-dicom.vue -->
<script setup>
import { ref } from 'vue'
import { useLiveServiceLogs } from '#app'
import { useLogLevel } from '#app'
import { useServiceFiles } from '#app'

const serviceName = ref('storeScp_1')
const { logs, isConnected } = useLiveServiceLogs(serviceName)
const { logLevel, setLogLevel } = useLogLevel(serviceName)
const { files, deleteFile, cleanup } = useServiceFiles(serviceName)
</script>

<template>
  <div class="dicom-admin">
    <!-- Your custom UI here -->
    <div class="logs">
      <h2>Logs</h2>
      <p>Status: {{ isConnected ? 'Connected' : 'Disconnected' }}</p>
      <div v-for="log in logs" :key="log.id" class="log-entry">
        {{ log.message }}
      </div>
    </div>

    <div class="settings">
      <h2>Log Level</h2>
      <select :value="logLevel" @change="setLogLevel">
        <option>debug</option>
        <option>info</option>
        <option>warn</option>
        <option>error</option>
      </select>
    </div>

    <div class="files">
      <h2>Files</h2>
      <button @click="cleanup(30)">Cleanup files older than 30 days</button>
    </div>
  </div>
</template>

Configuration

Service Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | name | string | Required | Unique service identifier | | port | number | Required | Port for DICOM server | | callingAETitle | string | 'STORESCP' | Application Entity Title | | outDir | string | './dicom-storage' | Directory for storing files | | autoStart | boolean | true | Start service automatically | | extractTags | string[] | [] | DICOM tags to extract |

Module Options

| Option | Type | Default | Description | |--------|------|---------|-------------| | services | object | {} | Service configurations | | autoDeleteAfterDays | number | 0 | Days before auto-deletion (0 = disabled) | | logLevel | string | 'info' | Default log level for all services | | serviceLogs | object | {} | Per-service log level overrides |

Example: Multiple Services

export default defineNuxtConfig({
  dicom: {
    services: {
      storeScp: {
        name: 'primary_scp',
        port: 11112,
        outDir: './storage/primary'
      },
      storeScp2: {
        name: 'backup_scp',
        port: 11113,
        outDir: './storage/backup'
      }
    },
    logLevel: 'info',
    serviceLogs: {
      primary_scp: 'debug',  // Override for specific service
      backup_scp: 'warn'
    }
  }
})

Event Handlers

Available Events

| Event | When Triggered | Payload | |-------|---------------|---------| | OnFileStored | After each DICOM file is received | { file, tags, studyInstanceUid, seriesInstanceUid } | | OnStudyCompleted | When a study is complete | { studyInstanceUid, series[], totalInstances } | | OnServerStarted | When service starts | { name, port, callingAETitle } | | OnError | On service error | { error, context } |

Creating Event Handlers

Create TypeScript files in the server/dicom/ directory. File names can be anything (e.g., storeScp.onFileStored.ts), but the event ID passed to defineDicomEvent() must match the format {serviceName}_{eventType}.

Available event IDs:

  • storeScp_onFileStored - After each file is received
  • storeScp_onStudyCompleted - When a study is complete
  • storeScp_onServerStarted - When service starts
  • storeScp_onError - On service error

Handler signature:

defineDicomEvent(eventId, async (payload, context) => {
  // payload: Event-specific data
  // context: { logger, serviceName }
  //   - logger: Service-scoped logger (automatically includes service name)
  //   - serviceName: Name of the service that triggered this event
})

Basic example:

// server/dicom/storeScp.onFileStored.ts
import { defineDicomEvent } from '#imports'

export default defineDicomEvent('storeScp_onFileStored', async (payload, { logger }) => {
  // Logger is automatically scoped to the service - no need to pass service name!
  logger.info(`File received: ${payload.sopInstanceUid}`)
  
  // Access extracted tags
  const patientName = payload.tags?.PatientName
  const modality = payload.tags?.Modality
  
  logger.debug(`Patient: ${patientName}, Modality: ${modality}`)
})

Using logger methods:

export default defineDicomEvent('storeScp_onStudyCompleted', async (payload, { logger }) => {
  const fileCount = payload.series.reduce((sum, s) => 
    sum + s.instances.length, 0
  )
  
  // Log at different levels - service name is automatically included
  logger.info(`Study completed: ${fileCount} files`)
  logger.debug(`Study UID: ${payload.studyInstanceUid}`)
  logger.warn('Low file count', { fileCount, threshold: 10 })
  logger.error('Processing failed', { error: 'Disk full' })
  
  // Metadata objects are displayed as expandable JSON in the UI
  logger.info('Study details', {
    studyInstanceUid: payload.studyInstanceUid,
    seriesCount: payload.series.length,
    fileCount
  })
})

Payload Types

// OnFileStored payload
interface OnFileStoredPayload {
  file: string                    // Absolute file path
  tags?: Record<string, any>      // Extracted DICOM tags
  studyInstanceUid: string
  seriesInstanceUid: string
  sopInstanceUid: string
}

// OnStudyCompleted payload
interface OnStudyCompletedPayload {
  studyInstanceUid: string
  series: Array<{
    seriesInstanceUid: string
    instances: Array<{
      sopInstanceUid: string
      file: string
    }>
  }>
  totalInstances: number
}

File Management

Browse Files via UI

Navigate to /services/[serviceName]Files tab:

  • View files in hierarchical tree structure (Study → Series → Instances)
  • See file creation date and size
  • Download individual files
  • Delete files with confirmation
  • Cleanup old files with custom retention period

Programmatic Access

// In your server API route
import { useStorage } from '#imports'

export default defineEventHandler(async (event) => {
  const storage = useStorage('dicom:storeScp_1')
  
  // List all files
  const files = await storage.getKeys()
  
  // Get file metadata
  const meta = await storage.getMeta('study:series:instance.dcm')
  
  // Read file
  const buffer = await storage.getItemRaw('study:series:instance.dcm')
  
  return { files, meta }
})

File Cleanup

Automatic Cleanup

// nuxt.config.ts
export default defineNuxtConfig({
  dicom: {
    autoDeleteAfterDays: 30  // Delete files older than 30 days
  }
})

When enabled:

  • Initial cleanup runs 5 seconds after server start
  • Daily cleanup runs at midnight
  • Applies to all services
  • Logs deleted file count

Manual Cleanup via UI

  1. Go to service Files tab
  2. Click "Cleanup Old Files"
  3. Enter number of days
  4. Confirm deletion

Programmatic Cleanup

import { cleanupOldFiles } from '#imports'

// Cleanup specific service
const result = await cleanupOldFiles('storeScp_1', 30)

console.log(`Deleted ${result.deletedCount} files`)

Log Management

Configure Log Levels

// nuxt.config.ts
export default defineNuxtConfig({
  dicom: {
    logLevel: 'info',  // Global default
    serviceLogs: {
      storeScp_1: 'debug',  // Per-service override
      storeScp_2: 'warn'
    }
  }
})

Runtime Log Level Control

Log levels can be changed at runtime via:

  • UI dropdown in service Logs tab
  • API endpoint
// Change log level via API
await $fetch('/api/dicom/log-level', {
  method: 'POST',
  body: {
    serviceName: 'storeScp_1',
    level: 'debug'
  }
})

View Live Logs

Navigate to /services/[serviceName]Logs tab:

  • Real-time log streaming via WebSocket
  • Filter by log level
  • Download logs as text file
  • Clear log buffer

API Routes

The module provides these API endpoints:

| Method | Endpoint | Description | |--------|----------|-------------| | GET | /api/dicom/services | List all services | | GET | /api/dicom/services/:name | Get service details | | POST | /api/dicom/services/:name/start | Start a service | | POST | /api/dicom/services/:name/stop | Stop a service | | GET | /api/dicom/services/:name/files | List files in tree structure | | GET | /api/dicom/services/:name/files/download | Download a file | | DELETE | /api/dicom/services/:name/files/delete | Delete a file | | POST | /api/dicom/services/:name/cleanup | Cleanup old files | | GET | /api/dicom/log-level | Get current log levels | | POST | /api/dicom/log-level | Update log level |

Testing

Send Test DICOM Files

Using dcmtk tools:

# Install dcmtk
brew install dcmtk  # macOS
apt-get install dcmtk  # Ubuntu

# Send a file
storescu -aec STORESCP localhost 11112 test.dcm

# Send multiple files
storescu -aec STORESCP localhost 11112 /path/to/study/*

Using the included script:

# From the module directory
node scripts/sendData.mjs

Advanced Usage

Custom Storage Backend

By default, files are stored in the filesystem. You can configure different storage backends:

// Future support for S3, Azure Blob, etc.
export default defineNuxtConfig({
  dicom: {
    services: {
      storeScp: {
        storage: {
          type: 's3',
          bucket: 'my-dicom-bucket',
          region: 'us-east-1'
        }
      }
    }
  }
})

Tag Extraction

Extract specific DICOM tags for use in event handlers:

export default defineNuxtConfig({
  dicom: {
    services: {
      storeScp: {
        extractTags: [
          // Patient Information
          'PatientName',
          'PatientID',
          'PatientBirthDate',
          'PatientSex',
          'PatientAge',
          
          // Study Information
          'StudyDate',
          'StudyTime',
          'StudyDescription',
          'AccessionNumber',
          
          // Series Information
          'Modality',
          'SeriesDescription',
          'SeriesNumber',
          
          // Instance Information
          'InstanceNumber',
          'SOPInstanceUID'
        ]
      }
    }
  }
})

Troubleshooting

Service Won't Start

  • Check if port is already in use: lsof -i :[port]
  • Verify outDir has write permissions
  • Check logs in the UI for error details

Files Not Appearing

  • Ensure outDir is correctly configured
  • Check service is running (green status badge)
  • Verify sender is using correct AE Title
  • Check service logs for incoming connections

Events Not Firing

  • Verify event ID matches format: {serviceName}_{eventType} (e.g., 'storeScp_onFileStored')
  • Check handler exports with export default defineDicomEvent(...)
  • Ensure handler is in server/dicom/ directory
  • Restart dev server after creating new handlers
  • Check console for event registration messages

Development

# Install dependencies
npm install

# Generate type stubs
npm run dev:prepare

# Develop with playground
npm run dev

# Build
npm run dev:build

# Run tests
npm run test

# Lint
npm run lint

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

MIT