Skip to content

Conversation

@gitmotion
Copy link
Member

@gitmotion gitmotion commented Jul 15, 2025

Summary by CodeRabbit

  • New Features

    • Introduced a modular integration system supporting external document management integrations (e.g., Paperless NGX, Papra, Home Assistant).
    • Added settings UI for managing and configuring integrations, including dynamic schema-driven forms and connection testing.
    • Enabled linking, searching, and attaching external documents to assets and sub-assets via a new modal interface.
    • Integration badges now appear on asset file previews and in the UI.
    • Enhanced file duplication logic to support external files.
  • UI/Style

    • Added new stylesheets for integration settings, external document search modal, Home Assistant integration, demo mode, and responsive layouts.
    • Updated file preview and upload UI to support integration badges and improved styling.
  • Documentation

    • Added comprehensive guides and references for implementing and managing integrations, including schema conventions and development checklists.
  • Bug Fixes

    • Improved handling and validation of sensitive integration credentials and external file paths.
  • Chores

    • Refactored codebase to support new integration managers and modular architecture.
    • Updated server endpoints and static asset handling for integration support.

abiteman and others added 30 commits June 16, 2025 19:07
feat: Add Paperless NGX integration for document management

🔗 **Integration Features:**
- Added Paperless NGX configuration in Settings → System → Integrations
- Host URL, API token configuration with connection testing
- Real-time connection status with document count display

📋 **Document Search & Attachment:**
- New PaperlessManager class with modal search interface
- Real-time document search with autocomplete and debouncing
- "Search Paperless Documents" buttons in all attachment sections
- Documents appear alongside regular file attachments with "P" badge

🔧 **Backend API Endpoints:**
- `POST /api/paperless/test-connection` - Connection validation
- `GET /api/paperless/search` - Document search with pagination
- `GET /api/paperless/document/:id/download` - Authenticated document proxy
- `GET /api/paperless/document/:id/info` - Document metadata retrieval

🎨 **UI/UX Enhancements:**
- Paperless documents visually distinguished with blue "P" indicator
- "From Paperless NGX" source labels on attached documents
- Consistent styling with existing attachment system
- Modal search interface with loading states and error handling

⚡ **Technical Implementation:**
- Documents linked (not downloaded) - stay in Paperless storage
- Authenticated proxy for seamless user access (no login required)
- Web Streams API integration for efficient document streaming
- Configuration stored in config.json with default settings
- Full error handling and user feedback throughout

🔐 **Security & Authentication:**
- API token-based authentication with Paperless
- Server-side credential management (tokens not exposed to frontend)
- Secure document proxy prevents direct Paperless exposure

**Files Modified:**
- `public/index.html` - Added integration settings UI and search buttons
- `public/styles.css` - Paperless-specific styling and visual indicators
- `public/managers/settings.js` - Integration configuration management
- `public/managers/paperlessManager.js` - New document search and attachment logic
- `public/managers/modalManager.js` - Paperless document attachment support
- `public/script.js` - Event listener setup and manager initialization
- `server.js` - API endpoints, configuration, and document proxy streaming

**Usage:**
1. Configure Paperless in Settings → System → Integrations
2. Enter host URL and API token, test connection
3. Use "Search Paperless Documents" buttons when adding attachments
4. Search and attach documents directly from Paperless library
5. Click attached documents to download directly from Paperless

Resolves integration requirements for external document management system.
fix: resolve Paperless NGX compressed PDF download issue

- Remove Content-Encoding header forwarding in document proxy endpoint
- Skip Content-Length header when content is compressed (br/gzip)
- Let fetch API handle decompression automatically instead of double-decompression
- Add debug logging for response headers sent to browser
- Fixes issue where text PDFs (compressed) failed to download while scanned PDFs (uncompressed) worked
- Browser now receives properly decompressed content without compression headers

The root cause was forwarding Content-Encoding: br header while streaming already-decompressed content,
causing browser to attempt decompression again and corrupting the download.

Resolves: Paperless NGX integration document download for compressed content
…point

fix(security): prevent API token exposure to frontend in settings endpoint

CRITICAL SECURITY FIX: Paperless NGX API tokens were being exposed to the
frontend through the /api/settings endpoint, creating a serious security
vulnerability.

Changes:
- Backend: Sanitize GET /api/settings to replace real API tokens with placeholder
- Backend: Enhanced test connection endpoint to handle both saved and new tokens
- Backend: Smart settings save preserves existing tokens when placeholder received
- Frontend: Show placeholder for saved tokens instead of exposing real values
- Frontend: Improved UX with dynamic placeholders and token field handlers
- Integration: Updated isEnabled() method to recognize placeholder tokens

Security Impact:
- BEFORE: Real API tokens sent to frontend (HIGH RISK)
- AFTER: Only placeholder (*********************) sent to frontend (SECURE)

Features:
- Users can test connections without re-entering saved tokens
- Clear feedback when tokens are saved vs need to be entered
- Backward compatible with existing configurations
- Maintains full Paperless integration functionality

Files modified:
- server.js: API endpoints sanitization and token handling
- public/managers/settings.js: Frontend token management and UX
- src/integrations/paperless.js: Integration compatibility fix
- Add Paperless NGX icon badges to identify documents from Paperless integration
- Implement badge detection using isPaperlessDocument flag in file metadata
- Update setupExistingFilePreview to accept and process file info parameters
- Modify createPhotoPreview and createDocumentPreview to support badge rendering
- Pass file metadata through all modal manager setupExistingFilePreview calls
- Position badges in top-left corner (14px top, 8px left) of file preview items
- Style badges as clean 20x20px icons with drop-shadow for contrast
- Ensure badge persistence when reopening edit modals for existing assets
- Support badges in both asset and sub-asset file previews across all types
- Maintain consistent badge appearance in modal and asset detail views

Paperless documents are now clearly identified with the Paperless NGX logo
badge, providing immediate visual recognition of document source across
all file preview contexts in the application.

Files modified:
- public/styles.css: Badge styling and positioning
- src/services/render/previewRenderer.js: Badge rendering logic
- public/managers/modalManager.js: File info parameter passing
…manager class, backend paperless integration code to its own integrations file which contains everything it needs (schema, functions, endpoints)
…ment linking system

BREAKING CHANGES:
- Replace "Search Paperless Documents" buttons with "Link External Docs" across all modals
- Modal now supports multi-document linking without closing

NEW FEATURES:
- Multi-integration external document search modal with extensible architecture
- Immediate document population on modal open with pagination (15 docs/page)
- Support for linking multiple documents in single session
- Integration filter system (All Sources, Paperless NGX, etc.)
- Real-time search with 300ms debouncing
- Visual feedback for successfully linked documents

BACKEND CHANGES:
- Add /api/integrations/enabled endpoint to return active integrations
- Enhance integration system to support frontend filtering

FRONTEND ARCHITECTURE:
- Create ExternalDocManager class for centralized document search/linking
- Enhance ModalManager with attachExternalDocument() method for forward compatibility
- Maintain backward compatibility with existing attachPaperlessDocument()

UI/UX IMPROVEMENTS:
- Compact, centered modal design (60vh max-height, 700px max-width)
- Immediate document loading instead of empty search state
- Pagination controls with document count display
- Button state management (Link → Linked with checkmark and green styling)
- Modal subtitle explaining multi-document linking capability
- Success toast notifications with shortened display time
- Loading states and error handling throughout

TECHNICAL DETAILS:
- Extensible integration architecture for future additions (Nextcloud, SharePoint, etc.)
- CSS custom properties for consistent theming
- Responsive design with proper viewport centering
- Debounced search to prevent excessive API calls
- Proper state management for pagination and search queries

FILES MODIFIED:
- public/index.html: Update button text, add external doc modal structure
- public/styles.css: Add complete styling for external doc system
- public/script.js: Implement ExternalDocManager class with pagination
- public/managers/modalManager.js: Add attachExternalDocument method
- server.js: Add /api/integrations/enabled endpoint

This change transforms a Paperless-specific feature into a generic, extensible
multi-integration document linking system while maintaining full backward
compatibility and significantly improving user experience.
…integrations' registerRoute() functions from server.js, update cursor/copilot rules, and add new endpoint to constants.js
Modified openAssetModal() method
Before: For new assets, this.currentAsset was set to null
After: For new assets, this.currentAsset is set to a temporary object with pre-initialized arrays for external document attachments
Modified openSubAssetModal() method
Applied the same fix for sub-assets to ensure consistency
3. Updated collectAssetFormData() and collectSubAssetFormData() methods
Before: New assets would get empty arrays, ignoring any external document attachments
After: New assets preserve the external document attachments from the temporary object

Now when adding a new asset:
✅ The modal opens with a temporary asset object that can store external document attachments
✅ External documents can be linked without errors
✅ When the form is submitted, the external document attachments are preserved and included in the final asset data
✅ The functionality works identically for both new assets and existing assets
Include extension filters so users only see jpg, png etc for photos and so on.
Added Papra as a search source

Seems to populate properly in the system settings and search modal. Do not have Papra instance running to test atm.
…- Fix hardcoded Paperless dependency causing errors when only Papra enabled - Add searchAllIntegrations() method to route to appropriate integration - Add searchPapra() method and dynamic source display names - Add comprehensive INTEGRATION.md guide with templates and examples
…er mismatch between frontend and Papra backend (searchQuery vs q) - Correct Papra pagination conversion from 1-based to 0-based - Remove debug logging statements - Ensure proper integration routing in modular search system
abiteman added 3 commits July 15, 2025 23:06
Extended createDocumentPreview to handle 'image' type, displaying an appropriate icon. Added logic in setupExistingFilePreview to use the image icon for Paperless image files, preventing broken image links. This improves the user experience when previewing image documents from Paperless integrations.
}

try {
return await this.testConnection(config);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invalid use of 'this' in a static context will cause a runtime error. Inside the static SCHEMA object's statusCheck method, 'this.testConnection' will be undefined because 'this' doesn't refer to the class in static field initializers. To fix this, change to 'PaperlessIntegration.testConnection(config)' to explicitly reference the static method through the class name.

📚 Relevant Docs

Suggested change
return await this.testConnection(config);
return await PaperlessIntegration.testConnection(config);

React with 👍 to tell me that this comment was useful, or 👎 if not (and I'll stop posting more comments like this in the future)

}

try {
return await this.testConnection(config);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using 'this' keyword in a static method to reference another static method will cause a runtime error. Static methods should be accessed through the class name instead. The line should be changed to 'return await PapraIntegration.testConnection(config);'

📚 Relevant Docs

Suggested change
return await this.testConnection(config);
return await PapraIntegration.testConnection(config);

React with 👍 to tell me that this comment was useful, or 👎 if not (and I'll stop posting more comments like this in the future)

documentCount: data.documentsCount || 0
};
} catch (error) {
console.error('Papra connection test failed:', error);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The testConnection method catches errors but doesn't rethrow them, which will result in undefined being returned silently instead of propagating the error. This breaks the error handling contract of the method and will cause silent failures. The catch block should rethrow the error after logging.

Suggested change
console.error('Papra connection test failed:', error);
console.error('Papra connection test failed:', error);
throw error;

React with 👍 to tell me that this comment was useful, or 👎 if not (and I'll stop posting more comments like this in the future)

this.buttonIds.forEach(buttonId => {
const button = document.getElementById(buttonId);
if (button) {
button.removeEventListener('click', (e) => this.handleLinkExternalDocs(e, buttonId));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Memory leak due to incorrect event listener removal. The code attempts to remove an event listener using an anonymous arrow function, but this creates a new function reference that doesn't match the original listener. Each time loadActiveIntegrations() is called, a new listener will be added without removing the old one. To fix, store the event listener function as a class property or use a bound method.

📚 Relevant Docs


React with 👍 to tell me that this comment was useful, or 👎 if not (and I'll stop posting more comments like this in the future)

@recurseml
Copy link

recurseml bot commented Jul 17, 2025

😱 Found 4 issues. Time to roll up your sleeves! 😱

⚠️ Only 5 files were analyzed due to processing limits.

Need help? Join our Discord for support!
https://discord.gg/qEjHQk64Z9

- Fix invalid use of 'this' in static context in paperless.js and papra.js statusCheck methods
- Fix error handling in papra.js testConnection to properly rethrow errors instead of returning undefined
- Fix memory leak in externalDocManager.js by storing event handler references for proper removal

Resolves code review feedback from PR #112 discussion
@coderabbitai
Copy link

coderabbitai bot commented Jul 17, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

This update introduces a comprehensive, schema-driven integration system to the application, affecting both backend and frontend. It adds an Integration Manager for centralized integration handling, new integration modules (Paperless NGX, Papra, Home Assistant), dynamic UI generation for integration settings, external document linking in asset management, and extensive new CSS for responsive and integration-specific styling. Documentation is added to guide integration development.

Changes

File(s) Change Summary
.github/copilot-instructions.md, integrations/INTEGRATION.md Added detailed documentation for the new schema-driven integration framework, including backend and frontend conventions, schema reference, error handling, and development checklist.
integrations/homeassistant.js, integrations/paperless.js, integrations/papra.js Added new integration modules for Home Assistant, Paperless NGX, and Papra, each defining a schema, configuration validation, API interaction methods, and Express route registration for integration-specific endpoints.
integrations/integrationManager.js Introduced an Integration Manager class to register, manage, validate, and expose integrations, handling sensitive data masking, config validation, and route registration.
public/assets/css/styles.css, public/assets/css/demo-styles.css, Added and updated CSS for integration settings UI, file type hints, and removed demo mode styles.
public/assets/css/homeassistant-styles.css, public/assets/css/integration-styles.css, Added new CSS files for Home Assistant and general integration UI, including modal, buttons, filter controls, results, badges, and responsive design.
public/assets/css/responsive-styles.css Added responsive CSS rules for various breakpoints to optimize layout and usability across devices.
public/index.html, public/login.html Updated HTML to use new CSS files, added external document linking buttons, a modal for external doc search, and an integrations section in settings.
public/managers/externalDocManager.js Added a manager for searching and linking external documents from integrations, supporting dynamic integration filters, debounced search, pagination, and UI updates.
public/managers/integrations.js Added a manager for loading, rendering, and managing integration settings in the UI, supporting schema-driven field generation, toggling, and connection testing.
public/managers/modalManager.js Enhanced modal manager to support attaching, previewing, and removing external documents from integrations for assets and sub-assets, including badge rendering.
public/managers/settings.js Updated settings manager to load and save integration settings dynamically, applying them to the form and reloading active integrations after save.
public/script.js Refactored main initialization to instantiate and inject new managers (IntegrationsManager, ExternalDocManager) into relevant modules, enabling integration features throughout the app.
server.js Added integration management to server: new API endpoints for integrations, configuration validation, secure token handling, registration of integration routes, and enhanced file duplication logic for external files.
src/constants.js Introduced a constants file for integration categories, statuses, field types, token masking, and API endpoints.
src/services/render/assetRenderer.js Added support for rendering integration badges on asset file previews by injecting integrationsManager and updating file grid HTML generation.
src/services/render/previewRenderer.js, src/services/render/index.js Enhanced preview rendering to display integration badges, handle integration-specific previews, and inject integrationsManager. Added and exported an initialization function for preview rendering.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Frontend
    participant IntegrationsManager (Frontend)
    participant ExternalDocManager
    participant ModalManager
    participant Backend
    participant IntegrationManager (Backend)
    participant IntegrationModule

    User->>Frontend: Opens Settings or clicks "Link External"
    Frontend->>IntegrationsManager (Frontend): Load integrations (GET /api/integrations)
    IntegrationsManager (Frontend)->>Backend: GET /api/integrations
    Backend->>IntegrationManager (Backend): getIntegrationsForSettings()
    IntegrationManager (Backend)-->>Backend: Return schemas (with sensitive fields masked)
    Backend-->>IntegrationsManager (Frontend): Integration schemas
    IntegrationsManager (Frontend)-->>Frontend: Render dynamic integration UI

    User->>Frontend: Clicks "Test Connection"
    Frontend->>Backend: POST /api/integrations/:id/test (with config)
    Backend->>IntegrationManager (Backend): prepareConfigForTesting()
    IntegrationManager (Backend)->>IntegrationModule: testConnection()
    IntegrationModule-->>IntegrationManager (Backend): Status/result
    IntegrationManager (Backend)-->>Backend: Status/result
    Backend-->>Frontend: Success/error response

    User->>Frontend: Clicks "Link External" for file attachment
    Frontend->>ExternalDocManager: Open modal, load integrations (GET /api/integrations/enabled)
    ExternalDocManager->>Backend: GET /api/integrations/enabled
    Backend->>IntegrationManager (Backend): getEnabledIntegrations()
    IntegrationManager (Backend)-->>Backend: Enabled integrations
    Backend-->>ExternalDocManager: Enabled integrations
    ExternalDocManager-->>Frontend: Render modal with filters

    User->>ExternalDocManager: Search or browse documents
    ExternalDocManager->>Backend: GET /api/{integration}/search
    Backend->>IntegrationModule: searchDocuments()
    IntegrationModule-->>Backend: Search results
    Backend-->>ExternalDocManager: Results
    ExternalDocManager-->>Frontend: Display results

    User->>ExternalDocManager: Clicks "Link" on a document
    ExternalDocManager->>ModalManager: Attach external document
    ModalManager-->>Frontend: Show preview with integration badge
Loading

Poem

🐇
A hop, a leap, integrations bloom,
With badges bright, they chase the gloom.
Paperless, Papra, Home Assistant too,
Linking docs and settings—something new!
Modals shimmer, badges gleam,
Schema-driven magic, a rabbit’s dream.
Hooray for code that hops so far—
Now every system’s not so far!

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch integration-manager-with-paperless

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@abiteman
Copy link
Contributor

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jul 17, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

coderabbitai bot added a commit that referenced this pull request Jul 17, 2025
Docstrings generation was requested by @abiteman.

* #112 (comment)

The following files were modified:

* `public/script.js`
* `server.js`
* `src/services/render/assetRenderer.js`
* `src/services/render/previewRenderer.js`
@coderabbitai
Copy link

coderabbitai bot commented Jul 17, 2025

Note

Generated docstrings for this pull request at #114

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

♻️ Duplicate comments (4)
integrations/papra.js (2)

124-124: Fix usage of 'this' in static methods.

Using 'this' in static methods should be replaced with the class name 'PapraIntegration' to avoid confusion and potential runtime errors.

- return await this.testConnection(config);
+ return await PapraIntegration.testConnection(config);

- const data = await this.getDocuments(config, 0, 1);
+ const data = await PapraIntegration.getDocuments(config, 0, 1);

- const data = await this.getDocuments(config, page, pageSize);
+ const data = await PapraIntegration.getDocuments(config, page, pageSize);

- const result = await this.testConnection({ hostUrl, organizationId, apiToken });
+ const result = await PapraIntegration.testConnection({ hostUrl, organizationId, apiToken });

- const results = await this.searchDocuments(
+ const results = await PapraIntegration.searchDocuments(

- const info = await this.getDocumentInfo(config, documentId);
+ const info = await PapraIntegration.getDocumentInfo(config, documentId);

- const response = await this.downloadDocument(config, documentId);
+ const response = await PapraIntegration.downloadDocument(config, documentId);

- const response = await this.downloadDocument(config, documentId);
+ const response = await PapraIntegration.downloadDocument(config, documentId);

Also applies to: 149-149, 243-243, 386-386, 403-403, 423-423, 437-437, 463-463


155-158: Rethrow error after logging in testConnection method.

The catch block should rethrow the error after logging to maintain proper error propagation.

 } catch (error) {
   console.error('Papra connection test failed:', error);
+  throw error;
 }
public/managers/externalDocManager.js (1)

119-164: Excellent implementation of event listener cleanup!

The use of a Map to store event handlers and the proper cleanup in loadActiveIntegrations effectively prevents memory leaks. This addresses the concern raised in the previous review about incorrect event listener removal.

integrations/paperless.js (1)

111-111: Fix invalid use of 'this' in static context.

The use of this.testConnection in a static context will cause a runtime error. This issue was previously identified but not fixed.

Apply this diff to fix the issue:

-return await this.testConnection(config);
+return await PaperlessIntegration.testConnection(config);
🧹 Nitpick comments (21)
public/login.html (1)

8-10: Verify new stylesheet paths & consider load-order/performance

The path prefix was changed from styles.css (root) to assets/css/*.css.

  1. Make sure assets/css/styles.css actually exists in the build output; otherwise the login page will lose all core styles.
  2. You now load three separate CSS files. For first-paint performance you may want to:
    • keep critical rules in a single file,
    • or rel="preload" the heaviest sheet.
  3. If responsive-styles.css or demo-styles.css re-declare rules already in styles.css, double-check the intended cascade since later files win.
src/services/render/index.js (1)

74-75: Keep export list alphabetised / grouped

Minor nit: to reduce merge conflicts, keep the public export list in the same order as the import list (or alphabetical). initPreviewRenderer is appended after setupExistingFilePreview, which is fine now but becomes a hotspot in large files.

public/assets/css/demo-styles.css (1)

46-56: Use transform opacity combo for GPU-friendly animation

opacity alone is fine, but pairing it with will-change: opacity (or using transform-based fade) can keep it on the compositor layer and avoid repaint jank.

 .demo-banner {
   ...
+  will-change: opacity;
 }
public/assets/css/homeassistant-styles.css (2)

7-12: Namespace custom CSS variables to avoid global collision

Placing Home-Assistant colours on :root means they leak into every page/theme.
Either scope them:

.integration-card[data-integration='homeassistant'] {
  --ha-primary: #41BDF5;
  /* … */
}

or move them under [data-theme] blocks to prevent accidental overrides elsewhere.


154-170: Spinner animation could respect reduced-motion & reuse global keyframe

You duplicate a generic spin keyframe that may already exist in styles.css. Re-use the existing one or define it once globally to cut CSS size.
Also gate it behind prefers-reduced-motion like:

@media (prefers-reduced-motion: no-preference) {
  .homeassistant-loading::before { animation: spin 1s linear infinite; }
}
public/assets/css/responsive-styles.css (1)

344-347: Hard-coded input size may break with larger default fonts

Setting pixel-perfect width/height on .pin-input ignores the root font size. Consider em or rem so the box scales with user zoom:

-width: 32px;
-height: 32px;
+width: 2rem;
+height: 2rem;

[accessibility]

public/assets/css/integration-styles.css (1)

62-62: Remove commented-out CSS properties.

Clean up the commented-out styles to maintain code clarity:

  • Line 62: /* border: 1px solid var(--border-color); */
  • Lines 385-386: /* object-fit: contain; filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5)); */
 .external-doc-search-input {
   width: 100%;
   padding: 0.5rem;
-  /* border: 1px solid var(--border-color); */
   border-radius: 0.25rem;
   background: var(--container);

 .integration-badge img {
   width: 20px;
   height: 20px;
-  /* object-fit: contain;
-  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5)); */
 }

Also applies to: 385-386

src/services/render/assetRenderer.js (1)

282-284: Consider consolidating integration badge logic.

The pattern of checking for integrationId and generating badges is repeated multiple times throughout the file. Consider extracting this into a helper function to reduce duplication.

+function getFileIntegrationData(fileInfo) {
+    const integrationId = fileInfo?.integrationId;
+    return {
+        integrationClass: integrationId ? ` ${integrationId}-document` : '',
+        integrationBadge: integrationId ? getIntegrationBadge(integrationId) : ''
+    };
+}

 // Then use it like:
-const integrationClass = photoInfo.integrationId ? ` ${photoInfo.integrationId}-document` : '';
-const integrationBadge = photoInfo.integrationId ? getIntegrationBadge(photoInfo.integrationId) : '';
+const { integrationClass, integrationBadge } = getFileIntegrationData(photoInfo);

Also applies to: 286-289

src/constants.js (1)

1-31: Consider freezing/typing the public constants

These objects are effectively enums. Calling Object.freeze (or using /** @readonly */ JSDoc / TS as const) prevents accidental mutation and gives editors intellisense for the literal string values.

-export const INTEGRATION_STATUS = {
+export const INTEGRATION_STATUS = /** @readonly */ Object.freeze({
   CONNECTED: 'connected',
   …
-};
+});
public/assets/css/styles.css (2)

992-1000: DRY up the repeated “exclude integration-badge” image rules

The same selector pattern (img:not(.integration-badge img)) appears four times with only max-size differences. This balloons the file and risks future inconsistencies.

Create a single utility class or compose selectors:

/* once, near the top of the helpers section */
img:not(.integration-badge img) {
  border-radius: var(--app-border-radius);
}

/* Scoped size variants */
.preview-container img:not(.integration-badge img) { … }
.file-preview img:not(.integration-badge img)      { … }
.preview-grid   img:not(.integration-badge img)    { … }
#photoPreview   img:not(.integration-badge img),
#subPhotoPreview img:not(.integration-badge img)   { … }

This removes duplication while retaining the different size constraints.

Also applies to: 1864-1868, 1995-1999, 2271-2276


1323-1329: Missing responsive behaviour on .file-upload-header

The new flex header aligns items horizontally but doesn’t wrap.
On narrow screens the file name can overflow. Consider flex-wrap: wrap; or a media query that converts to column layout below ~480 px.

public/index.html (2)

583-584: Move inline styles to CSS files.

Inline styles should be avoided in favor of CSS classes for better maintainability and consistency.

Move these styles to integration-styles.css:

-<div id="externalDocModal" class="modal" style="display: none;">
-    <div class="modal-content" style="max-width: 900px;">
+<div id="externalDocModal" class="modal external-doc-modal">
+    <div class="modal-content external-doc-modal-content">

Add to integration-styles.css:

.external-doc-modal {
    display: none;
}

.external-doc-modal-content {
    max-width: 900px;
}

591-593: Remove commented out code.

If this subtitle is not needed, the commented code should be removed to keep the codebase clean.

-<!-- <div class="modal-subtitle">
-    <p>You can link multiple documents - the modal will stay open until you close it.</p>
-</div> -->
integrations/integrationManager.js (1)

20-33: Add error handling for integration registration.

Consider wrapping each registration in a try-catch to prevent a single malformed integration from breaking the entire system.

 registerBuiltInIntegrations() {
-    // Register Paperless NGX integration
-    this.registerIntegration('paperless', PaperlessIntegration.SCHEMA);
-
-    // Register Papra integration
-    this.registerIntegration('papra', PapraIntegration.SCHEMA);
-
-    // Register Home Assistant integration
-    this.registerIntegration('homeassistant', HomeAssistantIntegration.SCHEMA);
+    const integrations = [
+        { id: 'paperless', schema: PaperlessIntegration.SCHEMA },
+        { id: 'papra', schema: PapraIntegration.SCHEMA },
+        { id: 'homeassistant', schema: HomeAssistantIntegration.SCHEMA }
+    ];
+
+    for (const { id, schema } of integrations) {
+        try {
+            this.registerIntegration(id, schema);
+        } catch (error) {
+            console.error(`Failed to register ${id} integration:`, error);
+        }
+    }
 
     // Future integrations can be added here
public/managers/integrations.js (1)

132-132: Use optional chaining for cleaner code.

The static analysis correctly identifies opportunities to use optional chaining.

For line 132:

-if (integration && integration.logoHref) {
+if (integration?.logoHref) {

For lines 437-439:

-return integration.endpoints && integration.endpoints.some(endpoint => 
-    endpoint.includes('test-connection') || endpoint.includes('/test')
-);
+return integration.endpoints?.some(endpoint => 
+    endpoint.includes('test-connection') || endpoint.includes('/test')
+) ?? false;

Also applies to: 437-439

src/services/render/previewRenderer.js (1)

253-260: Document the Paperless special case and consider a more scalable approach.

The special handling for Paperless images should be better documented, and consider a more scalable approach for integration-specific preview logic.

Add a comment explaining why this is necessary:

 // Special handling for Paperless images - show image icon instead of trying to load actual image
+// This is required because Paperless image URLs may not be directly accessible due to 
+// authentication requirements or CORS policies. Using a document preview with an image
+// icon provides a better user experience than showing broken image links.
 const isPaperlessImage = type === 'photo' && 
                         integrationId === 'paperless' && 
                         fileInfo.mimeType && 
                         fileInfo.mimeType.startsWith('image/');

For future scalability, consider moving integration-specific preview logic to the integration configuration:

// In integration schema
previewConfig: {
    imagePreviewMode: 'icon' // or 'direct', 'proxy', etc.
}
integrations/INTEGRATION.md (1)

261-261: Add language specification to fenced code block.

The fenced code block showing the directory structure should specify a language for better markdown compliance.

-```
+```text
 /public/assets/integrations/your-integration/
 ├── icon.png

</blockquote></details>
<details>
<summary>public/managers/externalDocManager.js (2)</summary><blockquote>

`188-192`: **Consider using optional chaining for cleaner code.**

The null check can be simplified using optional chaining.

Apply this diff to use optional chaining:

```diff
-const integrationData = this.integrationsManager.getIntegration(integration.id);
-if (integrationData && integrationData.colorScheme) {
-    btn.style.backgroundColor = integrationData.colorScheme;
-}
+const integrationData = this.integrationsManager.getIntegration(integration.id);
+if (integrationData?.colorScheme) {
+    btn.style.backgroundColor = integrationData.colorScheme;
+}

524-526: Use optional chaining for query parameter validation.

Simplify the query parameter checks using optional chaining.

For line 524-526, apply this diff:

-if (query && query.trim()) {
+if (query?.trim()) {
     params.append('q', query.trim());
 }

For line 569-571, apply the same pattern:

-if (query && query.trim()) {
+if (query?.trim()) {
     params.append('q', query.trim());
 }

Also applies to: 569-571

integrations/paperless.js (1)

8-463: Consider refactoring to use regular functions instead of a class with only static members.

The entire class contains only static members, which is an anti-pattern in JavaScript. Since this appears to be a consistent pattern across all integrations, consider refactoring the integration system to use module exports with regular functions instead.

This would make the code more idiomatic and avoid the confusion with this in static contexts. For example:

// Instead of class with static members
module.exports = {
  SCHEMA: { /* schema definition */ },
  testConnection,
  searchDocuments,
  getDocumentInfo,
  downloadDocument,
  registerRoutes
};
server.js (1)

952-976: Use optional chaining for cleaner code

The static analysis correctly identifies opportunities to use optional chaining for more concise and readable code.

Apply optional chaining in the file duplication logic:

-const infoVal = (source.photoInfo && source.photoInfo[i]) ? source.photoInfo[i] : null;
+const infoVal = source.photoInfo?.[i] ?? null;
-if (source.photoPath && !source.photoPaths) {
-    if (isExternalFile(source.photoPath, source.photoInfo && source.photoInfo[0])) {
+if (source.photoPath && !source.photoPaths) {
+    if (isExternalFile(source.photoPath, source.photoInfo?.[0])) {
-duplicate.photoInfo = [{ ...(source.photoInfo && source.photoInfo[0]), fileName: newPath.split('/').pop() }];
+duplicate.photoInfo = [{ ...source.photoInfo?.[0], fileName: newPath.split('/').pop() }];

Apply similar changes to the receipt and manual sections as well.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7c30fc4 and 9c2b607.

⛔ Files ignored due to path filters (4)
  • package-lock.json is excluded by !**/package-lock.json
  • public/assets/integrations/homeassistant/homeassistant.png is excluded by !**/*.png
  • public/assets/integrations/paperless/paperless-ngx.png is excluded by !**/*.png
  • public/assets/integrations/papra/papra.png is excluded by !**/*.png
📒 Files selected for processing (24)
  • .cursorrules (1 hunks)
  • .github/copilot-instructions.md (20 hunks)
  • integrations/INTEGRATION.md (1 hunks)
  • integrations/homeassistant.js (1 hunks)
  • integrations/integrationManager.js (1 hunks)
  • integrations/paperless.js (1 hunks)
  • integrations/papra.js (1 hunks)
  • public/assets/css/demo-styles.css (1 hunks)
  • public/assets/css/homeassistant-styles.css (1 hunks)
  • public/assets/css/integration-styles.css (1 hunks)
  • public/assets/css/responsive-styles.css (1 hunks)
  • public/assets/css/styles.css (12 hunks)
  • public/index.html (9 hunks)
  • public/login.html (1 hunks)
  • public/managers/externalDocManager.js (1 hunks)
  • public/managers/integrations.js (1 hunks)
  • public/managers/modalManager.js (19 hunks)
  • public/managers/settings.js (6 hunks)
  • public/script.js (7 hunks)
  • server.js (9 hunks)
  • src/constants.js (1 hunks)
  • src/services/render/assetRenderer.js (11 hunks)
  • src/services/render/index.js (2 hunks)
  • src/services/render/previewRenderer.js (6 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (5)
public/managers/modalManager.js (3)
src/services/render/previewRenderer.js (8)
  • integrationsManager (7-7)
  • previewElement (173-173)
  • previewElement (247-247)
  • previewItem (17-17)
  • previewItem (65-65)
  • integrationBadge (25-26)
  • integrationBadge (115-116)
  • integrationId (250-250)
src/services/render/assetRenderer.js (7)
  • integrationsManager (37-37)
  • photoInfo (303-303)
  • receiptInfo (340-340)
  • manualInfo (383-383)
  • integrationBadge (306-306)
  • integrationBadge (343-343)
  • integrationBadge (386-386)
public/script.js (1)
  • integrationsManager (124-124)
integrations/homeassistant.js (3)
integrations/integrationManager.js (2)
  • require (6-6)
  • HomeAssistantIntegration (9-9)
src/constants.js (6)
  • API_HOMEASSISTANT_ENDPOINT (38-38)
  • API_HOMEASSISTANT_ENDPOINT (38-38)
  • API_TEST_SUCCESS (33-33)
  • API_TEST_SUCCESS (33-33)
  • TOKENMASK (2-2)
  • TOKENMASK (2-2)
server.js (1)
  • app (30-30)
src/services/render/previewRenderer.js (2)
src/services/render/assetRenderer.js (7)
  • integrationsManager (37-37)
  • fileName (304-304)
  • fileName (341-341)
  • fileName (384-384)
  • integrationBadge (306-306)
  • integrationBadge (343-343)
  • integrationBadge (386-386)
public/script.js (1)
  • integrationsManager (124-124)
public/managers/externalDocManager.js (5)
public/script.js (3)
  • modalManager (120-120)
  • integrationsManager (124-124)
  • searchInput (78-78)
src/services/render/previewRenderer.js (2)
  • integrationsManager (7-7)
  • integrationId (250-250)
src/services/render/assetRenderer.js (4)
  • integrationsManager (37-37)
  • searchInput (22-22)
  • lastDotIndex (254-254)
  • extension (260-260)
src/constants.js (6)
  • INTEGRATION_CATEGORIES (5-11)
  • INTEGRATION_CATEGORIES (5-11)
  • API_PAPERLESS_ENDPOINT (36-36)
  • API_PAPERLESS_ENDPOINT (36-36)
  • API_PAPRA_ENDPOINT (37-37)
  • API_PAPRA_ENDPOINT (37-37)
src/services/fileUpload/fileUploader.js (1)
  • responseValidation (63-63)
src/services/render/assetRenderer.js (2)
src/services/render/previewRenderer.js (4)
  • integrationsManager (7-7)
  • integrationId (250-250)
  • integrationBadge (25-26)
  • integrationBadge (115-116)
public/script.js (1)
  • integrationsManager (124-124)
🪛 Biome (1.9.4)
public/managers/modalManager.js

[error] 1180-1180: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

integrations/homeassistant.js

[error] 7-548: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)


[error] 425-425: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 447-447: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 467-467: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 484-484: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 510-510: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 516-516: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)

public/managers/integrations.js

[error] 132-132: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 437-439: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

integrations/paperless.js

[error] 7-463: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)


[error] 354-354: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 371-371: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 391-391: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 405-405: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 431-431: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)

integrations/papra.js

[error] 7-495: Avoid classes that contain only static members.

Prefer using simple functions instead of classes with only static members.

(lint/complexity/noStaticOnlyClass)


[error] 149-149: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 243-243: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 386-386: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 403-403: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 423-423: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 437-437: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 463-463: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)

public/managers/externalDocManager.js

[error] 189-189: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 524-524: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 569-569: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

server.js

[error] 941-941: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 952-952: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 969-969: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 976-976: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 987-987: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 1001-1001: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 1008-1008: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 1019-1019: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 1033-1033: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


[error] 1040-1040: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

src/services/render/assetRenderer.js

[error] 507-507: expected : but instead the file ends

the file ends here

(parse)

🪛 markdownlint-cli2 (0.17.2)
integrations/INTEGRATION.md

261-261: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (43)
src/services/render/index.js (1)

48-50: Verified: initPreviewRenderer is correctly exported
The import in src/services/render/index.js is valid—initPreviewRenderer is exported at line 286 of src/services/render/previewRenderer.js.

public/assets/css/demo-styles.css (1)

2-18: Respect prefers-reduced-motion for the pulse animation

The banner’s perpetual pulse can trigger motion-sensitivity issues. Wrap the animation in a media query so users who opt-out are respected.

-.demo-banner {
+@media (prefers-reduced-motion: no-preference) {
+  .demo-banner {
     animation: pulse 2s infinite;
   }
+}

[accessibility]

Likely an incorrect or invalid review comment.

.github/copilot-instructions.md (2)

2-6: LGTM! Clear documentation introduction.

The added introduction clearly defines the document's purpose and goals, making it easier for developers to understand the context.


351-411: Well-structured integration system documentation.

The new Integration System section provides comprehensive guidance for adding integrations with:

  • Clear step-by-step instructions for backend setup
  • Automatic frontend integration details
  • Schema reference with supported field types
  • Error handling guidelines
  • Development checklist
  • Key conventions

This documentation aligns well with the integration framework implementation across the codebase.

.cursorrules (1)

328-380: Documentation consistency maintained across tools.

The Integration System documentation has been properly synchronized with the copilot-instructions.md file, ensuring developers using either Copilot or Cursor have access to the same integration guidelines.

public/assets/css/integration-styles.css (1)

1-388: Well-structured CSS for integration components.

The stylesheet provides comprehensive styling for the new integration UI features with:

  • Consistent use of CSS custom properties for theming
  • Proper hover states and transitions
  • Responsive design considerations
  • Clear class naming conventions
  • Good organization by component type
public/managers/settings.js (1)

218-224: Integration settings properly integrated into the settings workflow.

The implementation correctly:

  • Loads integration schemas dynamically
  • Applies existing integration settings to the form
  • Collects integration settings on save
  • Reloads active integrations after saving

Also applies to: 277-279, 305-305

src/services/render/assetRenderer.js (1)

44-46: Good defensive programming in getIntegrationBadge.

The function properly handles the case where integrationsManager might not be available by providing a fallback generic badge.

public/index.html (1)

267-277: Excellent consistency in file upload header implementation!

The file upload headers are consistently structured across all attachment types (photos, receipts, manuals) for both assets and sub-assets, with appropriate unique IDs and consistent SVG icons.

Also applies to: 294-304, 320-330, 480-490, 507-517, 533-543

integrations/integrationManager.js (1)

184-186: Excellent implementation of sensitive data handling!

The three-part approach properly secures sensitive fields:

  1. Masking with TOKENMASK when exposing to frontend
  2. Preserving stored values when TOKENMASK is received
  3. Retrieving actual values only when needed for testing

This pattern effectively prevents sensitive data exposure while maintaining functionality.

Also applies to: 233-240, 279-294

public/managers/integrations.js (1)

340-431: Excellent schema-driven UI generation!

The dynamic field rendering based on schema is well-implemented:

  • Properly handles different field types (password, url, boolean, text)
  • Supports field dependencies
  • Includes helpful descriptions and placeholders
  • Extensible design allows adding new integrations without frontend changes

This approach promotes maintainability and consistency across integrations.

src/services/render/previewRenderer.js (1)

25-27: Well-implemented integration badge support!

The integration badges are consistently added across all preview types with:

  • Optional parameters maintaining backward compatibility
  • Proper fallback handling when integrationsManager is unavailable
  • Clean integration into the existing HTML structure

Also applies to: 34-34, 115-117, 124-124, 250-250, 278-280

public/script.js (7)

50-51: LGTM!

The new manager imports follow the established pattern and naming conventions.


123-124: LGTM!

The manager variable declarations are consistent with the existing pattern.


129-150: LGTM!

The async initialization properly handles early integration loading with appropriate error handling that doesn't block application startup.


232-235: LGTM!

Passing integrationsManager to the renderer follows the established pattern and enables integration features in the UI.


321-325: LGTM!

The ModalManager initialization properly includes the integrations manager.


327-332: LGTM!

The ExternalDocManager is properly initialized with all required dependencies.


346-347: LGTM!

The SettingsManager integration is properly configured with the callback and manager reference.

integrations/homeassistant.js (4)

6-8: LGTM!

The imports and class declaration follow the integration framework pattern.


136-180: LGTM!

The connection test implementation has robust error handling and appropriate timeout management.


185-312: LGTM!

The data fetching methods are well-implemented with consistent error handling and appropriate timeouts.


317-369: LGTM!

The entity-to-asset conversion provides sensible categorization and preserves important metadata.

public/managers/modalManager.js (5)

50-54: LGTM! Clean integration of the integrations manager.

The addition of integrationsManager to the constructor follows the existing pattern and is properly stored as an instance property.

Also applies to: 101-102


137-148: Good approach for handling external documents in create mode.

Creating temporary objects with the necessary arrays allows external documents to be attached before the asset is saved. The structure is consistent between assets and sub-assets.

Also applies to: 238-249


564-565: Consistent enhancement to file preview setup.

Passing the full file info object to setupExistingFilePreview enables integration badge display and provides access to integration metadata. The changes are consistently applied across all file types.

Also applies to: 580-581, 598-599, 614-615, 632-633, 648-649, 683-684, 699-700, 717-718, 733-734, 751-752, 767-768


883-893: Proper handling of external document arrays in form data collection.

The code correctly preserves external document attachments from the temporary objects when collecting form data for new assets/sub-assets, with appropriate null checks.

Also applies to: 948-960


1097-1308: Well-implemented external document attachment functionality.

The implementation includes:

  • Proper error handling and logging
  • Security through HTML escaping
  • Support for both image previews and document icons
  • Integration badge display
  • Backward compatibility with the old method name
  • Clean separation of concerns with private helper methods

The static analysis hint about optional chaining on line 1180 can be safely ignored as the code already checks for mimeType existence before calling startsWith.

integrations/INTEGRATION.md (1)

1-260: Excellent comprehensive integration guide.

This documentation provides clear, step-by-step instructions for adding new integrations with:

  • Schema-driven architecture explanation
  • Complete code examples
  • File structure requirements
  • Testing checklist
  • References to existing implementations

This will be invaluable for developers adding new integrations.

Also applies to: 264-334

integrations/papra.js (1)

7-495: Static-only class pattern is appropriate for the integration architecture.

While the static analysis tool flags the use of a class with only static members, this pattern is intentionally used across all integrations in the codebase. It provides:

  • Consistent interface for integration registration
  • Clear organization of integration-specific logic
  • Compatibility with the schema-driven IntegrationManager

The pattern works well for this use case and maintains consistency with other integrations.

server.js (13)

27-28: LGTM!

The new imports for TOKENMASK and integrationManager are properly structured and align with the integration management system being introduced.


66-73: LGTM!

The integrationSettings structure is well-designed with appropriate default values. Starting with the integration disabled and empty configuration values is a good security practice.


204-204: LGTM!

Adding /assets/css/ to public paths is appropriate for serving CSS files without authentication.


938-945: LGTM!

The isExternalFile helper function properly identifies external files using both metadata (integrationId) and path-based (/external/) approaches, providing good backwards compatibility.


947-980: Good implementation of external file handling

The logic correctly differentiates between external and local files during duplication, preserving external file references while creating new copies for local files.


2072-2086: Well-implemented token sanitization

The stripIntegrationTokens function properly sanitizes sensitive integration data before sending to the frontend, using the integration manager's sanitization logic for each integration.


2088-2091: LGTM!

Clean delegation to the integration manager for validation and sensitive data handling.


2173-2189: Secure implementation of settings endpoint

The endpoint properly sanitizes integration tokens before sending settings to the frontend, preventing exposure of sensitive configuration data.


2191-2200: LGTM!

The /api/integrations endpoint properly delegates to the integration manager to get available integrations for the settings UI.


2202-2226: Well-implemented enabled integrations endpoint

The endpoint correctly filters integrations based on their enabled status in settings and returns only the necessary information for the frontend.


2228-2250: Robust integration testing endpoint

The test endpoint properly handles configuration preparation (including masked tokens) and returns appropriate status responses.


2252-2272: Proper validation for integration settings

The settings endpoint now validates integration configurations before saving, with appropriate error handling and user feedback.


2423-2426: LGTM!

The integration system is properly initialized with the Express app and settings getter function before starting the server.

localStorage.setItem(this.localSettingsStorageKey, JSON.stringify(settingsCopy));
this.closeSettingsModal();

// this.closeSettingsModal();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Address commented-out modal close.

The modal close operation is commented out. This might leave the settings modal open after saving, which could be confusing for users. Either remove the comment or provide a reason why the modal should remain open.

-            // this.closeSettingsModal();
+            this.closeSettingsModal();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// this.closeSettingsModal();
this.closeSettingsModal();
🤖 Prompt for AI Agents
In public/managers/settings.js at line 298, the call to closeSettingsModal() is
commented out, which may cause the settings modal to remain open after saving.
To fix this, either uncomment the this.closeSettingsModal() line to ensure the
modal closes after saving, or add a clear comment explaining why the modal
should stay open if that behavior is intentional.

Comment on lines +33 to +38
export const API_TEST_SUCCESS = 'connected';
// API ENDPOINTS
export const API_INTEGRATIONS_ENPOINT = 'api/integrations';
export const API_PAPERLESS_ENDPOINT = API_INTEGRATIONS_ENPOINT + '/paperless';
export const API_PAPRA_ENDPOINT = API_INTEGRATIONS_ENPOINT + '/papra';
export const API_HOMEASSISTANT_ENDPOINT = API_INTEGRATIONS_ENPOINT + '/homeassistant'; No newline at end of file
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Typo in constant name will propagate to all consumers

API_INTEGRATIONS_ENPOINT is missing the “D” in “ENDPOINT”.
Every subsequent constant (and any import elsewhere) re-uses this misspelling, so the typo will silently break dynamic string concatenation or lead to two co-existing constants once someone fixes it downstream.

-export const API_INTEGRATIONS_ENPOINT = 'api/integrations';
+export const API_INTEGRATIONS_ENDPOINT = 'api/integrations';
+// keep a temporary alias for backward-compat if needed
+// export const API_INTEGRATIONS_ENPOINT = API_INTEGRATIONS_ENDPOINT;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/constants.js around lines 33 to 38, the constant API_INTEGRATIONS_ENPOINT
is misspelled and should be API_INTEGRATIONS_ENDPOINT. Rename this constant to
fix the typo and update all references in the file accordingly to prevent
propagation of the error and ensure consistent usage.

* @returns {string} - The badge HTML
*/
function getIntegrationBadge(integrationId) {
return integrationsManager?.getIntegrationBadge(integrationId) || `<div class="integration-badge generic-badge"><span title="From ${integrationId}">${integrationId}</span></div>`;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Escape integrationId in fallback HTML.

While integrationId should come from trusted sources, it's best practice to escape it in the fallback HTML generation.

-return integrationsManager?.getIntegrationBadge(integrationId) || `<div class="integration-badge generic-badge"><span title="From ${integrationId}">${integrationId}</span></div>`;
+return integrationsManager?.getIntegrationBadge(integrationId) || 
+    `<div class="integration-badge generic-badge"><span title="From ${integrationId.replace(/"/g, '&quot;')}">${integrationId.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</span></div>`;

Or better yet, create the element programmatically:

function getIntegrationBadge(integrationId) {
    const badge = integrationsManager?.getIntegrationBadge(integrationId);
    if (badge) return badge;
    
    // Fallback - create element safely
    const div = document.createElement('div');
    div.className = 'integration-badge generic-badge';
    const span = document.createElement('span');
    span.title = `From ${integrationId}`;
    span.textContent = integrationId;
    div.appendChild(span);
    return div.outerHTML;
}
🤖 Prompt for AI Agents
In src/services/render/previewRenderer.js at line 279, the fallback HTML string
directly inserts integrationId without escaping, which risks XSS
vulnerabilities. To fix this, replace the fallback string with code that creates
the elements programmatically: create a div with the appropriate class, create a
span with title and textContent set to integrationId, append the span to the
div, and return the div's outerHTML. This ensures integrationId is safely
escaped.

Comment on lines +109 to +123
statusCheck: async (config) => {
if (!config.enabled) {
return { status: 'disabled', message: 'Integration is disabled' };
}

if (!config.hostUrl || !config.accessToken) {
return { status: 'misconfigured', message: 'Missing required configuration' };
}

try {
return await this.testConnection(config);
} catch (error) {
return { status: 'error', message: error.message };
}
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix static context reference.

The statusCheck function uses this in a static context, which should reference the class name instead.

     statusCheck: async (config) => {
       if (!config.enabled) {
         return { status: 'disabled', message: 'Integration is disabled' };
       }
         
       if (!config.hostUrl || !config.accessToken) {
         return { status: 'misconfigured', message: 'Missing required configuration' };
       }
         
       try {
-        return await this.testConnection(config);
+        return await HomeAssistantIntegration.testConnection(config);
       } catch (error) {
         return { status: 'error', message: error.message };
       }
     },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
statusCheck: async (config) => {
if (!config.enabled) {
return { status: 'disabled', message: 'Integration is disabled' };
}
if (!config.hostUrl || !config.accessToken) {
return { status: 'misconfigured', message: 'Missing required configuration' };
}
try {
return await this.testConnection(config);
} catch (error) {
return { status: 'error', message: error.message };
}
},
statusCheck: async (config) => {
if (!config.enabled) {
return { status: 'disabled', message: 'Integration is disabled' };
}
if (!config.hostUrl || !config.accessToken) {
return { status: 'misconfigured', message: 'Missing required configuration' };
}
try {
return await HomeAssistantIntegration.testConnection(config);
} catch (error) {
return { status: 'error', message: error.message };
}
},
🤖 Prompt for AI Agents
In integrations/homeassistant.js around lines 109 to 123, the statusCheck
function incorrectly uses `this` to call testConnection in a static context.
Replace `this.testConnection(config)` with a direct reference to the class name
followed by `.testConnection(config)` to correctly access the static method.

Comment on lines +374 to +547
static registerRoutes(app, getSettings) {
const BASE_PATH = process.env.BASE_PATH || '';

// Helper to get Home Assistant config
const getHomeAssistantConfig = async () => {
try {
const settings = await getSettings();
const haConfig = settings.integrationSettings?.homeassistant;

if (!haConfig?.enabled) {
throw new Error('Home Assistant integration is disabled');
}

return haConfig;
} catch (error) {
throw new Error('Failed to get Home Assistant configuration');
}
};

// Test Home Assistant connection
app.post(BASE_PATH + `/${API_HOMEASSISTANT_ENDPOINT}/test-connection`, async (req, res) => {
try {
let { hostUrl, accessToken } = req.body;

if (!hostUrl || !accessToken) {
return res.status(400).json({
success: false,
error: 'Host URL and Access Token are required'
});
}

// If the token is masked, get the real token from stored settings
if (accessToken === TOKENMASK) {
try {
const haConfig = await getHomeAssistantConfig();
if (haConfig?.accessToken && haConfig.accessToken !== TOKENMASK) {
accessToken = haConfig.accessToken;
} else {
return res.status(400).json({
success: false,
error: 'No saved access token found. Please enter a new token.'
});
}
} catch (error) {
return res.status(400).json({
success: false,
error: 'Failed to retrieve saved token: ' + error.message
});
}
}

const result = await this.testConnection({ hostUrl, accessToken });
res.json({ success: true, ...result });
} catch (error) {
console.error('Home Assistant connection test failed:', error);
res.status(400).json({
success: false,
error: error.message
});
}
});

// Get all Home Assistant devices
app.get(BASE_PATH + `/${API_HOMEASSISTANT_ENDPOINT}/devices`, async (req, res) => {
try {
const config = await getHomeAssistantConfig();

// Parse filters from config
let filters = [];
if (config.importFilters) {
filters = config.importFilters.split(',').map(f => f.trim()).filter(f => f);
}

const devices = await this.getDevices(config, filters);
res.json({
success: true,
devices,
total: devices.length,
filters: filters
});
} catch (error) {
console.error('Home Assistant devices fetch failed:', error);
res.status(500).json({
success: false,
error: error.message
});
}
});

// Get Home Assistant configuration
app.get(BASE_PATH + `/${API_HOMEASSISTANT_ENDPOINT}/config`, async (req, res) => {
try {
const config = await getHomeAssistantConfig();
const haConfig = await this.getConfig(config);
res.json({ success: true, config: haConfig });
} catch (error) {
console.error('Home Assistant config fetch failed:', error);
res.status(500).json({
success: false,
error: error.message
});
}
});

// Get specific device info
app.get(BASE_PATH + `/${API_HOMEASSISTANT_ENDPOINT}/device/:entity_id/info`, async (req, res) => {
try {
const config = await getHomeAssistantConfig();
const entityId = decodeURIComponent(req.params.entity_id);

const info = await this.getDeviceInfo(config, entityId);
res.json({ success: true, device: info });
} catch (error) {
console.error('Failed to get device info:', error);
res.status(500).json({
success: false,
error: error.message
});
}
});

// Import devices as assets
app.post(BASE_PATH + `/${API_HOMEASSISTANT_ENDPOINT}/import-devices`, async (req, res) => {
try {
const config = await getHomeAssistantConfig();
const { selectedEntities } = req.body;

if (!selectedEntities || !Array.isArray(selectedEntities)) {
return res.status(400).json({
success: false,
error: 'selectedEntities array is required'
});
}

// Get full device info for selected entities
const devicePromises = selectedEntities.map(entityId =>
this.getDeviceInfo(config, entityId)
);

const entities = await Promise.all(devicePromises);

// Convert to asset format
const assets = this.convertToAssets(entities.map(entity => ({
entity_id: entity.entity_id,
state: entity.state,
attributes: entity.attributes,
last_changed: entity.last_changed,
last_updated: entity.last_updated,
area: entity.attributes.area_id
})), config);

res.json({
success: true,
assets,
imported: assets.length,
message: `Successfully converted ${assets.length} Home Assistant entities to assets`
});
} catch (error) {
console.error('Home Assistant import failed:', error);
res.status(500).json({
success: false,
error: error.message
});
}
});

// Test endpoint for debugging
app.get(BASE_PATH + `/${API_HOMEASSISTANT_ENDPOINT}/test`, (req, res) => {
res.json({
message: 'Home Assistant integration endpoints are working',
timestamp: new Date().toISOString()
});
});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix static context references throughout registerRoutes.

Multiple instances of this are used in static context and should reference the class name instead.

Apply these changes:

-        const result = await this.testConnection({ hostUrl, accessToken });
+        const result = await HomeAssistantIntegration.testConnection({ hostUrl, accessToken });
-        const devices = await this.getDevices(config, filters);
+        const devices = await HomeAssistantIntegration.getDevices(config, filters);
-        const haConfig = await this.getConfig(config);
+        const haConfig = await HomeAssistantIntegration.getConfig(config);
-        const info = await this.getDeviceInfo(config, entityId);
+        const info = await HomeAssistantIntegration.getDeviceInfo(config, entityId);
-          this.getDeviceInfo(config, entityId)
+          HomeAssistantIntegration.getDeviceInfo(config, entityId)
-        const assets = this.convertToAssets(entities.map(entity => ({
+        const assets = HomeAssistantIntegration.convertToAssets(entities.map(entity => ({
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static registerRoutes(app, getSettings) {
const BASE_PATH = process.env.BASE_PATH || '';
// Helper to get Home Assistant config
const getHomeAssistantConfig = async () => {
try {
const settings = await getSettings();
const haConfig = settings.integrationSettings?.homeassistant;
if (!haConfig?.enabled) {
throw new Error('Home Assistant integration is disabled');
}
return haConfig;
} catch (error) {
throw new Error('Failed to get Home Assistant configuration');
}
};
// Test Home Assistant connection
app.post(BASE_PATH + `/${API_HOMEASSISTANT_ENDPOINT}/test-connection`, async (req, res) => {
try {
let { hostUrl, accessToken } = req.body;
if (!hostUrl || !accessToken) {
return res.status(400).json({
success: false,
error: 'Host URL and Access Token are required'
});
}
// If the token is masked, get the real token from stored settings
if (accessToken === TOKENMASK) {
try {
const haConfig = await getHomeAssistantConfig();
if (haConfig?.accessToken && haConfig.accessToken !== TOKENMASK) {
accessToken = haConfig.accessToken;
} else {
return res.status(400).json({
success: false,
error: 'No saved access token found. Please enter a new token.'
});
}
} catch (error) {
return res.status(400).json({
success: false,
error: 'Failed to retrieve saved token: ' + error.message
});
}
}
const result = await this.testConnection({ hostUrl, accessToken });
res.json({ success: true, ...result });
} catch (error) {
console.error('Home Assistant connection test failed:', error);
res.status(400).json({
success: false,
error: error.message
});
}
});
// Get all Home Assistant devices
app.get(BASE_PATH + `/${API_HOMEASSISTANT_ENDPOINT}/devices`, async (req, res) => {
try {
const config = await getHomeAssistantConfig();
// Parse filters from config
let filters = [];
if (config.importFilters) {
filters = config.importFilters.split(',').map(f => f.trim()).filter(f => f);
}
const devices = await this.getDevices(config, filters);
res.json({
success: true,
devices,
total: devices.length,
filters: filters
});
} catch (error) {
console.error('Home Assistant devices fetch failed:', error);
res.status(500).json({
success: false,
error: error.message
});
}
});
// Get Home Assistant configuration
app.get(BASE_PATH + `/${API_HOMEASSISTANT_ENDPOINT}/config`, async (req, res) => {
try {
const config = await getHomeAssistantConfig();
const haConfig = await this.getConfig(config);
res.json({ success: true, config: haConfig });
} catch (error) {
console.error('Home Assistant config fetch failed:', error);
res.status(500).json({
success: false,
error: error.message
});
}
});
// Get specific device info
app.get(BASE_PATH + `/${API_HOMEASSISTANT_ENDPOINT}/device/:entity_id/info`, async (req, res) => {
try {
const config = await getHomeAssistantConfig();
const entityId = decodeURIComponent(req.params.entity_id);
const info = await this.getDeviceInfo(config, entityId);
res.json({ success: true, device: info });
} catch (error) {
console.error('Failed to get device info:', error);
res.status(500).json({
success: false,
error: error.message
});
}
});
// Import devices as assets
app.post(BASE_PATH + `/${API_HOMEASSISTANT_ENDPOINT}/import-devices`, async (req, res) => {
try {
const config = await getHomeAssistantConfig();
const { selectedEntities } = req.body;
if (!selectedEntities || !Array.isArray(selectedEntities)) {
return res.status(400).json({
success: false,
error: 'selectedEntities array is required'
});
}
// Get full device info for selected entities
const devicePromises = selectedEntities.map(entityId =>
this.getDeviceInfo(config, entityId)
);
const entities = await Promise.all(devicePromises);
// Convert to asset format
const assets = this.convertToAssets(entities.map(entity => ({
entity_id: entity.entity_id,
state: entity.state,
attributes: entity.attributes,
last_changed: entity.last_changed,
last_updated: entity.last_updated,
area: entity.attributes.area_id
})), config);
res.json({
success: true,
assets,
imported: assets.length,
message: `Successfully converted ${assets.length} Home Assistant entities to assets`
});
} catch (error) {
console.error('Home Assistant import failed:', error);
res.status(500).json({
success: false,
error: error.message
});
}
});
// Test endpoint for debugging
app.get(BASE_PATH + `/${API_HOMEASSISTANT_ENDPOINT}/test`, (req, res) => {
res.json({
message: 'Home Assistant integration endpoints are working',
timestamp: new Date().toISOString()
});
});
}
static registerRoutes(app, getSettings) {
const BASE_PATH = process.env.BASE_PATH || '';
// Helper to get Home Assistant config
const getHomeAssistantConfig = async () => {
try {
const settings = await getSettings();
const haConfig = settings.integrationSettings?.homeassistant;
if (!haConfig?.enabled) {
throw new Error('Home Assistant integration is disabled');
}
return haConfig;
} catch (error) {
throw new Error('Failed to get Home Assistant configuration');
}
};
// Test Home Assistant connection
app.post(BASE_PATH + `/${API_HOMEASSISTANT_ENDPOINT}/test-connection`, async (req, res) => {
try {
let { hostUrl, accessToken } = req.body;
if (!hostUrl || !accessToken) {
return res.status(400).json({
success: false,
error: 'Host URL and Access Token are required'
});
}
// If the token is masked, get the real token from stored settings
if (accessToken === TOKENMASK) {
try {
const haConfig = await getHomeAssistantConfig();
if (haConfig?.accessToken && haConfig.accessToken !== TOKENMASK) {
accessToken = haConfig.accessToken;
} else {
return res.status(400).json({
success: false,
error: 'No saved access token found. Please enter a new token.'
});
}
} catch (error) {
return res.status(400).json({
success: false,
error: 'Failed to retrieve saved token: ' + error.message
});
}
}
const result = await HomeAssistantIntegration.testConnection({ hostUrl, accessToken });
res.json({ success: true, ...result });
} catch (error) {
console.error('Home Assistant connection test failed:', error);
res.status(400).json({
success: false,
error: error.message
});
}
});
// Get all Home Assistant devices
app.get(BASE_PATH + `/${API_HOMEASSISTANT_ENDPOINT}/devices`, async (req, res) => {
try {
const config = await getHomeAssistantConfig();
// Parse filters from config
let filters = [];
if (config.importFilters) {
filters = config.importFilters.split(',').map(f => f.trim()).filter(f => f);
}
const devices = await HomeAssistantIntegration.getDevices(config, filters);
res.json({
success: true,
devices,
total: devices.length,
filters: filters
});
} catch (error) {
console.error('Home Assistant devices fetch failed:', error);
res.status(500).json({
success: false,
error: error.message
});
}
});
// Get Home Assistant configuration
app.get(BASE_PATH + `/${API_HOMEASSISTANT_ENDPOINT}/config`, async (req, res) => {
try {
const config = await getHomeAssistantConfig();
const haConfig = await HomeAssistantIntegration.getConfig(config);
res.json({ success: true, config: haConfig });
} catch (error) {
console.error('Home Assistant config fetch failed:', error);
res.status(500).json({
success: false,
error: error.message
});
}
});
// Get specific device info
app.get(BASE_PATH + `/${API_HOMEASSISTANT_ENDPOINT}/device/:entity_id/info`, async (req, res) => {
try {
const config = await getHomeAssistantConfig();
const entityId = decodeURIComponent(req.params.entity_id);
const info = await HomeAssistantIntegration.getDeviceInfo(config, entityId);
res.json({ success: true, device: info });
} catch (error) {
console.error('Failed to get device info:', error);
res.status(500).json({
success: false,
error: error.message
});
}
});
// Import devices as assets
app.post(BASE_PATH + `/${API_HOMEASSISTANT_ENDPOINT}/import-devices`, async (req, res) => {
try {
const config = await getHomeAssistantConfig();
const { selectedEntities } = req.body;
if (!selectedEntities || !Array.isArray(selectedEntities)) {
return res.status(400).json({
success: false,
error: 'selectedEntities array is required'
});
}
// Get full device info for selected entities
const devicePromises = selectedEntities.map(entityId =>
HomeAssistantIntegration.getDeviceInfo(config, entityId)
);
const entities = await Promise.all(devicePromises);
// Convert to asset format
const assets = HomeAssistantIntegration.convertToAssets(
entities.map(entity => ({
entity_id: entity.entity_id,
state: entity.state,
attributes: entity.attributes,
last_changed: entity.last_changed,
last_updated: entity.last_updated,
area: entity.attributes.area_id
})),
config
);
res.json({
success: true,
assets,
imported: assets.length,
message: `Successfully converted ${assets.length} Home Assistant entities to assets`
});
} catch (error) {
console.error('Home Assistant import failed:', error);
res.status(500).json({
success: false,
error: error.message
});
}
});
// Test endpoint for debugging
app.get(BASE_PATH + `/${API_HOMEASSISTANT_ENDPOINT}/test`, (req, res) => {
res.json({
message: 'Home Assistant integration endpoints are working',
timestamp: new Date().toISOString()
});
});
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 425-425: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 447-447: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 467-467: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 484-484: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 510-510: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


[error] 516-516: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)

🤖 Prompt for AI Agents
In integrations/homeassistant.js from lines 374 to 547, the static method
registerRoutes uses 'this' to call other static methods, which is incorrect in
static context. Replace all 'this' references with the class name to correctly
reference static methods. For example, change 'this.testConnection' to
'HomeAssistant.testConnection' and similarly for 'getDevices', 'getConfig',
'getDeviceInfo', and 'convertToAssets'.

}
}

const result = await this.testConnection({ hostUrl, apiToken });
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix all instances of 'this' in static context within registerRoutes.

Multiple uses of this in the static registerRoutes method will cause runtime errors. Replace all instances with the class name.

Apply this diff to fix all instances:

-const result = await this.testConnection({ hostUrl, apiToken });
+const result = await PaperlessIntegration.testConnection({ hostUrl, apiToken });

-const results = await this.searchDocuments(
+const results = await PaperlessIntegration.searchDocuments(

-const info = await this.getDocumentInfo(config, documentId);
+const info = await PaperlessIntegration.getDocumentInfo(config, documentId);

-const response = await this.downloadDocument(config, documentId);
+const response = await PaperlessIntegration.downloadDocument(config, documentId);

-const response = await this.downloadDocument(config, documentId);
+const response = await PaperlessIntegration.downloadDocument(config, documentId);

Also applies to: 371-371, 391-391, 405-405, 431-431

🧰 Tools
🪛 Biome (1.9.4)

[error] 354-354: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)

🤖 Prompt for AI Agents
In integrations/paperless.js at lines 354, 371, 391, 405, and 431, the static
method registerRoutes incorrectly uses 'this', which causes runtime errors.
Replace all instances of 'this' within this static method with the class name to
correctly reference class properties or methods.

Comment on lines +447 to +450
app.use(BASE_PATH + '/src', express.static('src'));
// app.use(BASE_PATH + '/src/services', express.static('src/services'));
// app.use(BASE_PATH + '/src/services/fileUpload', express.static('src/services/fileUpload'));
// app.use(BASE_PATH + '/src/services/render', express.static('src/services/render'));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Security concern: Avoid serving the entire /src directory

Serving the entire /src directory statically could expose sensitive source code files. Consider serving only specific subdirectories that contain client-side assets.

Replace the broad directory serving with specific paths:

-app.use(BASE_PATH + '/src', express.static('src'));
-// app.use(BASE_PATH + '/src/services', express.static('src/services'));
-// app.use(BASE_PATH + '/src/services/fileUpload', express.static('src/services/fileUpload'));
-// app.use(BASE_PATH + '/src/services/render', express.static('src/services/render'));
+// Only serve specific directories needed by the frontend
+app.use(BASE_PATH + '/src/services/fileUpload', express.static('src/services/fileUpload'));
+app.use(BASE_PATH + '/src/services/render', express.static('src/services/render'));
+app.use(BASE_PATH + '/src/constants', express.static('src/constants'));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
app.use(BASE_PATH + '/src', express.static('src'));
// app.use(BASE_PATH + '/src/services', express.static('src/services'));
// app.use(BASE_PATH + '/src/services/fileUpload', express.static('src/services/fileUpload'));
// app.use(BASE_PATH + '/src/services/render', express.static('src/services/render'));
// Only serve specific directories needed by the frontend
app.use(BASE_PATH + '/src/services/fileUpload', express.static('src/services/fileUpload'));
app.use(BASE_PATH + '/src/services/render', express.static('src/services/render'));
app.use(BASE_PATH + '/src/constants', express.static('src/constants'));
🤖 Prompt for AI Agents
In server.js around lines 447 to 450, avoid serving the entire /src directory
statically as it may expose sensitive source code. Instead, uncomment and use
the more specific static serving lines for subdirectories like /src/services,
/src/services/fileUpload, and /src/services/render to limit exposure only to
necessary client-side assets.

abiteman and others added 14 commits July 16, 2025 22:40
Replaces the sidebar's left positioning and transition with transform-based sliding for improved performance and smoother animations. The sidebar now uses translateX to hide and show itself.
- Complete incomplete template literal in assetRenderer.js with proper action buttons, content sections, and event listeners
- Add copy link, edit, duplicate, and delete functionality to asset details
- Include asset info, file grid, and maintenance events sections in rendered template
- Ensure proper structural integrity with closed tags and function braces
…lectors

- Move integration CSS from styles.css to dedicated integration-styles.css
- Scope generic .switch and .slider selectors with .integration-section parent
- Prevent style leakage to other components using similar class names
- Maintain proper CSS loading order with integration styles loading last
- Consolidate all integration-related styles in single dedicated file
- Add comingSoon flag to Home Assistant integration schema
- Replace enable toggle with animated 'Coming Soon!' badge for disabled integrations
- Hide all dependent fields and test buttons for coming soon integrations
- Add gradient text styling with pulse animation for coming soon message
- Ensure proper handling in settings load/save for disabled integrations
- Feature ready for future activation by removing comingSoon flag
- Add cache-busting timestamp to integrations API call to prevent stale data
- Simplify toggleIntegrationFields logic for Coming Soon integrations
- Use display: flex instead of block for better field alignment
- Ensure integrationSupportsTestConnection returns false for Coming Soon integrations
- Reset loadingPromise to allow re-fetching when needed
- Improve robustness of Coming Soon feature display
- Log the loaded Home Assistant integration schema for debugging
- Help diagnose why comingSoon flag isn't appearing in API response
…oper module cache clearing

- Fixed integration schema registration to include comingSoon flag in final integration object
- Implemented proper module cache clearing for fresh integration loading on restart
- Store integration classes as instance variables for proper route registration
- This resolves the issue where comingSoon flag was present in schema files but not appearing in API responses
- Export formatFilePath, initRenderer, updateState, updateSelectedIds, and renderAssetDetails functions
- Remove debug console.log from integrationManager.js
- Resolves 'does not provide an export named formatFilePath' JavaScript module error
- Frontend should now load properly with Home Assistant 'Coming Soon!' message displaying correctly
…moother animation

- Changed gradient colors from red/orange to blue-green (#14b8a6, #06b6d4)
- Updated background and border colors to match teal theme
- Smoothed animation: increased duration from 2s to 3s
- Reduced scaling from 1.02 to 1.01 for more subtle effect
- Improved keyframe animation for gentler pulse glow
- Enhanced overall visual polish for Coming Soon integrations
- update to put each attachment type in their own div/row
- remove set height on file-preview so less space is taken up
- adjust file-info /filename pill so it stays contained within their own respective containers
- preview should now pull from by id/thumbs/ in paperless
- download: append query param for original so it always downloads the original file to handle any file type
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants