#
tokens: 50696/50000 1/625 files (page 45/47)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 45 of 47. Use http://codebase.md/doobidoo/mcp-memory-service?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .claude
│   ├── agents
│   │   ├── amp-bridge.md
│   │   ├── amp-pr-automator.md
│   │   ├── code-quality-guard.md
│   │   ├── gemini-pr-automator.md
│   │   └── github-release-manager.md
│   ├── settings.local.json.backup
│   └── settings.local.json.local
├── .commit-message
├── .dockerignore
├── .env.example
├── .env.sqlite.backup
├── .envnn#
├── .gitattributes
├── .github
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.yml
│   │   ├── config.yml
│   │   ├── feature_request.yml
│   │   └── performance_issue.yml
│   ├── pull_request_template.md
│   └── workflows
│       ├── bridge-tests.yml
│       ├── CACHE_FIX.md
│       ├── claude-code-review.yml
│       ├── claude.yml
│       ├── cleanup-images.yml.disabled
│       ├── dev-setup-validation.yml
│       ├── docker-publish.yml
│       ├── LATEST_FIXES.md
│       ├── main-optimized.yml.disabled
│       ├── main.yml
│       ├── publish-and-test.yml
│       ├── README_OPTIMIZATION.md
│       ├── release-tag.yml.disabled
│       ├── release.yml
│       ├── roadmap-review-reminder.yml
│       ├── SECRET_CONDITIONAL_FIX.md
│       └── WORKFLOW_FIXES.md
├── .gitignore
├── .mcp.json.backup
├── .mcp.json.template
├── .pyscn
│   ├── .gitignore
│   └── reports
│       └── analyze_20251123_214224.html
├── AGENTS.md
├── archive
│   ├── deployment
│   │   ├── deploy_fastmcp_fixed.sh
│   │   ├── deploy_http_with_mcp.sh
│   │   └── deploy_mcp_v4.sh
│   ├── deployment-configs
│   │   ├── empty_config.yml
│   │   └── smithery.yaml
│   ├── development
│   │   └── test_fastmcp.py
│   ├── docs-removed-2025-08-23
│   │   ├── authentication.md
│   │   ├── claude_integration.md
│   │   ├── claude-code-compatibility.md
│   │   ├── claude-code-integration.md
│   │   ├── claude-code-quickstart.md
│   │   ├── claude-desktop-setup.md
│   │   ├── complete-setup-guide.md
│   │   ├── database-synchronization.md
│   │   ├── development
│   │   │   ├── autonomous-memory-consolidation.md
│   │   │   ├── CLEANUP_PLAN.md
│   │   │   ├── CLEANUP_README.md
│   │   │   ├── CLEANUP_SUMMARY.md
│   │   │   ├── dream-inspired-memory-consolidation.md
│   │   │   ├── hybrid-slm-memory-consolidation.md
│   │   │   ├── mcp-milestone.md
│   │   │   ├── multi-client-architecture.md
│   │   │   ├── test-results.md
│   │   │   └── TIMESTAMP_FIX_SUMMARY.md
│   │   ├── distributed-sync.md
│   │   ├── invocation_guide.md
│   │   ├── macos-intel.md
│   │   ├── master-guide.md
│   │   ├── mcp-client-configuration.md
│   │   ├── multi-client-server.md
│   │   ├── service-installation.md
│   │   ├── sessions
│   │   │   └── MCP_ENHANCEMENT_SESSION_MEMORY_v4.1.0.md
│   │   ├── UBUNTU_SETUP.md
│   │   ├── ubuntu.md
│   │   ├── windows-setup.md
│   │   └── windows.md
│   ├── docs-root-cleanup-2025-08-23
│   │   ├── AWESOME_LIST_SUBMISSION.md
│   │   ├── CLOUDFLARE_IMPLEMENTATION.md
│   │   ├── DOCUMENTATION_ANALYSIS.md
│   │   ├── DOCUMENTATION_CLEANUP_PLAN.md
│   │   ├── DOCUMENTATION_CONSOLIDATION_COMPLETE.md
│   │   ├── LITESTREAM_SETUP_GUIDE.md
│   │   ├── lm_studio_system_prompt.md
│   │   ├── PYTORCH_DOWNLOAD_FIX.md
│   │   └── README-ORIGINAL-BACKUP.md
│   ├── investigations
│   │   └── MACOS_HOOKS_INVESTIGATION.md
│   ├── litestream-configs-v6.3.0
│   │   ├── install_service.sh
│   │   ├── litestream_master_config_fixed.yml
│   │   ├── litestream_master_config.yml
│   │   ├── litestream_replica_config_fixed.yml
│   │   ├── litestream_replica_config.yml
│   │   ├── litestream_replica_simple.yml
│   │   ├── litestream-http.service
│   │   ├── litestream.service
│   │   └── requirements-cloudflare.txt
│   ├── release-notes
│   │   └── release-notes-v7.1.4.md
│   └── setup-development
│       ├── README.md
│       ├── setup_consolidation_mdns.sh
│       ├── STARTUP_SETUP_GUIDE.md
│       └── test_service.sh
├── CHANGELOG-HISTORIC.md
├── CHANGELOG.md
├── claude_commands
│   ├── memory-context.md
│   ├── memory-health.md
│   ├── memory-ingest-dir.md
│   ├── memory-ingest.md
│   ├── memory-recall.md
│   ├── memory-search.md
│   ├── memory-store.md
│   ├── README.md
│   └── session-start.md
├── claude-hooks
│   ├── config.json
│   ├── config.template.json
│   ├── CONFIGURATION.md
│   ├── core
│   │   ├── memory-retrieval.js
│   │   ├── mid-conversation.js
│   │   ├── session-end.js
│   │   ├── session-start.js
│   │   └── topic-change.js
│   ├── debug-pattern-test.js
│   ├── install_claude_hooks_windows.ps1
│   ├── install_hooks.py
│   ├── memory-mode-controller.js
│   ├── MIGRATION.md
│   ├── README-NATURAL-TRIGGERS.md
│   ├── README-phase2.md
│   ├── README.md
│   ├── simple-test.js
│   ├── statusline.sh
│   ├── test-adaptive-weights.js
│   ├── test-dual-protocol-hook.js
│   ├── test-mcp-hook.js
│   ├── test-natural-triggers.js
│   ├── test-recency-scoring.js
│   ├── tests
│   │   ├── integration-test.js
│   │   ├── phase2-integration-test.js
│   │   ├── test-code-execution.js
│   │   ├── test-cross-session.json
│   │   ├── test-session-tracking.json
│   │   └── test-threading.json
│   ├── utilities
│   │   ├── adaptive-pattern-detector.js
│   │   ├── context-formatter.js
│   │   ├── context-shift-detector.js
│   │   ├── conversation-analyzer.js
│   │   ├── dynamic-context-updater.js
│   │   ├── git-analyzer.js
│   │   ├── mcp-client.js
│   │   ├── memory-client.js
│   │   ├── memory-scorer.js
│   │   ├── performance-manager.js
│   │   ├── project-detector.js
│   │   ├── session-tracker.js
│   │   ├── tiered-conversation-monitor.js
│   │   └── version-checker.js
│   └── WINDOWS-SESSIONSTART-BUG.md
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Development-Sprint-November-2025.md
├── docs
│   ├── amp-cli-bridge.md
│   ├── api
│   │   ├── code-execution-interface.md
│   │   ├── memory-metadata-api.md
│   │   ├── PHASE1_IMPLEMENTATION_SUMMARY.md
│   │   ├── PHASE2_IMPLEMENTATION_SUMMARY.md
│   │   ├── PHASE2_REPORT.md
│   │   └── tag-standardization.md
│   ├── architecture
│   │   ├── search-enhancement-spec.md
│   │   └── search-examples.md
│   ├── architecture.md
│   ├── archive
│   │   └── obsolete-workflows
│   │       ├── load_memory_context.md
│   │       └── README.md
│   ├── assets
│   │   └── images
│   │       ├── dashboard-v3.3.0-preview.png
│   │       ├── memory-awareness-hooks-example.png
│   │       ├── project-infographic.svg
│   │       └── README.md
│   ├── CLAUDE_CODE_QUICK_REFERENCE.md
│   ├── cloudflare-setup.md
│   ├── deployment
│   │   ├── docker.md
│   │   ├── dual-service.md
│   │   ├── production-guide.md
│   │   └── systemd-service.md
│   ├── development
│   │   ├── ai-agent-instructions.md
│   │   ├── code-quality
│   │   │   ├── phase-2a-completion.md
│   │   │   ├── phase-2a-handle-get-prompt.md
│   │   │   ├── phase-2a-index.md
│   │   │   ├── phase-2a-install-package.md
│   │   │   └── phase-2b-session-summary.md
│   │   ├── code-quality-workflow.md
│   │   ├── dashboard-workflow.md
│   │   ├── issue-management.md
│   │   ├── pr-review-guide.md
│   │   ├── refactoring-notes.md
│   │   ├── release-checklist.md
│   │   └── todo-tracker.md
│   ├── docker-optimized-build.md
│   ├── document-ingestion.md
│   ├── DOCUMENTATION_AUDIT.md
│   ├── enhancement-roadmap-issue-14.md
│   ├── examples
│   │   ├── analysis-scripts.js
│   │   ├── maintenance-session-example.md
│   │   ├── memory-distribution-chart.jsx
│   │   └── tag-schema.json
│   ├── first-time-setup.md
│   ├── glama-deployment.md
│   ├── guides
│   │   ├── advanced-command-examples.md
│   │   ├── chromadb-migration.md
│   │   ├── commands-vs-mcp-server.md
│   │   ├── mcp-enhancements.md
│   │   ├── mdns-service-discovery.md
│   │   ├── memory-consolidation-guide.md
│   │   ├── migration.md
│   │   ├── scripts.md
│   │   └── STORAGE_BACKENDS.md
│   ├── HOOK_IMPROVEMENTS.md
│   ├── hooks
│   │   └── phase2-code-execution-migration.md
│   ├── http-server-management.md
│   ├── ide-compatability.md
│   ├── IMAGE_RETENTION_POLICY.md
│   ├── images
│   │   └── dashboard-placeholder.md
│   ├── implementation
│   │   ├── health_checks.md
│   │   └── performance.md
│   ├── IMPLEMENTATION_PLAN_HTTP_SSE.md
│   ├── integration
│   │   ├── homebrew.md
│   │   └── multi-client.md
│   ├── integrations
│   │   ├── gemini.md
│   │   ├── groq-bridge.md
│   │   ├── groq-integration-summary.md
│   │   └── groq-model-comparison.md
│   ├── integrations.md
│   ├── legacy
│   │   └── dual-protocol-hooks.md
│   ├── LM_STUDIO_COMPATIBILITY.md
│   ├── maintenance
│   │   └── memory-maintenance.md
│   ├── mastery
│   │   ├── api-reference.md
│   │   ├── architecture-overview.md
│   │   ├── configuration-guide.md
│   │   ├── local-setup-and-run.md
│   │   ├── testing-guide.md
│   │   └── troubleshooting.md
│   ├── migration
│   │   └── code-execution-api-quick-start.md
│   ├── natural-memory-triggers
│   │   ├── cli-reference.md
│   │   ├── installation-guide.md
│   │   └── performance-optimization.md
│   ├── oauth-setup.md
│   ├── pr-graphql-integration.md
│   ├── quick-setup-cloudflare-dual-environment.md
│   ├── README.md
│   ├── remote-configuration-wiki-section.md
│   ├── research
│   │   ├── code-execution-interface-implementation.md
│   │   └── code-execution-interface-summary.md
│   ├── ROADMAP.md
│   ├── sqlite-vec-backend.md
│   ├── statistics
│   │   ├── charts
│   │   │   ├── activity_patterns.png
│   │   │   ├── contributors.png
│   │   │   ├── growth_trajectory.png
│   │   │   ├── monthly_activity.png
│   │   │   └── october_sprint.png
│   │   ├── data
│   │   │   ├── activity_by_day.csv
│   │   │   ├── activity_by_hour.csv
│   │   │   ├── contributors.csv
│   │   │   └── monthly_activity.csv
│   │   ├── generate_charts.py
│   │   └── REPOSITORY_STATISTICS.md
│   ├── technical
│   │   ├── development.md
│   │   ├── memory-migration.md
│   │   ├── migration-log.md
│   │   ├── sqlite-vec-embedding-fixes.md
│   │   └── tag-storage.md
│   ├── testing
│   │   └── regression-tests.md
│   ├── testing-cloudflare-backend.md
│   ├── troubleshooting
│   │   ├── cloudflare-api-token-setup.md
│   │   ├── cloudflare-authentication.md
│   │   ├── general.md
│   │   ├── hooks-quick-reference.md
│   │   ├── pr162-schema-caching-issue.md
│   │   ├── session-end-hooks.md
│   │   └── sync-issues.md
│   └── tutorials
│       ├── advanced-techniques.md
│       ├── data-analysis.md
│       └── demo-session-walkthrough.md
├── examples
│   ├── claude_desktop_config_template.json
│   ├── claude_desktop_config_windows.json
│   ├── claude-desktop-http-config.json
│   ├── config
│   │   └── claude_desktop_config.json
│   ├── http-mcp-bridge.js
│   ├── memory_export_template.json
│   ├── README.md
│   ├── setup
│   │   └── setup_multi_client_complete.py
│   └── start_https_example.sh
├── install_service.py
├── install.py
├── LICENSE
├── NOTICE
├── pyproject.toml
├── pytest.ini
├── README.md
├── run_server.py
├── scripts
│   ├── .claude
│   │   └── settings.local.json
│   ├── archive
│   │   └── check_missing_timestamps.py
│   ├── backup
│   │   ├── backup_memories.py
│   │   ├── backup_sqlite_vec.sh
│   │   ├── export_distributable_memories.sh
│   │   └── restore_memories.py
│   ├── benchmarks
│   │   ├── benchmark_code_execution_api.py
│   │   ├── benchmark_hybrid_sync.py
│   │   └── benchmark_server_caching.py
│   ├── database
│   │   ├── analyze_sqlite_vec_db.py
│   │   ├── check_sqlite_vec_status.py
│   │   ├── db_health_check.py
│   │   └── simple_timestamp_check.py
│   ├── development
│   │   ├── debug_server_initialization.py
│   │   ├── find_orphaned_files.py
│   │   ├── fix_mdns.sh
│   │   ├── fix_sitecustomize.py
│   │   ├── remote_ingest.sh
│   │   ├── setup-git-merge-drivers.sh
│   │   ├── uv-lock-merge.sh
│   │   └── verify_hybrid_sync.py
│   ├── hooks
│   │   └── pre-commit
│   ├── installation
│   │   ├── install_linux_service.py
│   │   ├── install_macos_service.py
│   │   ├── install_uv.py
│   │   ├── install_windows_service.py
│   │   ├── install.py
│   │   ├── setup_backup_cron.sh
│   │   ├── setup_claude_mcp.sh
│   │   └── setup_cloudflare_resources.py
│   ├── linux
│   │   ├── service_status.sh
│   │   ├── start_service.sh
│   │   ├── stop_service.sh
│   │   ├── uninstall_service.sh
│   │   └── view_logs.sh
│   ├── maintenance
│   │   ├── assign_memory_types.py
│   │   ├── check_memory_types.py
│   │   ├── cleanup_corrupted_encoding.py
│   │   ├── cleanup_memories.py
│   │   ├── cleanup_organize.py
│   │   ├── consolidate_memory_types.py
│   │   ├── consolidation_mappings.json
│   │   ├── delete_orphaned_vectors_fixed.py
│   │   ├── fast_cleanup_duplicates_with_tracking.sh
│   │   ├── find_all_duplicates.py
│   │   ├── find_cloudflare_duplicates.py
│   │   ├── find_duplicates.py
│   │   ├── memory-types.md
│   │   ├── README.md
│   │   ├── recover_timestamps_from_cloudflare.py
│   │   ├── regenerate_embeddings.py
│   │   ├── repair_malformed_tags.py
│   │   ├── repair_memories.py
│   │   ├── repair_sqlite_vec_embeddings.py
│   │   ├── repair_zero_embeddings.py
│   │   ├── restore_from_json_export.py
│   │   └── scan_todos.sh
│   ├── migration
│   │   ├── cleanup_mcp_timestamps.py
│   │   ├── legacy
│   │   │   └── migrate_chroma_to_sqlite.py
│   │   ├── mcp-migration.py
│   │   ├── migrate_sqlite_vec_embeddings.py
│   │   ├── migrate_storage.py
│   │   ├── migrate_tags.py
│   │   ├── migrate_timestamps.py
│   │   ├── migrate_to_cloudflare.py
│   │   ├── migrate_to_sqlite_vec.py
│   │   ├── migrate_v5_enhanced.py
│   │   ├── TIMESTAMP_CLEANUP_README.md
│   │   └── verify_mcp_timestamps.py
│   ├── pr
│   │   ├── amp_collect_results.sh
│   │   ├── amp_detect_breaking_changes.sh
│   │   ├── amp_generate_tests.sh
│   │   ├── amp_pr_review.sh
│   │   ├── amp_quality_gate.sh
│   │   ├── amp_suggest_fixes.sh
│   │   ├── auto_review.sh
│   │   ├── detect_breaking_changes.sh
│   │   ├── generate_tests.sh
│   │   ├── lib
│   │   │   └── graphql_helpers.sh
│   │   ├── quality_gate.sh
│   │   ├── resolve_threads.sh
│   │   ├── run_pyscn_analysis.sh
│   │   ├── run_quality_checks.sh
│   │   ├── thread_status.sh
│   │   └── watch_reviews.sh
│   ├── quality
│   │   ├── fix_dead_code_install.sh
│   │   ├── phase1_dead_code_analysis.md
│   │   ├── phase2_complexity_analysis.md
│   │   ├── README_PHASE1.md
│   │   ├── README_PHASE2.md
│   │   ├── track_pyscn_metrics.sh
│   │   └── weekly_quality_review.sh
│   ├── README.md
│   ├── run
│   │   ├── run_mcp_memory.sh
│   │   ├── run-with-uv.sh
│   │   └── start_sqlite_vec.sh
│   ├── run_memory_server.py
│   ├── server
│   │   ├── check_http_server.py
│   │   ├── check_server_health.py
│   │   ├── memory_offline.py
│   │   ├── preload_models.py
│   │   ├── run_http_server.py
│   │   ├── run_memory_server.py
│   │   ├── start_http_server.bat
│   │   └── start_http_server.sh
│   ├── service
│   │   ├── deploy_dual_services.sh
│   │   ├── install_http_service.sh
│   │   ├── mcp-memory-http.service
│   │   ├── mcp-memory.service
│   │   ├── memory_service_manager.sh
│   │   ├── service_control.sh
│   │   ├── service_utils.py
│   │   └── update_service.sh
│   ├── sync
│   │   ├── check_drift.py
│   │   ├── claude_sync_commands.py
│   │   ├── export_memories.py
│   │   ├── import_memories.py
│   │   ├── litestream
│   │   │   ├── apply_local_changes.sh
│   │   │   ├── enhanced_memory_store.sh
│   │   │   ├── init_staging_db.sh
│   │   │   ├── io.litestream.replication.plist
│   │   │   ├── manual_sync.sh
│   │   │   ├── memory_sync.sh
│   │   │   ├── pull_remote_changes.sh
│   │   │   ├── push_to_remote.sh
│   │   │   ├── README.md
│   │   │   ├── resolve_conflicts.sh
│   │   │   ├── setup_local_litestream.sh
│   │   │   ├── setup_remote_litestream.sh
│   │   │   ├── staging_db_init.sql
│   │   │   ├── stash_local_changes.sh
│   │   │   ├── sync_from_remote_noconfig.sh
│   │   │   └── sync_from_remote.sh
│   │   ├── README.md
│   │   ├── safe_cloudflare_update.sh
│   │   ├── sync_memory_backends.py
│   │   └── sync_now.py
│   ├── testing
│   │   ├── run_complete_test.py
│   │   ├── run_memory_test.sh
│   │   ├── simple_test.py
│   │   ├── test_cleanup_logic.py
│   │   ├── test_cloudflare_backend.py
│   │   ├── test_docker_functionality.py
│   │   ├── test_installation.py
│   │   ├── test_mdns.py
│   │   ├── test_memory_api.py
│   │   ├── test_memory_simple.py
│   │   ├── test_migration.py
│   │   ├── test_search_api.py
│   │   ├── test_sqlite_vec_embeddings.py
│   │   ├── test_sse_events.py
│   │   ├── test-connection.py
│   │   └── test-hook.js
│   ├── utils
│   │   ├── claude_commands_utils.py
│   │   ├── generate_personalized_claude_md.sh
│   │   ├── groq
│   │   ├── groq_agent_bridge.py
│   │   ├── list-collections.py
│   │   ├── memory_wrapper_uv.py
│   │   ├── query_memories.py
│   │   ├── smithery_wrapper.py
│   │   ├── test_groq_bridge.sh
│   │   └── uv_wrapper.py
│   └── validation
│       ├── check_dev_setup.py
│       ├── check_documentation_links.py
│       ├── diagnose_backend_config.py
│       ├── validate_configuration_complete.py
│       ├── validate_memories.py
│       ├── validate_migration.py
│       ├── validate_timestamp_integrity.py
│       ├── verify_environment.py
│       ├── verify_pytorch_windows.py
│       └── verify_torch.py
├── SECURITY.md
├── selective_timestamp_recovery.py
├── SPONSORS.md
├── src
│   └── mcp_memory_service
│       ├── __init__.py
│       ├── api
│       │   ├── __init__.py
│       │   ├── client.py
│       │   ├── operations.py
│       │   ├── sync_wrapper.py
│       │   └── types.py
│       ├── backup
│       │   ├── __init__.py
│       │   └── scheduler.py
│       ├── cli
│       │   ├── __init__.py
│       │   ├── ingestion.py
│       │   ├── main.py
│       │   └── utils.py
│       ├── config.py
│       ├── consolidation
│       │   ├── __init__.py
│       │   ├── associations.py
│       │   ├── base.py
│       │   ├── clustering.py
│       │   ├── compression.py
│       │   ├── consolidator.py
│       │   ├── decay.py
│       │   ├── forgetting.py
│       │   ├── health.py
│       │   └── scheduler.py
│       ├── dependency_check.py
│       ├── discovery
│       │   ├── __init__.py
│       │   ├── client.py
│       │   └── mdns_service.py
│       ├── embeddings
│       │   ├── __init__.py
│       │   └── onnx_embeddings.py
│       ├── ingestion
│       │   ├── __init__.py
│       │   ├── base.py
│       │   ├── chunker.py
│       │   ├── csv_loader.py
│       │   ├── json_loader.py
│       │   ├── pdf_loader.py
│       │   ├── registry.py
│       │   ├── semtools_loader.py
│       │   └── text_loader.py
│       ├── lm_studio_compat.py
│       ├── mcp_server.py
│       ├── models
│       │   ├── __init__.py
│       │   └── memory.py
│       ├── server.py
│       ├── services
│       │   ├── __init__.py
│       │   └── memory_service.py
│       ├── storage
│       │   ├── __init__.py
│       │   ├── base.py
│       │   ├── cloudflare.py
│       │   ├── factory.py
│       │   ├── http_client.py
│       │   ├── hybrid.py
│       │   └── sqlite_vec.py
│       ├── sync
│       │   ├── __init__.py
│       │   ├── exporter.py
│       │   ├── importer.py
│       │   └── litestream_config.py
│       ├── utils
│       │   ├── __init__.py
│       │   ├── cache_manager.py
│       │   ├── content_splitter.py
│       │   ├── db_utils.py
│       │   ├── debug.py
│       │   ├── document_processing.py
│       │   ├── gpu_detection.py
│       │   ├── hashing.py
│       │   ├── http_server_manager.py
│       │   ├── port_detection.py
│       │   ├── system_detection.py
│       │   └── time_parser.py
│       └── web
│           ├── __init__.py
│           ├── api
│           │   ├── __init__.py
│           │   ├── analytics.py
│           │   ├── backup.py
│           │   ├── consolidation.py
│           │   ├── documents.py
│           │   ├── events.py
│           │   ├── health.py
│           │   ├── manage.py
│           │   ├── mcp.py
│           │   ├── memories.py
│           │   ├── search.py
│           │   └── sync.py
│           ├── app.py
│           ├── dependencies.py
│           ├── oauth
│           │   ├── __init__.py
│           │   ├── authorization.py
│           │   ├── discovery.py
│           │   ├── middleware.py
│           │   ├── models.py
│           │   ├── registration.py
│           │   └── storage.py
│           ├── sse.py
│           └── static
│               ├── app.js
│               ├── index.html
│               ├── README.md
│               ├── sse_test.html
│               └── style.css
├── start_http_debug.bat
├── start_http_server.sh
├── test_document.txt
├── test_version_checker.js
├── tests
│   ├── __init__.py
│   ├── api
│   │   ├── __init__.py
│   │   ├── test_compact_types.py
│   │   └── test_operations.py
│   ├── bridge
│   │   ├── mock_responses.js
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   └── test_http_mcp_bridge.js
│   ├── conftest.py
│   ├── consolidation
│   │   ├── __init__.py
│   │   ├── conftest.py
│   │   ├── test_associations.py
│   │   ├── test_clustering.py
│   │   ├── test_compression.py
│   │   ├── test_consolidator.py
│   │   ├── test_decay.py
│   │   └── test_forgetting.py
│   ├── contracts
│   │   └── api-specification.yml
│   ├── integration
│   │   ├── package-lock.json
│   │   ├── package.json
│   │   ├── test_api_key_fallback.py
│   │   ├── test_api_memories_chronological.py
│   │   ├── test_api_tag_time_search.py
│   │   ├── test_api_with_memory_service.py
│   │   ├── test_bridge_integration.js
│   │   ├── test_cli_interfaces.py
│   │   ├── test_cloudflare_connection.py
│   │   ├── test_concurrent_clients.py
│   │   ├── test_data_serialization_consistency.py
│   │   ├── test_http_server_startup.py
│   │   ├── test_mcp_memory.py
│   │   ├── test_mdns_integration.py
│   │   ├── test_oauth_basic_auth.py
│   │   ├── test_oauth_flow.py
│   │   ├── test_server_handlers.py
│   │   └── test_store_memory.py
│   ├── performance
│   │   ├── test_background_sync.py
│   │   └── test_hybrid_live.py
│   ├── README.md
│   ├── smithery
│   │   └── test_smithery.py
│   ├── sqlite
│   │   └── simple_sqlite_vec_test.py
│   ├── test_client.py
│   ├── test_content_splitting.py
│   ├── test_database.py
│   ├── test_hybrid_cloudflare_limits.py
│   ├── test_hybrid_storage.py
│   ├── test_memory_ops.py
│   ├── test_semantic_search.py
│   ├── test_sqlite_vec_storage.py
│   ├── test_time_parser.py
│   ├── test_timestamp_preservation.py
│   ├── timestamp
│   │   ├── test_hook_vs_manual_storage.py
│   │   ├── test_issue99_final_validation.py
│   │   ├── test_search_retrieval_inconsistency.py
│   │   ├── test_timestamp_issue.py
│   │   └── test_timestamp_simple.py
│   └── unit
│       ├── conftest.py
│       ├── test_cloudflare_storage.py
│       ├── test_csv_loader.py
│       ├── test_fastapi_dependencies.py
│       ├── test_import.py
│       ├── test_json_loader.py
│       ├── test_mdns_simple.py
│       ├── test_mdns.py
│       ├── test_memory_service.py
│       ├── test_memory.py
│       ├── test_semtools_loader.py
│       ├── test_storage_interface_compatibility.py
│       └── test_tag_time_filtering.py
├── tools
│   ├── docker
│   │   ├── DEPRECATED.md
│   │   ├── docker-compose.http.yml
│   │   ├── docker-compose.pythonpath.yml
│   │   ├── docker-compose.standalone.yml
│   │   ├── docker-compose.uv.yml
│   │   ├── docker-compose.yml
│   │   ├── docker-entrypoint-persistent.sh
│   │   ├── docker-entrypoint-unified.sh
│   │   ├── docker-entrypoint.sh
│   │   ├── Dockerfile
│   │   ├── Dockerfile.glama
│   │   ├── Dockerfile.slim
│   │   ├── README.md
│   │   └── test-docker-modes.sh
│   └── README.md
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/src/mcp_memory_service/web/static/app.js:
--------------------------------------------------------------------------------

```javascript
   1 | /**
   2 |  * MCP Memory Service Dashboard - Main Application
   3 |  * Interactive frontend for memory management with real-time updates
   4 |  */
   5 | 
   6 | console.log('⚡ app.js loading - TOP OF FILE');
   7 | 
   8 | class MemoryDashboard {
   9 |     // Delay between individual file uploads to avoid overwhelming the server (ms)
  10 |     static INDIVIDUAL_UPLOAD_DELAY = 500;
  11 | 
  12 |     // Static configuration for settings modal system information
  13 |     static SYSTEM_INFO_CONFIG = {
  14 |         settingsVersion: {
  15 |             sources: [{ path: 'version', api: 'health' }],
  16 |             formatter: (value) => value || 'N/A'
  17 |         },
  18 |         settingsBackend: {
  19 |             sources: [
  20 |                 { path: 'storage.storage_backend', api: 'detailedHealth' },
  21 |                 { path: 'storage.backend', api: 'detailedHealth' }
  22 |             ],
  23 |             formatter: (value) => value || 'N/A'
  24 |         },
  25 |         settingsPrimaryBackend: {
  26 |             sources: [
  27 |                 { path: 'storage.primary_backend', api: 'detailedHealth' },
  28 |                 { path: 'storage.backend', api: 'detailedHealth' }
  29 |             ],
  30 |             formatter: (value) => value || 'N/A'
  31 |         },
  32 |         settingsEmbeddingModel: {
  33 |             sources: [
  34 |                 { path: 'storage.primary_stats.embedding_model', api: 'detailedHealth' },
  35 |                 { path: 'storage.embedding_model', api: 'detailedHealth' }
  36 |             ],
  37 |             formatter: (value) => value || 'N/A'
  38 |         },
  39 |         settingsEmbeddingDim: {
  40 |             sources: [
  41 |                 { path: 'storage.primary_stats.embedding_dimension', api: 'detailedHealth' },
  42 |                 { path: 'storage.embedding_dimension', api: 'detailedHealth' }
  43 |             ],
  44 |             formatter: (value) => value || 'N/A'
  45 |         },
  46 |         settingsDbSize: {
  47 |             sources: [
  48 |                 { path: 'storage.primary_stats.database_size_mb', api: 'detailedHealth' },
  49 |                 { path: 'storage.database_size_mb', api: 'detailedHealth' }
  50 |             ],
  51 |             formatter: (value) => (value != null) ? `${value.toFixed(2)} MB` : 'N/A'
  52 |         },
  53 |         settingsTotalMemories: {
  54 |             sources: [{ path: 'storage.total_memories', api: 'detailedHealth' }],
  55 |             formatter: (value) => (value != null) ? value.toLocaleString() : 'N/A'
  56 |         },
  57 |         settingsUptime: {
  58 |             sources: [{ path: 'uptime_seconds', api: 'detailedHealth' }],
  59 |             formatter: (value) => (value != null) ? MemoryDashboard.formatUptime(value) : 'N/A'
  60 |         }
  61 |     };
  62 | 
  63 |     constructor() {
  64 |         this.apiBase = '/api';
  65 |         this.eventSource = null;
  66 |         this.memories = [];
  67 |         this.currentView = 'dashboard';
  68 |         this.searchResults = [];
  69 |         this.isLoading = false;
  70 |         this.liveSearchEnabled = true;
  71 |         this.debounceTimer = null;
  72 | 
  73 |         // Settings with defaults
  74 |         this.settings = {
  75 |             theme: 'light',
  76 |             viewDensity: 'comfortable',
  77 |             previewLines: 3
  78 |         };
  79 | 
  80 |         // Documents upload state
  81 |         this.selectedFiles = [];
  82 |         this.documentsListenersSetup = false;
  83 |         this.processingMode = 'batch'; // 'batch' or 'individual'
  84 | 
  85 |         // Bind methods
  86 |         this.handleSearch = this.handleSearch.bind(this);
  87 |         this.handleQuickSearch = this.handleQuickSearch.bind(this);
  88 |         this.handleNavigation = this.handleNavigation.bind(this);
  89 |         this.handleAddMemory = this.handleAddMemory.bind(this);
  90 |         this.handleMemoryClick = this.handleMemoryClick.bind(this);
  91 | 
  92 |         this.init();
  93 |     }
  94 | 
  95 |     /**
  96 |      * Initialize the application
  97 |      */
  98 |     async init() {
  99 |         this.loadSettings();
 100 |         this.applyTheme();
 101 |         this.setupEventListeners();
 102 |         this.setupSSE();
 103 |         await this.loadVersion();
 104 |         await this.loadDashboardData();
 105 |         this.updateConnectionStatus('connected');
 106 | 
 107 |         // Initialize sync status monitoring for hybrid mode
 108 |         await this.checkSyncStatus();
 109 |         this.startSyncStatusMonitoring();
 110 |     }
 111 | 
 112 |     /**
 113 |      * Set up event listeners for UI interactions
 114 |      */
 115 |     setupEventListeners() {
 116 |         console.log('⚡ setupEventListeners() called');
 117 |         // Navigation
 118 |         document.querySelectorAll('.nav-item').forEach(item => {
 119 |             item.addEventListener('click', this.handleNavigation);
 120 |         });
 121 | 
 122 |         // Search functionality
 123 |         const quickSearch = document.getElementById('quickSearch');
 124 |         const searchBtn = document.querySelector('.search-btn');
 125 | 
 126 |         if (quickSearch) {
 127 |             quickSearch.addEventListener('input', this.debounce(this.handleQuickSearch, 300));
 128 |             quickSearch.addEventListener('keypress', (e) => {
 129 |                 if (e.key === 'Enter') {
 130 |                     this.handleSearch(e.target.value);
 131 |                 }
 132 |             });
 133 |         }
 134 | 
 135 |         if (searchBtn && quickSearch) {
 136 |             searchBtn.addEventListener('click', () => {
 137 |                 this.handleSearch(quickSearch.value);
 138 |             });
 139 |         }
 140 | 
 141 |         // Add memory functionality
 142 |         const addMemoryBtn = document.getElementById('addMemoryBtn');
 143 |         if (addMemoryBtn) {
 144 |             addMemoryBtn.addEventListener('click', this.handleAddMemory);
 145 |         }
 146 |         document.querySelectorAll('[data-action="add-memory"]').forEach(btn => {
 147 |             btn.addEventListener('click', this.handleAddMemory);
 148 |         });
 149 | 
 150 |         // Modal close handlers
 151 |         document.querySelectorAll('.modal-close').forEach(btn => {
 152 |             btn.addEventListener('click', (e) => {
 153 |                 this.closeModal(e.target.closest('.modal-overlay'));
 154 |             });
 155 |         });
 156 | 
 157 |         // Modal overlay click to close
 158 |         document.querySelectorAll('.modal-overlay').forEach(overlay => {
 159 |             overlay.addEventListener('click', (e) => {
 160 |                 if (e.target === overlay) {
 161 |                     this.closeModal(overlay);
 162 |                 }
 163 |             });
 164 |         });
 165 | 
 166 |         // Add memory form submission
 167 |         const saveMemoryBtn = document.getElementById('saveMemoryBtn');
 168 |         if (saveMemoryBtn) {
 169 |             saveMemoryBtn.addEventListener('click', this.handleSaveMemory.bind(this));
 170 |         }
 171 | 
 172 |         const cancelAddBtn = document.getElementById('cancelAddBtn');
 173 |         if (cancelAddBtn) {
 174 |             cancelAddBtn.addEventListener('click', () => {
 175 |                 this.closeModal(document.getElementById('addMemoryModal'));
 176 |             });
 177 |         }
 178 | 
 179 |         // Quick action handlers
 180 |         document.querySelectorAll('.action-card').forEach(card => {
 181 |             card.addEventListener('click', (e) => {
 182 |                 const action = e.currentTarget.dataset.action;
 183 |                 this.handleQuickAction(action);
 184 |             });
 185 |         });
 186 | 
 187 |         // Live search toggle handler
 188 |         const liveSearchToggle = document.getElementById('liveSearchToggle');
 189 |         liveSearchToggle?.addEventListener('change', this.handleLiveSearchToggle.bind(this));
 190 | 
 191 |         // Filter handlers for search view
 192 |         const tagFilterInput = document.getElementById('tagFilter');
 193 |         tagFilterInput?.addEventListener('input', this.handleDebouncedFilterChange.bind(this));
 194 |         tagFilterInput?.addEventListener('keypress', (e) => {
 195 |             if (e.key === 'Enter') {
 196 |                 this.handleFilterChange();
 197 |             }
 198 |         });
 199 |         document.getElementById('dateFilter')?.addEventListener('change', this.handleFilterChange.bind(this));
 200 |         document.getElementById('typeFilter')?.addEventListener('change', this.handleFilterChange.bind(this));
 201 | 
 202 |         // View option handlers
 203 |         document.querySelectorAll('.view-btn').forEach(btn => {
 204 |             btn.addEventListener('click', (e) => {
 205 |                 this.handleViewModeChange(e.target.dataset.view);
 206 |             });
 207 |         });
 208 | 
 209 |         // New filter action handlers
 210 |         document.getElementById('applyFiltersBtn')?.addEventListener('click', this.handleFilterChange.bind(this));
 211 |         document.getElementById('clearFiltersBtn')?.addEventListener('click', this.clearAllFilters.bind(this));
 212 | 
 213 |         // Theme toggle button
 214 |         document.getElementById('themeToggleBtn')?.addEventListener('click', () => {
 215 |             this.toggleTheme();
 216 |         });
 217 | 
 218 |         // Settings button
 219 |         document.getElementById('settingsBtn')?.addEventListener('click', () => {
 220 |             this.openSettingsModal();
 221 |         });
 222 | 
 223 |         // Settings modal handlers
 224 |         document.getElementById('saveSettingsBtn')?.addEventListener('click', () => {
 225 |             this.saveSettings();
 226 |         });
 227 | 
 228 |         document.getElementById('cancelSettingsBtn')?.addEventListener('click', () => {
 229 |             this.closeModal(document.getElementById('settingsModal'));
 230 |         });
 231 | 
 232 |         // Tag cloud event delegation
 233 |         document.getElementById('tagsCloudContainer')?.addEventListener('click', (e) => {
 234 |             if (e.target.classList.contains('tag-bubble') || e.target.closest('.tag-bubble')) {
 235 |                 const tagButton = e.target.classList.contains('tag-bubble') ? e.target : e.target.closest('.tag-bubble');
 236 |                 const tag = tagButton.dataset.tag;
 237 |                 if (tag) {
 238 |                     this.filterByTag(tag);
 239 |                 }
 240 |             }
 241 |         });
 242 | 
 243 |         // Manage tab event listeners
 244 |         document.getElementById('deleteByTagBtn')?.addEventListener('click', this.handleBulkDeleteByTag.bind(this));
 245 |         document.getElementById('cleanupDuplicatesBtn')?.addEventListener('click', this.handleCleanupDuplicates.bind(this));
 246 |         document.getElementById('deleteByDateBtn')?.addEventListener('click', this.handleBulkDeleteByDate.bind(this));
 247 |         document.getElementById('optimizeDbBtn')?.addEventListener('click', this.handleOptimizeDatabase.bind(this));
 248 |         document.getElementById('rebuildIndexBtn')?.addEventListener('click', this.handleRebuildIndex.bind(this));
 249 | 
 250 |         // Analytics tab event listeners
 251 |         document.getElementById('growthPeriodSelect')?.addEventListener('change', this.handleGrowthPeriodChange.bind(this));
 252 |         document.getElementById('heatmapPeriodSelect')?.addEventListener('change', this.handleHeatmapPeriodChange.bind(this));
 253 |         document.getElementById('topTagsPeriodSelect')?.addEventListener('change', this.handleTopTagsPeriodChange.bind(this));
 254 |         document.getElementById('activityGranularitySelect')?.addEventListener('change', this.handleActivityGranularityChange.bind(this));
 255 | 
 256 |         // Keyboard shortcuts
 257 |         document.addEventListener('keydown', (e) => {
 258 |             if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
 259 |                 e.preventDefault();
 260 |                 document.getElementById('searchInput').focus();
 261 |             }
 262 |             if ((e.ctrlKey || e.metaKey) && e.key === 'm') {
 263 |                 e.preventDefault();
 264 |                 this.handleAddMemory();
 265 |             }
 266 |         });
 267 |     }
 268 | 
 269 |     /**
 270 |      * Set up Server-Sent Events for real-time updates
 271 |      */
 272 |     setupSSE() {
 273 |         try {
 274 |             this.eventSource = new EventSource(`${this.apiBase}/events`);
 275 | 
 276 |             this.eventSource.onopen = () => {
 277 |                 this.updateConnectionStatus('connected');
 278 |             };
 279 | 
 280 |             this.eventSource.onmessage = (event) => {
 281 |                 try {
 282 |                     const data = JSON.parse(event.data);
 283 |                     this.handleRealtimeUpdate(data);
 284 |                 } catch (error) {
 285 |                     console.error('Error parsing SSE data:', error);
 286 |                 }
 287 |             };
 288 | 
 289 |             // Add specific event listeners for sync progress
 290 |             this.eventSource.addEventListener('sync_progress', (event) => {
 291 |                 try {
 292 |                     const data = JSON.parse(event.data);
 293 |                     this.handleSyncProgress(data);
 294 |                 } catch (error) {
 295 |                     console.error('Error parsing sync_progress event:', error);
 296 |                 }
 297 |             });
 298 | 
 299 |             this.eventSource.addEventListener('sync_completed', (event) => {
 300 |                 try {
 301 |                     const data = JSON.parse(event.data);
 302 |                     this.handleSyncCompleted(data);
 303 |                 } catch (error) {
 304 |                     console.error('Error parsing sync_completed event:', error);
 305 |                 }
 306 |             });
 307 | 
 308 |             this.eventSource.onerror = (error) => {
 309 |                 console.error('SSE connection error:', error);
 310 |                 this.updateConnectionStatus('disconnected');
 311 | 
 312 |                 // Attempt to reconnect after 5 seconds
 313 |                 setTimeout(() => {
 314 |                     if (this.eventSource.readyState === EventSource.CLOSED) {
 315 |                         this.setupSSE();
 316 |                     }
 317 |                 }, 5000);
 318 |             };
 319 | 
 320 |         } catch (error) {
 321 |             console.error('Failed to establish SSE connection:', error);
 322 |             this.updateConnectionStatus('disconnected');
 323 |         }
 324 |     }
 325 | 
 326 |     /**
 327 |      * Handle real-time updates from SSE
 328 |      */
 329 |     handleRealtimeUpdate(data) {
 330 |         switch (data.type) {
 331 |             case 'memory_added':
 332 |                 this.handleMemoryAdded(data.memory);
 333 |                 this.showToast('Memory added successfully', 'success');
 334 |                 break;
 335 |             case 'memory_deleted':
 336 |                 this.handleMemoryDeleted(data.memory_id);
 337 |                 this.showToast('Memory deleted', 'success');
 338 |                 break;
 339 |             case 'memory_updated':
 340 |                 this.handleMemoryUpdated(data.memory);
 341 |                 this.showToast('Memory updated', 'success');
 342 |                 break;
 343 |             case 'stats_updated':
 344 |                 this.updateDashboardStats(data.stats);
 345 |                 break;
 346 |             default:
 347 |                 // Unknown event type - ignore silently
 348 |         }
 349 |     }
 350 | 
 351 |     /**
 352 |      * Handle sync progress updates from SSE
 353 |      */
 354 |     handleSyncProgress(data) {
 355 |         console.log('Sync progress:', data);
 356 | 
 357 |         // Update sync status display if visible
 358 |         const syncStatus = document.getElementById('syncStatus');
 359 |         if (syncStatus) {
 360 |             const progressText = `Syncing: ${data.synced_count}/${data.total_count} (${data.progress_percentage}%)`;
 361 |             syncStatus.textContent = progressText;
 362 |             syncStatus.className = 'sync-status syncing';
 363 |         }
 364 | 
 365 |         // Update memory count in real-time if on dashboard
 366 |         if (this.currentView === 'dashboard') {
 367 |             const memoryCountElement = document.getElementById('totalMemories');
 368 |             if (memoryCountElement && data.synced_count) {
 369 |                 // Refresh the detailed health to get accurate count
 370 |                 this.loadDashboardData().catch(err => console.error('Error refreshing dashboard:', err));
 371 |             }
 372 |         }
 373 | 
 374 |         // Show toast notification for manual sync
 375 |         if (data.sync_type === 'manual') {
 376 |             this.showToast(data.message || `Syncing: ${data.synced_count}/${data.total_count}`, 'info');
 377 |         }
 378 |     }
 379 | 
 380 |     /**
 381 |      * Handle sync completion from SSE
 382 |      */
 383 |     handleSyncCompleted(data) {
 384 |         console.log('Sync completed:', data);
 385 | 
 386 |         // Update sync status display
 387 |         const syncStatus = document.getElementById('syncStatus');
 388 |         if (syncStatus) {
 389 |             syncStatus.textContent = 'Synced';
 390 |             syncStatus.className = 'sync-status synced';
 391 |         }
 392 | 
 393 |         // Refresh dashboard data to show updated counts
 394 |         if (this.currentView === 'dashboard') {
 395 |             this.loadDashboardData().catch(err => console.error('Error refreshing dashboard:', err));
 396 |         }
 397 | 
 398 |         // Also refresh sync status for hybrid mode
 399 |         this.checkSyncStatus().catch(err => console.error('Error checking sync status:', err));
 400 | 
 401 |         // Show completion notification
 402 |         const message = data.message || `Sync completed: ${data.synced_count} memories synced`;
 403 |         this.showToast(message, 'success');
 404 |     }
 405 | 
 406 |     /**
 407 |      * Load application version from health endpoint
 408 |      */
 409 |     async loadVersion() {
 410 |         try {
 411 |             const healthResponse = await this.apiCall('/health');
 412 |             const versionBadge = document.getElementById('versionBadge');
 413 |             if (versionBadge && healthResponse.version) {
 414 |                 versionBadge.textContent = `v${healthResponse.version}`;
 415 |             }
 416 |         } catch (error) {
 417 |             console.error('Error loading version:', error);
 418 |             const versionBadge = document.getElementById('versionBadge');
 419 |             if (versionBadge) {
 420 |                 versionBadge.textContent = 'v?.?.?';
 421 |             }
 422 |         }
 423 |     }
 424 | 
 425 |     /**
 426 |      * Load initial dashboard data
 427 |      */
 428 |     async loadDashboardData() {
 429 |         this.setLoading(true);
 430 | 
 431 |         try {
 432 |             // Load recent memories for dashboard display
 433 |             const memoriesResponse = await this.apiCall('/memories?page=1&page_size=100');
 434 |             if (memoriesResponse.memories) {
 435 |                 this.memories = memoriesResponse.memories;
 436 |                 this.renderRecentMemories(memoriesResponse.memories);
 437 |             }
 438 | 
 439 |             // Load basic statistics
 440 |             const statsResponse = await this.apiCall('/health/detailed');
 441 |             if (statsResponse.storage) {
 442 |                 this.updateDashboardStats(statsResponse.storage);
 443 |             }
 444 | 
 445 | 
 446 |         } catch (error) {
 447 |             console.error('Error loading dashboard data:', error);
 448 |             this.showToast('Failed to load dashboard data', 'error');
 449 |         } finally {
 450 |             this.setLoading(false);
 451 |         }
 452 |     }
 453 | 
 454 |     /**
 455 |      * Load browse view data (tags)
 456 |      */
 457 |     async loadBrowseData() {
 458 |         this.setLoading(true);
 459 |         try {
 460 |             // Load tags with counts from the dedicated endpoint
 461 |             const tagsResponse = await this.apiCall('/tags');
 462 |             if (tagsResponse.tags) {
 463 |                 this.tags = tagsResponse.tags;
 464 |                 this.renderTagsCloud();
 465 |             }
 466 |         } catch (error) {
 467 |             console.error('Error loading browse data:', error);
 468 |             this.showToast('Failed to load browse data', 'error');
 469 |         } finally {
 470 |             this.setLoading(false);
 471 |         }
 472 |     }
 473 | 
 474 |     /**
 475 |      * Load documents view data
 476 |      */
 477 |     async loadDocumentsData() {
 478 |         this.setLoading(true);
 479 |         try {
 480 |             // Load upload history
 481 |             await this.loadUploadHistory();
 482 |             // Setup document upload event listeners
 483 |             this.setupDocumentsEventListeners();
 484 |         } catch (error) {
 485 |             console.error('Error loading documents data:', error);
 486 |             this.showToast('Failed to load documents data', 'error');
 487 |         } finally {
 488 |             this.setLoading(false);
 489 |         }
 490 |     }
 491 | 
 492 |     /**
 493 |      * Load upload history from API
 494 |      */
 495 |     async loadUploadHistory() {
 496 |         console.log('Loading upload history...');
 497 |         try {
 498 |             const historyResponse = await this.apiCall('/documents/history');
 499 |             console.log('Upload history response:', historyResponse);
 500 |             if (historyResponse.uploads) {
 501 |                 this.renderUploadHistory(historyResponse.uploads);
 502 |             } else {
 503 |                 console.warn('No uploads property in response');
 504 |                 this.renderUploadHistory([]);
 505 |             }
 506 |         } catch (error) {
 507 |             console.error('Error loading upload history:', error);
 508 |             // Show a message in the history container instead of just logging
 509 |             const historyContainer = document.getElementById('uploadHistory');
 510 |             if (historyContainer) {
 511 |                 historyContainer.innerHTML = '<p style="text-align: center; color: var(--error);">Failed to load upload history. Please check the console for details.</p>';
 512 |             }
 513 |         }
 514 |     }
 515 | 
 516 |     /**
 517 |      * Render upload history
 518 |      */
 519 |     renderUploadHistory(uploads) {
 520 |         const historyContainer = document.getElementById('uploadHistory');
 521 |         if (!historyContainer) return;
 522 | 
 523 |         if (uploads.length === 0) {
 524 |             historyContainer.innerHTML = '<p style="text-align: center; color: var(--neutral-500);">No uploads yet. Start by uploading some documents!</p>';
 525 |             return;
 526 |         }
 527 | 
 528 |         const historyHtml = uploads.map(upload => {
 529 |         const statusClass = upload.status.toLowerCase();
 530 |         const statusText = upload.status.charAt(0).toUpperCase() + upload.status.slice(1);
 531 |         const progressPercent = upload.progress || 0;
 532 |         const hasMemories = upload.chunks_stored > 0;
 533 | 
 534 |         return `
 535 |         <div class="upload-item ${statusClass}" data-upload-id="${upload.upload_id}" data-filename="${this.escapeHtml(upload.filename)}">
 536 |         <div class="upload-info">
 537 |         <div class="upload-filename">${this.escapeHtml(upload.filename)}</div>
 538 |                         <div class="upload-meta">
 539 |                             ${upload.chunks_stored || 0} chunks stored •
 540 |                             ${(upload.file_size / 1024).toFixed(1)} KB •
 541 |                             ${new Date(upload.created_at).toLocaleString()}
 542 |                         </div>
 543 |                         ${upload.status === 'processing' ? `
 544 |                             <div class="progress-bar">
 545 |                                 <div class="progress-fill" style="width: ${progressPercent}%"></div>
 546 |                             </div>
 547 |                         ` : ''}
 548 |                     </div>
 549 |                     <div class="upload-actions-container">
 550 |                         <div class="upload-status ${statusClass}">
 551 |                             <span>${statusText}</span>
 552 |                             ${upload.errors && upload.errors.length > 0 ? `
 553 |                                 <span title="${this.escapeHtml(upload.errors.join('; '))}">⚠️</span>
 554 |                             ` : ''}
 555 |                         </div>
 556 |                         ${upload.status === 'completed' && hasMemories ? `
 557 |                             <div class="upload-actions">
 558 |                                 <button class="btn-icon btn-view-memory"
 559 |                                 title="View memory chunks">
 560 |                                     <svg width="16" height="16" fill="currentColor" viewBox="0 0 24 24">
 561 |                                         <path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/>
 562 |                                     </svg>
 563 |                                     <span>View</span>
 564 |                                 </button>
 565 |                                 <button class="btn-icon btn-remove"
 566 |                                 title="Remove document and memories">
 567 |                                     <svg width="16" height="16" fill="currentColor" viewBox="0 0 24 24">
 568 |                                         <path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
 569 |                                     </svg>
 570 |                                     <span>Remove</span>
 571 |                                 </button>
 572 |                             </div>
 573 |                         ` : ''}
 574 |                     </div>
 575 |                 </div>
 576 |             `;
 577 |         }).join('');
 578 | 
 579 |         historyContainer.innerHTML = historyHtml;
 580 |     }
 581 | 
 582 |     /**
 583 |      * Setup event listeners for documents view
 584 |      */
 585 |     setupDocumentsEventListeners() {
 586 |         // Prevent duplicate event listener setup
 587 |         if (this.documentsListenersSetup) {
 588 |             console.log('Document listeners already set up, skipping...');
 589 |             return;
 590 |         }
 591 | 
 592 |         // File selection buttons
 593 |         const fileSelectBtn = document.getElementById('fileSelectBtn');
 594 |         const fileInput = document.getElementById('fileInput');
 595 | 
 596 |         if (fileSelectBtn && fileInput) {
 597 |             fileSelectBtn.addEventListener('click', () => {
 598 |                 fileInput.click();
 599 |             });
 600 | 
 601 |             fileInput.addEventListener('change', (e) => {
 602 |                 this.handleFileSelection(e.target.files);
 603 |             });
 604 |         }
 605 | 
 606 |         // Drag and drop
 607 |         const dropZone = document.getElementById('dropZone');
 608 |         if (dropZone) {
 609 |             dropZone.addEventListener('dragover', (e) => {
 610 |                 e.preventDefault();
 611 |                 dropZone.classList.add('drag-over');
 612 |             });
 613 | 
 614 |             dropZone.addEventListener('dragleave', (e) => {
 615 |                 e.preventDefault();
 616 |                 dropZone.classList.remove('drag-over');
 617 |             });
 618 | 
 619 |             dropZone.addEventListener('drop', (e) => {
 620 |                 e.preventDefault();
 621 |                 dropZone.classList.remove('drag-over');
 622 |                 const files = e.dataTransfer.files;
 623 |                 this.handleFileSelection(files);
 624 |             });
 625 |         }
 626 | 
 627 |         // Configuration controls - cache for performance
 628 |         this.chunkSizeInput = document.getElementById('chunkSize');
 629 |         this.chunkOverlapInput = document.getElementById('chunkOverlap');
 630 |         this.memoryTypeInput = document.getElementById('memoryType');
 631 |         const chunkSizeValue = document.getElementById('chunkSizeValue');
 632 |         const chunkOverlapValue = document.getElementById('chunkOverlapValue');
 633 | 
 634 |         if (this.chunkSizeInput && chunkSizeValue) {
 635 |             this.chunkSizeInput.addEventListener('input', (e) => {
 636 |                 chunkSizeValue.textContent = e.target.value;
 637 |                 this.updateUploadButton();
 638 |             });
 639 |         }
 640 | 
 641 |         // Chunking help info icon
 642 |         const infoIcon = document.querySelector('.info-icon');
 643 |         if (infoIcon) {
 644 |             infoIcon.addEventListener('click', () => {
 645 |                 this.toggleChunkingHelp();
 646 |             });
 647 |         }
 648 | 
 649 |         // Overlap help info icon
 650 |         const overlapInfoIcon = document.querySelector('.info-icon-overlap');
 651 |         if (overlapInfoIcon) {
 652 |             overlapInfoIcon.addEventListener('click', () => {
 653 |                 this.toggleOverlapHelp();
 654 |             });
 655 |         }
 656 | 
 657 |         // Processing mode help info icon
 658 |         const processingModeInfoIcon = document.querySelector('.info-icon-processing');
 659 |         if (processingModeInfoIcon) {
 660 |             processingModeInfoIcon.addEventListener('click', () => {
 661 |                 this.toggleProcessingModeHelp();
 662 |             });
 663 |         }
 664 | 
 665 |         if (this.chunkOverlapInput && chunkOverlapValue) {
 666 |             this.chunkOverlapInput.addEventListener('input', (e) => {
 667 |                 chunkOverlapValue.textContent = e.target.value;
 668 |                 this.updateUploadButton();
 669 |             });
 670 |         }
 671 | 
 672 |         // Processing mode toggle buttons - cache for performance
 673 |         this.batchModeBtn = document.getElementById('batchModeBtn');
 674 |         this.individualModeBtn = document.getElementById('individualModeBtn');
 675 |         this.modeDescription = document.getElementById('modeDescription');
 676 | 
 677 |         if (this.batchModeBtn) {
 678 |             this.batchModeBtn.addEventListener('click', () => {
 679 |                 this.setProcessingMode('batch');
 680 |             });
 681 |         }
 682 | 
 683 |         if (this.individualModeBtn) {
 684 |             this.individualModeBtn.addEventListener('click', () => {
 685 |                 this.setProcessingMode('individual');
 686 |             });
 687 |         }
 688 | 
 689 |         // Upload button
 690 |         const uploadBtn = document.getElementById('uploadBtn');
 691 |         if (uploadBtn) {
 692 |             uploadBtn.addEventListener('click', () => {
 693 |                 this.handleDocumentUpload();
 694 |             });
 695 |         }
 696 | 
 697 |         // Add event listeners for buttons with data-action attribute
 698 |         document.querySelectorAll('[data-action]').forEach(button => {
 699 |             button.addEventListener('click', (e) => {
 700 |                 const action = e.currentTarget.dataset.action;
 701 |                 if (this[action] && typeof this[action] === 'function') {
 702 |                     this[action]();
 703 |                 }
 704 |             });
 705 |         });
 706 | 
 707 |         // Sync buttons event listeners are attached in checkSyncStatus()
 708 |         // after buttons are confirmed to be accessible in the DOM
 709 | 
 710 |         // Backup now button
 711 |         const backupNowButton = document.getElementById('backupNowButton');
 712 |         if (backupNowButton) {
 713 |             backupNowButton.addEventListener('click', () => {
 714 |                 this.createBackup();
 715 |             });
 716 |         }
 717 | 
 718 |         // Document search button
 719 |         const docSearchBtn = document.getElementById('docSearchBtn');
 720 |         const docSearchInput = document.getElementById('docSearchInput');
 721 |         if (docSearchBtn && docSearchInput) {
 722 |             docSearchBtn.addEventListener('click', () => {
 723 |                 const query = docSearchInput.value.trim();
 724 |                 if (query) {
 725 |                     this.searchDocumentContent(query);
 726 |                 } else {
 727 |                     this.showToast('Please enter a search query', 'warning');
 728 |                 }
 729 |             });
 730 | 
 731 |             // Enter key to search
 732 |             docSearchInput.addEventListener('keypress', (e) => {
 733 |                 if (e.key === 'Enter') {
 734 |                     const query = docSearchInput.value.trim();
 735 |                     if (query) {
 736 |                         this.searchDocumentContent(query);
 737 |                     }
 738 |                 }
 739 |             });
 740 |         }
 741 | 
 742 |         // Upload history action buttons (event delegation)
 743 |         const uploadHistory = document.getElementById('uploadHistory');
 744 |         if (uploadHistory) {
 745 |             uploadHistory.addEventListener('click', (e) => {
 746 |                 const button = e.target.closest('.btn-view-memory, .btn-remove');
 747 |                 if (!button) return;
 748 | 
 749 |                 const uploadItem = button.closest('.upload-item');
 750 |                 const uploadId = uploadItem?.dataset.uploadId;
 751 |                 const filename = uploadItem?.dataset.filename;
 752 | 
 753 |                 if (!uploadId) return;
 754 | 
 755 |                 if (button.classList.contains('btn-view-memory')) {
 756 |                     this.viewDocumentMemory(uploadId);
 757 |                 } else if (button.classList.contains('btn-remove')) {
 758 |                     this.removeDocument(uploadId, filename);
 759 |                 }
 760 |             });
 761 |         }
 762 | 
 763 |         // Close modal when clicking outside
 764 |         const memoryViewerModal = document.getElementById('memoryViewerModal');
 765 |         if (memoryViewerModal) {
 766 |             memoryViewerModal.addEventListener('click', (e) => {
 767 |                 if (e.target === memoryViewerModal) {
 768 |                     this.closeMemoryViewer();
 769 |                 }
 770 |             });
 771 |         }
 772 | 
 773 |         // Mark listeners as set up to prevent duplicates
 774 |         this.documentsListenersSetup = true;
 775 |         console.log('Document listeners setup complete');
 776 |     }
 777 | 
 778 |     /**
 779 |      * Handle file selection from input or drag-drop
 780 |      */
 781 |     handleFileSelection(files) {
 782 |         if (!files || files.length === 0) return;
 783 | 
 784 |         this.selectedFiles = Array.from(files);
 785 |         this.updateUploadButton();
 786 | 
 787 |         // Show file preview in drop zone
 788 |         const dropZone = document.getElementById('dropZone');
 789 |         if (dropZone) {
 790 |             const fileNames = this.selectedFiles.map(f => this.escapeHtml(f.name)).join(', ');
 791 |             const content = dropZone.querySelector('.drop-zone-content');
 792 |             if (content) {
 793 |                 content.innerHTML = `
 794 |                     <svg width="48" height="48" fill="currentColor" viewBox="0 0 24 24">
 795 |                         <path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/>
 796 |                     </svg>
 797 |                     <h3>${files.length} file${files.length > 1 ? 's' : ''} selected</h3>
 798 |                     <p>${fileNames}</p>
 799 |                     <input type="file" id="fileInput" multiple accept=".pdf,.docx,.pptx,.txt,.md,.json" style="display: none;">
 800 |                 `;
 801 |             }
 802 |         }
 803 |     }
 804 | 
 805 |     /**
 806 |      * Update upload button state based on selections
 807 |      */
 808 |     updateUploadButton() {
 809 |         const uploadBtn = document.getElementById('uploadBtn');
 810 |         const hasFiles = this.selectedFiles && this.selectedFiles.length > 0;
 811 | 
 812 |         if (uploadBtn) {
 813 |             uploadBtn.disabled = !hasFiles;
 814 |             uploadBtn.textContent = hasFiles ?
 815 |                 `Upload & Ingest ${this.selectedFiles.length} file${this.selectedFiles.length > 1 ? 's' : ''}` :
 816 |                 'Upload & Ingest';
 817 |         }
 818 | 
 819 |         // Show/hide processing mode section based on file count
 820 |         const processingModeSection = document.getElementById('processingModeSection');
 821 |         if (processingModeSection) {
 822 |             processingModeSection.style.display = (hasFiles && this.selectedFiles.length > 1) ? 'block' : 'none';
 823 |         }
 824 |     }
 825 | 
 826 |     /**
 827 |      * Set processing mode (batch or individual)
 828 |      */
 829 |     setProcessingMode(mode) {
 830 |         this.processingMode = mode;
 831 | 
 832 |         // Update button states (using cached DOM elements)
 833 |         if (this.batchModeBtn) {
 834 |             this.batchModeBtn.classList.toggle('active', mode === 'batch');
 835 |         }
 836 |         if (this.individualModeBtn) {
 837 |             this.individualModeBtn.classList.toggle('active', mode === 'individual');
 838 |         }
 839 |         if (this.modeDescription) {
 840 |             this.modeDescription.innerHTML = mode === 'batch'
 841 |                 ? '<small>All selected files will be processed together with the same tags.</small>'
 842 |                 : '<small>Each file will be processed individually with the same tags.</small>';
 843 |         }
 844 | 
 845 |         console.log(`Processing mode set to: ${mode}`);
 846 |     }
 847 | 
 848 |     /**
 849 |      * Handle document upload
 850 |      */
 851 |     async handleDocumentUpload() {
 852 |         if (!this.selectedFiles || this.selectedFiles.length === 0) {
 853 |             this.showToast('No files selected', 'error');
 854 |             return;
 855 |         }
 856 | 
 857 |         const tags = document.getElementById('docTags')?.value || '';
 858 |         const chunkSize = this.chunkSizeInput?.value || 1000;
 859 |         const chunkOverlap = this.chunkOverlapInput?.value || 200;
 860 |         const memoryType = this.memoryTypeInput?.value || 'document';
 861 | 
 862 |         try {
 863 |             this.setLoading(true);
 864 | 
 865 |             if (this.selectedFiles.length === 1 || this.processingMode === 'individual') {
 866 |                 // Individual file processing (single file or individual mode for multiple files)
 867 |                 for (let i = 0; i < this.selectedFiles.length; i++) {
 868 |                     const file = this.selectedFiles[i];
 869 |                     try {
 870 |                         await this.uploadSingleDocument(file, {
 871 |                             tags,
 872 |                             chunk_size: parseInt(chunkSize),
 873 |                             chunk_overlap: parseInt(chunkOverlap),
 874 |                             memory_type: memoryType
 875 |                         });
 876 | 
 877 |                         // Small delay between individual uploads to avoid overwhelming the server
 878 |                         if (i < this.selectedFiles.length - 1) {
 879 |                             await new Promise(resolve => setTimeout(resolve, this.constructor.INDIVIDUAL_UPLOAD_DELAY));
 880 |                         }
 881 |                     } catch (error) {
 882 |                         console.error(`Failed to upload ${file.name}:`, error);
 883 |                         this.showToast(`Failed to upload ${file.name}: ${error.message}`, 'error');
 884 |                         // Continue with remaining files
 885 |                     }
 886 |                 }
 887 |             } else {
 888 |                 // Batch upload
 889 |                 await this.uploadBatchDocuments(this.selectedFiles, {
 890 |                     tags,
 891 |                     chunk_size: parseInt(chunkSize),
 892 |                     chunk_overlap: parseInt(chunkOverlap),
 893 |                     memory_type: memoryType
 894 |                 });
 895 |             }
 896 | 
 897 |             // Clear selection and reload history
 898 |             this.selectedFiles = [];
 899 |             this.updateUploadButton();
 900 |             await this.loadUploadHistory();
 901 | 
 902 |             // Reset drop zone
 903 |             const dropZone = document.getElementById('dropZone');
 904 |             if (dropZone) {
 905 |                 const content = dropZone.querySelector('.drop-zone-content');
 906 |                 if (content) {
 907 |                     content.innerHTML = `
 908 |                         <svg width="48" height="48" fill="currentColor" viewBox="0 0 24 24">
 909 |                             <path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z"/>
 910 |                         </svg>
 911 |                         <h3>Drag & drop files here</h3>
 912 |                         <p>or <button id="fileSelectBtn" class="link-button">browse to select files</button></p>
 913 |                         <p class="supported-formats">Supported formats: PDF, DOCX, PPTX, TXT, MD, JSON</p>
 914 |                         <input type="file" id="fileInput" multiple accept=".pdf,.docx,.pptx,.txt,.md,.json" style="display: none;">
 915 |                     `;
 916 |                 }
 917 |                 // Re-setup event listeners for the new elements
 918 |                 this.setupDocumentsEventListeners();
 919 |             }
 920 | 
 921 |         } catch (error) {
 922 |             console.error('Upload error:', error);
 923 |             this.showToast('Upload failed: ' + error.message, 'error');
 924 |         } finally {
 925 |             this.setLoading(false);
 926 |         }
 927 |     }
 928 | 
 929 |     /**
 930 |      * Upload single document
 931 |      */
 932 |     async uploadSingleDocument(file, config) {
 933 |         console.log(`Uploading file: ${file.name}, size: ${file.size} bytes`);
 934 |         const formData = new FormData();
 935 |         formData.append('file', file);
 936 |         formData.append('tags', config.tags);
 937 |         formData.append('chunk_size', config.chunk_size.toString());
 938 |         formData.append('chunk_overlap', config.chunk_overlap.toString());
 939 |         formData.append('memory_type', config.memory_type);
 940 | 
 941 |         const response = await fetch(`${this.apiBase}/documents/upload`, {
 942 |             method: 'POST',
 943 |             body: formData
 944 |         });
 945 | 
 946 |         console.log(`Upload response status: ${response.status}`);
 947 | 
 948 |         if (!response.ok) {
 949 |             let errorMessage = `Upload failed with status ${response.status}`;
 950 |             try {
 951 |                 const error = await response.json();
 952 |                 console.error('Upload error details:', error);
 953 |                 errorMessage = error.detail || error.message || errorMessage;
 954 |             } catch (e) {
 955 |                 console.error('Could not parse error response:', e);
 956 |                 try {
 957 |                     const errorText = await response.text();
 958 |                     console.error('Error response text:', errorText);
 959 |                     errorMessage = errorText || errorMessage;
 960 |                 } catch (e2) {
 961 |                     console.error('Could not read error response:', e2);
 962 |                 }
 963 |             }
 964 |             throw new Error(errorMessage);
 965 |         }
 966 | 
 967 |         const result = await response.json();
 968 |         console.log('Upload result:', result);
 969 |         this.showToast(`Upload started for ${file.name}`, 'success');
 970 | 
 971 |         // Monitor progress if we have an upload ID
 972 |         if (result.upload_id) {
 973 |             this.monitorUploadProgress(result.upload_id);
 974 |         }
 975 | 
 976 |         return result;
 977 |     }
 978 | 
 979 |     /**
 980 |      * Upload batch documents
 981 |      */
 982 |     async uploadBatchDocuments(files, config) {
 983 |         const formData = new FormData();
 984 |         files.forEach(file => {
 985 |             formData.append('files', file);
 986 |         });
 987 |         formData.append('tags', config.tags);
 988 |         formData.append('chunk_size', config.chunk_size.toString());
 989 |         formData.append('chunk_overlap', config.chunk_overlap.toString());
 990 |         formData.append('memory_type', config.memory_type);
 991 | 
 992 |         const response = await fetch(`${this.apiBase}/documents/batch-upload`, {
 993 |             method: 'POST',
 994 |             body: formData
 995 |         });
 996 | 
 997 |         if (!response.ok) {
 998 |             const error = await response.json();
 999 |             throw new Error(error.detail || 'Batch upload failed');
1000 |         }
1001 | 
1002 |         const result = await response.json();
1003 |         this.showToast(`Batch upload started for ${files.length} files`, 'success');
1004 | 
1005 |         // Monitor progress if we have an upload ID
1006 |         if (result.upload_id) {
1007 |             this.monitorUploadProgress(result.upload_id);
1008 |         }
1009 | 
1010 |         return result;
1011 |     }
1012 | 
1013 |     /**
1014 |      * Monitor upload progress by polling status endpoint
1015 |      */
1016 |     monitorUploadProgress(uploadId) {
1017 |         const pollStatus = async () => {
1018 |             try {
1019 |                 const statusResponse = await this.apiCall(`/documents/status/${uploadId}`);
1020 |                 this.updateUploadProgress(uploadId, statusResponse);
1021 | 
1022 |                 if (statusResponse.progress >= 100 || statusResponse.status === 'completed' || statusResponse.status === 'failed') {
1023 |                     // Upload completed, refresh history
1024 |                     this.loadUploadHistory();
1025 |                 } else {
1026 |                     // Continue polling
1027 |                     setTimeout(pollStatus, 2000); // Poll every 2 seconds
1028 |                 }
1029 |             } catch (error) {
1030 |                 // If polling fails, try again with longer interval
1031 |                 setTimeout(pollStatus, 5000);
1032 |             }
1033 |         };
1034 | 
1035 |         // Start polling after a short delay
1036 |         setTimeout(pollStatus, 1000);
1037 |     }
1038 | 
1039 |     /**
1040 |      * Update upload progress display
1041 |      */
1042 |     updateUploadProgress(uploadId, statusData) {
1043 |         // Find the upload item in history and update it
1044 |         const historyContainer = document.getElementById('uploadHistory');
1045 |         if (!historyContainer) return;
1046 | 
1047 |         const uploadItems = historyContainer.querySelectorAll('.upload-item');
1048 |         uploadItems.forEach(item => {
1049 |             const filename = item.querySelector('.upload-filename');
1050 |             if (filename && filename.textContent.includes(uploadId)) {
1051 |                 // This is a simplified update - in practice you'd match by upload ID
1052 |                 this.loadUploadHistory(); // For now, just refresh the entire history
1053 |             }
1054 |         });
1055 |     }
1056 | 
1057 |     /**
1058 |      * Check hybrid backend sync status
1059 |      */
1060 |     async checkSyncStatus() {
1061 |         // Skip UI updates during force sync to prevent periodic polling from overwriting the syncing state
1062 |         if (this._isForceSyncing) {
1063 |             return;
1064 |         }
1065 | 
1066 |         try {
1067 |             const syncStatus = await this.apiCall('/sync/status');
1068 | 
1069 |             // Get compact sync control element
1070 |             const syncControl = document.getElementById('syncControl');
1071 |             if (!syncControl) {
1072 |                 console.warn('Sync control element not found');
1073 |                 return;
1074 |             }
1075 | 
1076 |             if (!syncStatus.is_hybrid) {
1077 |                 syncControl.style.display = 'none';
1078 |                 return;
1079 |             }
1080 | 
1081 |             // Show sync control for hybrid mode
1082 |             syncControl.style.display = 'block';
1083 | 
1084 |             // Update sync status UI elements
1085 |             const statusText = document.getElementById('syncStatusText');
1086 |             const syncProgress = document.getElementById('syncProgress');
1087 |             const pauseButton = document.getElementById('pauseSyncButton');
1088 |             const resumeButton = document.getElementById('resumeSyncButton');
1089 |             const syncButton = document.getElementById('forceSyncButton');
1090 | 
1091 |             // Update pause/resume button visibility based on running state
1092 |             const isPaused = syncStatus.is_paused || !syncStatus.is_running;
1093 | 
1094 |             // Attach event listeners if not already attached
1095 |             if (pauseButton && !pauseButton._listenerAttached) {
1096 |                 pauseButton.addEventListener('click', () => {
1097 |                     this.pauseSync();
1098 |                 });
1099 |                 pauseButton._listenerAttached = true;
1100 |             }
1101 |             if (resumeButton && !resumeButton._listenerAttached) {
1102 |                 resumeButton.addEventListener('click', () => {
1103 |                     this.resumeSync();
1104 |                 });
1105 |                 resumeButton._listenerAttached = true;
1106 |             }
1107 |             if (syncButton && !syncButton._listenerAttached) {
1108 |                 syncButton.addEventListener('click', () => {
1109 |                     this.forceSync();
1110 |                 });
1111 |                 syncButton._listenerAttached = true;
1112 |             }
1113 | 
1114 |             if (pauseButton) {
1115 |                 pauseButton.style.display = isPaused ? 'none' : 'flex';
1116 |             }
1117 |             if (resumeButton) {
1118 |                 resumeButton.style.display = isPaused ? 'flex' : 'none';
1119 |             }
1120 | 
1121 |             // Determine status and update UI (dot color is handled by CSS classes)
1122 |             if (isPaused) {
1123 |                 statusText.textContent = 'Paused';
1124 |                 syncProgress.textContent = '';
1125 |                 syncControl.className = 'sync-control-compact paused';
1126 |                 if (syncButton) syncButton.disabled = true;
1127 |             } else if (syncStatus.status === 'syncing') {
1128 |                 statusText.textContent = 'Syncing';
1129 |                 syncProgress.textContent = syncStatus.operations_pending > 0 ? `${syncStatus.operations_pending} pending` : '';
1130 |                 syncControl.className = 'sync-control-compact syncing';
1131 |                 if (syncButton) syncButton.disabled = true;
1132 |             } else if (syncStatus.status === 'pending') {
1133 |                 statusText.textContent = 'Pending';
1134 |                 syncProgress.textContent = `${syncStatus.operations_pending} ops`;
1135 |                 syncControl.className = 'sync-control-compact pending';
1136 |                 if (syncButton) syncButton.disabled = false;
1137 |             } else if (syncStatus.status === 'error') {
1138 |                 statusText.textContent = 'Error';
1139 |                 syncProgress.textContent = `${syncStatus.operations_failed} failed`;
1140 |                 syncControl.className = 'sync-control-compact error';
1141 |                 if (syncButton) syncButton.disabled = false;
1142 |             } else {
1143 |                 // synced status
1144 |                 statusText.textContent = 'Synced';
1145 |                 syncProgress.textContent = '';
1146 |                 syncControl.className = 'sync-control-compact synced';
1147 |                 if (syncButton) syncButton.disabled = false;
1148 |             }
1149 | 
1150 |         } catch (error) {
1151 |             console.error('Error checking sync status:', error);
1152 |             // Hide sync control on error (likely not hybrid mode)
1153 |             const syncControl = document.getElementById('syncControl');
1154 |             if (syncControl) syncControl.style.display = 'none';
1155 |         }
1156 |     }
1157 | 
1158 |     /**
1159 |      * Format time delta in human readable format
1160 |      */
1161 |     formatTimeDelta(seconds) {
1162 |         if (seconds < 60) return `${seconds}s ago`;
1163 |         if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
1164 |         if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
1165 |         return `${Math.floor(seconds / 86400)}d ago`;
1166 |     }
1167 | 
1168 |     /**
1169 |      * Pause background sync
1170 |      */
1171 |     async pauseSync() {
1172 |         try {
1173 |             const result = await this.apiCall('/sync/pause', 'POST');
1174 |             if (result.success) {
1175 |                 this.showToast('Sync paused', 'success');
1176 | 
1177 |                 // Update UI immediately using API response data
1178 |                 const pauseButton = document.getElementById('pauseSyncButton');
1179 |                 const resumeButton = document.getElementById('resumeSyncButton');
1180 |                 const statusText = document.getElementById('syncStatusText');
1181 |                 const syncControl = document.getElementById('syncControl');
1182 | 
1183 |                 if (pauseButton) pauseButton.style.display = 'none';
1184 |                 if (resumeButton) resumeButton.style.display = 'flex';
1185 |                 if (statusText) statusText.textContent = 'Paused';
1186 |                 if (syncControl) syncControl.className = 'sync-control-compact paused';
1187 | 
1188 |                 // Small delay to allow backend state to propagate before checking status
1189 |                 await new Promise(resolve => setTimeout(resolve, 200));
1190 |             } else {
1191 |                 this.showToast('Failed to pause sync: ' + result.message, 'error');
1192 |             }
1193 |             await this.checkSyncStatus();
1194 |         } catch (error) {
1195 |             console.error('Error pausing sync:', error);
1196 |             this.showToast('Failed to pause sync', 'error');
1197 |         }
1198 |     }
1199 | 
1200 |     /**
1201 |      * Resume background sync
1202 |      */
1203 |     async resumeSync() {
1204 |         try {
1205 |             const result = await this.apiCall('/sync/resume', 'POST');
1206 |             if (result.success) {
1207 |                 this.showToast('Sync resumed', 'success');
1208 | 
1209 |                 // Update UI immediately using API response data
1210 |                 const pauseButton = document.getElementById('pauseSyncButton');
1211 |                 const resumeButton = document.getElementById('resumeSyncButton');
1212 |                 const statusText = document.getElementById('syncStatusText');
1213 |                 const syncControl = document.getElementById('syncControl');
1214 | 
1215 |                 if (pauseButton) pauseButton.style.display = 'flex';
1216 |                 if (resumeButton) resumeButton.style.display = 'none';
1217 |                 if (statusText) statusText.textContent = 'Synced';
1218 |                 if (syncControl) syncControl.className = 'sync-control-compact synced';
1219 | 
1220 |                 // Small delay to allow backend state to propagate before checking status
1221 |                 await new Promise(resolve => setTimeout(resolve, 200));
1222 |             } else {
1223 |                 this.showToast('Failed to resume sync: ' + result.message, 'error');
1224 |             }
1225 |             await this.checkSyncStatus();
1226 |         } catch (error) {
1227 |             console.error('Error resuming sync:', error);
1228 |             this.showToast('Failed to resume sync', 'error');
1229 |         }
1230 |     }
1231 | 
1232 |     /**
1233 |      * Check backup status and update Settings modal
1234 |      */
1235 |     async checkBackupStatus() {
1236 |         try {
1237 |             const backupStatus = await this.apiCall('/backup/status');
1238 | 
1239 |             // Update backup elements in Settings modal
1240 |             const lastBackup = document.getElementById('settingsLastBackup');
1241 |             const backupCount = document.getElementById('settingsBackupCount');
1242 |             const nextBackup = document.getElementById('settingsNextBackup');
1243 | 
1244 |             if (!backupStatus.enabled) {
1245 |                 if (lastBackup) lastBackup.textContent = 'Backups disabled';
1246 |                 if (backupCount) backupCount.textContent = '-';
1247 |                 if (nextBackup) nextBackup.textContent = '-';
1248 |                 return;
1249 |             }
1250 | 
1251 |             // Update last backup time
1252 |             if (lastBackup) {
1253 |                 if (backupStatus.time_since_last_seconds) {
1254 |                     lastBackup.textContent = this.formatTimeDelta(Math.floor(backupStatus.time_since_last_seconds)) + ' ago';
1255 |                 } else {
1256 |                     lastBackup.textContent = 'Never';
1257 |                 }
1258 |             }
1259 | 
1260 |             // Update backup count with size
1261 |             if (backupCount) {
1262 |                 const sizeMB = (backupStatus.total_size_bytes / 1024 / 1024).toFixed(1);
1263 |                 backupCount.textContent = `${backupStatus.backup_count} (${sizeMB} MB)`;
1264 |             }
1265 | 
1266 |             // Update next scheduled backup
1267 |             if (nextBackup && backupStatus.next_backup_at) {
1268 |                 const nextDate = new Date(backupStatus.next_backup_at);
1269 |                 nextBackup.textContent = nextDate.toLocaleString();
1270 |             } else if (nextBackup) {
1271 |                 nextBackup.textContent = backupStatus.scheduler_running ? 'Scheduled' : 'Not scheduled';
1272 |             }
1273 | 
1274 |         } catch (error) {
1275 |             console.error('Error checking backup status:', error);
1276 |         }
1277 |     }
1278 | 
1279 |     /**
1280 |      * Create a backup manually
1281 |      */
1282 |     async createBackup() {
1283 |         const backupButton = document.getElementById('backupNowButton');
1284 |         if (backupButton) backupButton.disabled = true;
1285 | 
1286 |         try {
1287 |             this.showToast('Creating backup...', 'info');
1288 |             const result = await this.apiCall('/backup/now', 'POST');
1289 | 
1290 |             if (result.success) {
1291 |                 const sizeMB = (result.size_bytes / 1024 / 1024).toFixed(2);
1292 |                 this.showToast(`Backup created: ${result.filename} (${sizeMB} MB)`, 'success');
1293 |             } else {
1294 |                 this.showToast('Backup failed: ' + result.error, 'error');
1295 |             }
1296 | 
1297 |             await this.checkBackupStatus();
1298 | 
1299 |         } catch (error) {
1300 |             console.error('Error creating backup:', error);
1301 |             this.showToast('Failed to create backup', 'error');
1302 |         } finally {
1303 |             if (backupButton) backupButton.disabled = false;
1304 |         }
1305 |     }
1306 | 
1307 |     /**
1308 |      * Start periodic sync status monitoring
1309 |      */
1310 |     startSyncStatusMonitoring() {
1311 |         // Check sync status every 10 seconds
1312 |         setInterval(() => {
1313 |             this.checkSyncStatus();
1314 |         }, 10000);
1315 |     }
1316 | 
1317 |     /**
1318 |      * Manually force sync to Cloudflare
1319 |      */
1320 |     async forceSync() {
1321 |         const syncButton = document.getElementById('forceSyncButton');
1322 |         const originalText = syncButton.innerHTML;
1323 | 
1324 |         try {
1325 |             // Check if sync was paused before force sync
1326 |             const statusBefore = await this.apiCall('/sync/status');
1327 |             const wasPaused = statusBefore.is_paused;
1328 | 
1329 |             // Set flag to prevent periodic polling from overwriting UI during force sync
1330 |             this._isForceSyncing = true;
1331 | 
1332 |             // Disable button and show loading state (just egg timer, no text - widget shows "Syncing")
1333 |             syncButton.disabled = true;
1334 |             syncButton.innerHTML = '<span class="sync-button-icon">⏳</span>';
1335 | 
1336 |             // IMMEDIATELY update sync control widget to show syncing state
1337 |             const statusText = document.getElementById('syncStatusText');
1338 |             const syncProgress = document.getElementById('syncProgress');
1339 |             const syncControl = document.getElementById('syncControl');
1340 | 
1341 |             if (statusText) statusText.textContent = 'Syncing';
1342 |             if (syncProgress) syncProgress.textContent = statusBefore.operations_pending > 0 ? `${statusBefore.operations_pending} pending` : '';
1343 |             if (syncControl) syncControl.className = 'sync-control-compact syncing';
1344 | 
1345 |             // Show toast when sync starts
1346 |             this.showToast('Starting sync...', 'info');
1347 | 
1348 |             const result = await this.apiCall('/sync/force', 'POST');
1349 | 
1350 |             if (result.success) {
1351 |                 this.showToast(`Synced ${result.operations_synced} operations in ${result.time_taken_seconds}s`, 'success');
1352 | 
1353 |                 // Refresh dashboard data to show newly synced memories
1354 |                 if (this.currentView === 'dashboard') {
1355 |                     await this.loadDashboardData();
1356 |                 }
1357 | 
1358 |                 // If sync was paused before, pause it again after force sync
1359 |                 if (wasPaused) {
1360 |                     await this.apiCall('/sync/pause', 'POST');
1361 |                 }
1362 |             } else {
1363 |                 this.showToast('Sync failed: ' + result.message, 'error');
1364 |             }
1365 | 
1366 |         } catch (error) {
1367 |             console.error('Error forcing sync:', error);
1368 |             this.showToast('Failed to force sync: ' + error.message, 'error');
1369 |         } finally {
1370 |             // Clear flag to allow periodic polling to resume
1371 |             this._isForceSyncing = false;
1372 | 
1373 |             // Re-enable button
1374 |             syncButton.disabled = false;
1375 |             syncButton.innerHTML = originalText;
1376 | 
1377 |             // Refresh sync status immediately
1378 |             await this.checkSyncStatus();
1379 |         }
1380 |     }
1381 | 
1382 |     /**
1383 |      * Render tags cloud from API data
1384 |      */
1385 |     renderTagsCloud() {
1386 |         const container = document.getElementById('tagsCloudContainer');
1387 |         const taggedContainer = document.getElementById('taggedMemoriesContainer');
1388 | 
1389 |         // Hide the tagged memories view initially
1390 |         taggedContainer.style.display = 'none';
1391 | 
1392 |         if (!this.tags || this.tags.length === 0) {
1393 |             container.innerHTML = '<p class="text-neutral-600">No tags found. Start adding tags to your memories to see them here.</p>';
1394 |             return;
1395 |         }
1396 | 
1397 |         // Render tag bubbles (tags are already sorted by count from backend)
1398 |         container.innerHTML = this.tags.map(tagData => `
1399 |             <button class="tag-bubble" data-tag="${this.escapeHtml(tagData.tag)}">
1400 |                 ${this.escapeHtml(tagData.tag)}
1401 |                 <span class="count">${tagData.count}</span>
1402 |             </button>
1403 |         `).join('');
1404 |     }
1405 | 
1406 |     /**
1407 |      * Filter memories by selected tag
1408 |      */
1409 |     async filterByTag(tag) {
1410 |         const taggedContainer = document.getElementById('taggedMemoriesContainer');
1411 |         const tagNameSpan = document.getElementById('selectedTagName');
1412 |         const memoriesList = document.getElementById('taggedMemoriesList');
1413 | 
1414 |         try {
1415 |             // Fetch memories for this specific tag
1416 |             const memoriesResponse = await this.apiCall(`/memories?tag=${encodeURIComponent(tag)}&limit=100`);
1417 |             const filteredMemories = memoriesResponse.memories || [];
1418 | 
1419 |             // Show the tagged memories section
1420 |             tagNameSpan.textContent = tag;
1421 |             taggedContainer.style.display = 'block';
1422 | 
1423 |             // Smooth scroll to results section for better UX
1424 |             taggedContainer.scrollIntoView({
1425 |                 behavior: 'smooth',
1426 |                 block: 'start'
1427 |             });
1428 | 
1429 |             // Render filtered memories
1430 |             this.renderMemoriesInContainer(filteredMemories, memoriesList);
1431 | 
1432 |             // Add event listener for clear filter button
1433 |             const clearBtn = document.getElementById('clearTagFilter');
1434 |             clearBtn.onclick = () => this.clearTagFilter();
1435 |         } catch (error) {
1436 |             console.error('Error filtering by tag:', error);
1437 |             this.showToast('Failed to load memories for tag', 'error');
1438 |         }
1439 |     }
1440 | 
1441 |     /**
1442 |      * Clear tag filter and show all tags
1443 |      */
1444 |     clearTagFilter() {
1445 |         const taggedContainer = document.getElementById('taggedMemoriesContainer');
1446 |         taggedContainer.style.display = 'none';
1447 |     }
1448 | 
1449 |     /**
1450 |      * Escape HTML to prevent XSS
1451 |      */
1452 |     escapeHtml(text) {
1453 |         const div = document.createElement('div');
1454 |         div.textContent = text;
1455 |         return div.innerHTML;
1456 |     }
1457 | 
1458 |     /**
1459 |      * Toggle chunking help section visibility
1460 |      */
1461 |     toggleChunkingHelp() {
1462 |         const helpSection = document.getElementById('chunkingHelpSection');
1463 |         if (helpSection) {
1464 |             if (helpSection.style.display === 'none') {
1465 |                 helpSection.style.display = 'block';
1466 |             } else {
1467 |                 helpSection.style.display = 'none';
1468 |             }
1469 |         }
1470 |     }
1471 | 
1472 |     /**
1473 |      * Hide chunking help section
1474 |      */
1475 |     hideChunkingHelp() {
1476 |         const helpSection = document.getElementById('chunkingHelpSection');
1477 |         if (helpSection) {
1478 |             helpSection.style.display = 'none';
1479 |         }
1480 |     }
1481 | 
1482 |     /**
1483 |      * Toggle overlap help section visibility
1484 |      */
1485 |     toggleOverlapHelp() {
1486 |         const helpSection = document.getElementById('overlapHelpSection');
1487 |         if (helpSection) {
1488 |             if (helpSection.style.display === 'none') {
1489 |                 helpSection.style.display = 'block';
1490 |             } else {
1491 |                 helpSection.style.display = 'none';
1492 |             }
1493 |         }
1494 |     }
1495 | 
1496 |     /**
1497 |      * Hide overlap help section
1498 |      */
1499 |     hideOverlapHelp() {
1500 |         const helpSection = document.getElementById('overlapHelpSection');
1501 |         if (helpSection) {
1502 |             helpSection.style.display = 'none';
1503 |         }
1504 |     }
1505 | 
1506 |     /**
1507 |      * Toggle processing mode help section visibility
1508 |      */
1509 |     toggleProcessingModeHelp() {
1510 |         const helpSection = document.getElementById('processingModeHelpSection');
1511 |         if (helpSection) {
1512 |             helpSection.style.display = helpSection.style.display === 'block' ? 'none' : 'block';
1513 |         }
1514 |     }
1515 | 
1516 |     /**
1517 |      * Hide processing mode help section
1518 |      */
1519 |     hideProcessingModeHelp() {
1520 |         const helpSection = document.getElementById('processingModeHelpSection');
1521 |         if (helpSection) {
1522 |             helpSection.style.display = 'none';
1523 |         }
1524 |     }
1525 | 
1526 |     /**
1527 |      * Render memories in a specific container
1528 |      */
1529 |     renderMemoriesInContainer(memories, container) {
1530 |         if (!memories || memories.length === 0) {
1531 |             container.innerHTML = '<p class="empty-state">No memories found with this tag.</p>';
1532 |             return;
1533 |         }
1534 | 
1535 |         container.innerHTML = memories.map(memory => this.renderMemoryCard(memory)).join('');
1536 | 
1537 |         // Add click handlers
1538 |         container.querySelectorAll('.memory-card').forEach((card, index) => {
1539 |             card.addEventListener('click', () => this.handleMemoryClick(memories[index]));
1540 |         });
1541 |     }
1542 | 
1543 |     /**
1544 |      * Handle navigation between views
1545 |      */
1546 |     handleNavigation(e) {
1547 |         const viewName = e.currentTarget.dataset.view;
1548 |         this.switchView(viewName);
1549 |     }
1550 | 
1551 |     /**
1552 |      * Switch between different views
1553 |      */
1554 |     switchView(viewName) {
1555 |         // Update navigation active state (if navigation exists)
1556 |         document.querySelectorAll('.nav-item').forEach(item => {
1557 |             item.classList.remove('active');
1558 |         });
1559 |         const navItem = document.querySelector(`[data-view="${viewName}"]`);
1560 |         if (navItem) {
1561 |             navItem.classList.add('active');
1562 |         }
1563 | 
1564 |         // Hide all views (if view containers exist)
1565 |         document.querySelectorAll('.view-container').forEach(view => {
1566 |             view.classList.remove('active');
1567 |         });
1568 | 
1569 |         // Show target view (if it exists)
1570 |         const targetView = document.getElementById(`${viewName}View`);
1571 |         if (targetView) {
1572 |             targetView.classList.add('active');
1573 |             this.currentView = viewName;
1574 | 
1575 |             // Load view-specific data
1576 |             this.loadViewData(viewName);
1577 |         }
1578 |     }
1579 | 
1580 |     /**
1581 |      * Load data specific to the current view
1582 |      */
1583 |     async loadViewData(viewName) {
1584 |         switch (viewName) {
1585 |             case 'search':
1586 |                 // Initialize search view with recent search or empty state
1587 |                 break;
1588 |             case 'browse':
1589 |                 await this.loadBrowseData();
1590 |                 break;
1591 |             case 'documents':
1592 |                 await this.loadDocumentsData();
1593 |                 break;
1594 |             case 'manage':
1595 |                 await this.loadManageData();
1596 |                 break;
1597 |             case 'analytics':
1598 |                 await this.loadAnalyticsData();
1599 |                 break;
1600 |             case 'apiDocs':
1601 |                 // API docs view - static content, no additional loading needed
1602 |                 break;
1603 |             default:
1604 |                 // Dashboard view is loaded in loadDashboardData
1605 |                 break;
1606 |         }
1607 |     }
1608 | 
1609 |     /**
1610 |      * Handle quick search input
1611 |      */
1612 |     async handleQuickSearch(e) {
1613 |         const query = e.target.value.trim();
1614 |         if (query.length >= 2) {
1615 |             try {
1616 |                 const results = await this.searchMemories(query);
1617 |                 // Could show dropdown suggestions here
1618 |             } catch (error) {
1619 |                 console.error('Quick search error:', error);
1620 |             }
1621 |         }
1622 |     }
1623 | 
1624 |     /**
1625 |      * Handle full search
1626 |      */
1627 |     async handleSearch(query) {
1628 |         if (!query.trim()) return;
1629 | 
1630 |         this.switchView('search');
1631 |         this.setLoading(true);
1632 | 
1633 |         try {
1634 |             const results = await this.searchMemories(query);
1635 |             this.searchResults = results;
1636 |             this.renderSearchResults(results);
1637 |             this.updateResultsCount(results.length);
1638 |         } catch (error) {
1639 |             console.error('Search error:', error);
1640 |             this.showToast('Search failed', 'error');
1641 |         } finally {
1642 |             this.setLoading(false);
1643 |         }
1644 |     }
1645 | 
1646 |     /**
1647 |      * Search memories using the API
1648 |      */
1649 |     async searchMemories(query, filters = {}) {
1650 |         // Detect tag search patterns: #tag, tag:value, or "tag:value"
1651 |         const tagPattern = /^(#|tag:)(.+)$/i;
1652 |         const tagMatch = query.match(tagPattern);
1653 | 
1654 |         if (tagMatch) {
1655 |             // Use tag search endpoint
1656 |             const tagValue = tagMatch[2].trim();
1657 |             const payload = {
1658 |                 tags: [tagValue],
1659 |                 match_all: false // ANY match by default
1660 |             };
1661 | 
1662 |             const response = await this.apiCall('/search/by-tag', 'POST', payload);
1663 |             return response.results || [];
1664 |         } else {
1665 |             // Use semantic search endpoint
1666 |             const payload = {
1667 |                 query: query,
1668 |                 n_results: filters.limit || 20,
1669 |                 ...filters
1670 |             };
1671 | 
1672 |             // Only add similarity_threshold if explicitly set in filters
1673 |             if (filters.threshold !== undefined) {
1674 |                 payload.similarity_threshold = filters.threshold;
1675 |             }
1676 | 
1677 |             const response = await this.apiCall('/search', 'POST', payload);
1678 |             return response.results || [];
1679 |         }
1680 |     }
1681 | 
1682 |     /**
1683 |      * Handle filter changes in search view
1684 |      */
1685 |     async handleFilterChange() {
1686 |         const tagFilter = document.getElementById('tagFilter')?.value;
1687 |         const dateFilter = document.getElementById('dateFilter')?.value;
1688 |         const typeFilter = document.getElementById('typeFilter')?.value;
1689 |         const query = document.getElementById('quickSearch')?.value?.trim() || '';
1690 | 
1691 |         // Add loading state
1692 |         const applyBtn = document.getElementById('applyFiltersBtn');
1693 |         if (applyBtn) {
1694 |             applyBtn.classList.add('loading');
1695 |             applyBtn.disabled = true;
1696 |         }
1697 | 
1698 |         try {
1699 |             let results = [];
1700 | 
1701 |             // Priority 1: If we have a semantic query, start with semantic search
1702 |             if (query) {
1703 |                 const filters = {};
1704 |                 if (typeFilter) filters.type = typeFilter;
1705 |                 results = await this.searchMemories(query, filters);
1706 | 
1707 |                 // Apply tag filtering to semantic search results if tags are specified
1708 |                 if (tagFilter && tagFilter.trim()) {
1709 |                     const tags = tagFilter.split(',').map(t => t.trim()).filter(t => t);
1710 |                     if (tags.length > 0) {
1711 |                         results = results.filter(result => {
1712 |                             const memoryTags = result.memory.tags || [];
1713 |                             // Check if any of the specified tags match memory tags (case-insensitive)
1714 |                             return tags.some(filterTag =>
1715 |                                 memoryTags.some(memoryTag =>
1716 |                                     memoryTag.toLowerCase().includes(filterTag.toLowerCase())
1717 |                                 )
1718 |                             );
1719 |                         });
1720 |                     }
1721 |                 }
1722 |             }
1723 |             // Priority 2: Tag-only search (when no semantic query)
1724 |             else if (tagFilter && tagFilter.trim()) {
1725 |                 const tags = tagFilter.split(',').map(t => t.trim()).filter(t => t);
1726 | 
1727 |                 if (tags.length > 0) {
1728 |                     const payload = {
1729 |                         tags: tags,
1730 |                         match_all: false // ANY match by default
1731 |                     };
1732 | 
1733 |                     const response = await this.apiCall('/search/by-tag', 'POST', payload);
1734 |                     results = response.results || [];
1735 | 
1736 |                     // Apply type filter if present
1737 |                     if (typeFilter && typeFilter.trim()) {
1738 |                         results = results.filter(result => {
1739 |                             const memoryType = result.memory.memory_type || 'note';
1740 |                             return memoryType === typeFilter;
1741 |                         });
1742 |                     }
1743 |                 }
1744 |             }
1745 |             // Priority 3: Date-based search
1746 |             else if (dateFilter && dateFilter.trim()) {
1747 |                 const payload = {
1748 |                     query: dateFilter,
1749 |                     n_results: 100
1750 |                 };
1751 |                 const response = await this.apiCall('/search/by-time', 'POST', payload);
1752 |                 results = response.results || [];
1753 | 
1754 |                 // Apply type filter if present
1755 |                 if (typeFilter && typeFilter.trim()) {
1756 |                     results = results.filter(result => {
1757 |                         const memoryType = result.memory.memory_type || 'note';
1758 |                         return memoryType === typeFilter;
1759 |                     });
1760 |                 }
1761 |             }
1762 |             // Priority 4: Type-only filter
1763 |             else if (typeFilter && typeFilter.trim()) {
1764 |                 const allMemoriesResponse = await this.apiCall('/memories?page=1&page_size=1000');
1765 |                 if (allMemoriesResponse.memories) {
1766 |                     results = allMemoriesResponse.memories
1767 |                         .filter(memory => (memory.memory_type || 'note') === typeFilter)
1768 |                         .map(memory => ({ memory, similarity: 1.0 }));
1769 |                 }
1770 |             } else {
1771 |                 // No filters, clear results
1772 |                 results = [];
1773 |             }
1774 | 
1775 |             this.searchResults = results;
1776 |             this.renderSearchResults(results);
1777 |             this.updateResultsCount(results.length);
1778 |             this.updateActiveFilters();
1779 | 
1780 |         } catch (error) {
1781 |             console.error('Filter search error:', error);
1782 |             this.showToast('Filter search failed', 'error');
1783 |         } finally {
1784 |             // Remove loading state
1785 |             const applyBtn = document.getElementById('applyFiltersBtn');
1786 |             if (applyBtn) {
1787 |                 applyBtn.classList.remove('loading');
1788 |                 applyBtn.disabled = false;
1789 |             }
1790 |         }
1791 |     }
1792 | 
1793 |     /**
1794 |      * Handle view mode changes (grid/list)
1795 |      */
1796 |     handleViewModeChange(mode) {
1797 |         document.querySelectorAll('.view-btn').forEach(btn => {
1798 |             btn.classList.remove('active');
1799 |         });
1800 |         document.querySelector(`[data-view="${mode}"]`).classList.add('active');
1801 | 
1802 |         const resultsContainer = document.getElementById('searchResultsList');
1803 |         resultsContainer.className = mode === 'grid' ? 'memory-grid' : 'memory-list';
1804 |     }
1805 | 
1806 |     /**
1807 |      * Handle quick actions
1808 |      */
1809 |     handleQuickAction(action) {
1810 |         switch (action) {
1811 |             case 'quick-search':
1812 |                 this.switchView('search');
1813 |                 const searchInput = document.getElementById('quickSearch');
1814 |                 if (searchInput) {
1815 |                     searchInput.focus();
1816 |                 }
1817 |                 break;
1818 |             case 'add-memory':
1819 |                 this.handleAddMemory();
1820 |                 break;
1821 |             case 'browse-tags':
1822 |                 this.switchView('browse');
1823 |                 break;
1824 |             case 'export-data':
1825 |                 this.handleExportData();
1826 |                 break;
1827 |         }
1828 |     }
1829 | 
1830 |     /**
1831 |      * Handle add memory action
1832 |      */
1833 |     handleAddMemory() {
1834 |         const modal = document.getElementById('addMemoryModal');
1835 | 
1836 |         // Reset modal for adding new memory
1837 |         this.resetAddMemoryModal();
1838 | 
1839 |         this.openModal(modal);
1840 |         document.getElementById('memoryContent').focus();
1841 |     }
1842 | 
1843 |     /**
1844 |      * Reset add memory modal to default state
1845 |      */
1846 |     resetAddMemoryModal() {
1847 |         const modal = document.getElementById('addMemoryModal');
1848 |         const title = modal.querySelector('.modal-header h3');
1849 |         const saveBtn = document.getElementById('saveMemoryBtn');
1850 | 
1851 |         // Reset modal title and button text
1852 |         title.textContent = 'Add New Memory';
1853 |         saveBtn.textContent = 'Save Memory';
1854 | 
1855 |         // Clear form
1856 |         document.getElementById('addMemoryForm').reset();
1857 | 
1858 |         // Clear editing state
1859 |         this.editingMemory = null;
1860 |     }
1861 | 
1862 |     /**
1863 |      * Handle save memory
1864 |      */
1865 |     async handleSaveMemory() {
1866 |         const content = document.getElementById('memoryContent').value.trim();
1867 |         const tags = document.getElementById('memoryTags').value.trim();
1868 |         const type = document.getElementById('memoryType').value;
1869 | 
1870 |         if (!content) {
1871 |             this.showToast('Please enter memory content', 'warning');
1872 |             return;
1873 |         }
1874 | 
1875 |         const payload = {
1876 |             content: content,
1877 |             tags: tags ? tags.split(',').map(t => t.trim()) : [],
1878 |             memory_type: type,
1879 |             metadata: {
1880 |                 created_via: 'dashboard',
1881 |                 user_agent: navigator.userAgent,
1882 |                 updated_via: this.editingMemory ? 'dashboard_edit' : 'dashboard_create'
1883 |             }
1884 |         };
1885 | 
1886 | 
1887 |         try {
1888 |             let response;
1889 |             let successMessage;
1890 | 
1891 |             if (this.editingMemory) {
1892 |                 // Smart update: check if only metadata changed vs content changes
1893 |                 const originalContentHash = this.editingMemory.content_hash;
1894 |                 const contentChanged = this.editingMemory.content !== payload.content;
1895 | 
1896 | 
1897 |                 if (!contentChanged) {
1898 |                     // Only metadata (tags, type, metadata) changed - use PUT endpoint
1899 |                     const updatePayload = {
1900 |                         tags: payload.tags,
1901 |                         memory_type: payload.memory_type,
1902 |                         metadata: payload.metadata
1903 |                     };
1904 | 
1905 |                     response = await this.apiCall(`/memories/${originalContentHash}`, 'PUT', updatePayload);
1906 |                     successMessage = 'Memory updated successfully';
1907 |                 } else {
1908 |                     // Content changed - use create-delete approach (but with proper error handling)
1909 | 
1910 |                     try {
1911 |                         // Step 1: Create updated memory first
1912 |                         response = await this.apiCall('/memories', 'POST', payload);
1913 | 
1914 |                         // CRITICAL: Only proceed with deletion if creation actually succeeded
1915 |                         if (response.success) {
1916 |                             successMessage = 'Memory updated successfully';
1917 | 
1918 |                             try {
1919 |                                 // Step 2: Delete original memory (only after successful creation)
1920 |                                 const deleteResponse = await this.apiCall(`/memories/${originalContentHash}`, 'DELETE');
1921 |                             } catch (deleteError) {
1922 |                                 console.error('Failed to delete original memory after creating new version:', deleteError);
1923 |                                 this.showToast('Memory updated, but original version still exists. You may need to manually delete the duplicate.', 'warning');
1924 |                             }
1925 |                         } else {
1926 |                             // Creation failed - do NOT delete original memory
1927 |                             console.error('Creation failed:', response.message);
1928 |                             throw new Error(`Failed to create updated memory: ${response.message}`);
1929 |                         }
1930 |                     } catch (createError) {
1931 |                         // CREATE failed - original memory intact, no cleanup needed
1932 |                         console.error('Failed to create updated memory:', createError);
1933 |                         throw new Error(`Failed to update memory: ${createError.message}`);
1934 |                     }
1935 |                 }
1936 |             } else {
1937 |                 // Create new memory
1938 |                 response = await this.apiCall('/memories', 'POST', payload);
1939 |                 successMessage = 'Memory saved successfully';
1940 |             }
1941 | 
1942 |             this.closeModal(document.getElementById('addMemoryModal'));
1943 |             this.showToast(successMessage, 'success');
1944 | 
1945 |             // Reset editing state
1946 |             this.editingMemory = null;
1947 |             this.resetAddMemoryModal();
1948 | 
1949 |             // Refresh current view if needed
1950 |             if (this.currentView === 'dashboard') {
1951 |                 this.loadDashboardData();
1952 |             } else if (this.currentView === 'search') {
1953 |                 // Refresh search results
1954 |                 const query = document.getElementById('searchInput').value.trim();
1955 |                 if (query) {
1956 |                     this.handleSearch(query);
1957 |                 }
1958 |             } else if (this.currentView === 'browse') {
1959 |                 // Refresh browse view (tags cloud)
1960 |                 this.loadBrowseData();
1961 |             }
1962 |         } catch (error) {
1963 |             console.error('Error saving memory:', error);
1964 |             this.showToast(error.message || 'Failed to save memory', 'error');
1965 |         }
1966 |     }
1967 | 
1968 |     /**
1969 |      * Handle memory click to show details
1970 |      */
1971 |     handleMemoryClick(memory) {
1972 |         this.showMemoryDetails(memory);
1973 |     }
1974 | 
1975 |     /**
1976 |      * Show memory details in modal
1977 |      */
1978 |     showMemoryDetails(memory) {
1979 |         const modal = document.getElementById('memoryModal');
1980 |         const title = document.getElementById('modalTitle');
1981 |         const content = document.getElementById('modalContent');
1982 | 
1983 |         title.textContent = 'Memory Details';
1984 |         content.innerHTML = this.renderMemoryDetails(memory);
1985 | 
1986 |         // Set up action buttons
1987 |         document.getElementById('editMemoryBtn').onclick = () => this.editMemory(memory);
1988 |         document.getElementById('deleteMemoryBtn').onclick = () => this.deleteMemory(memory);
1989 |         document.getElementById('shareMemoryBtn').onclick = () => this.shareMemory(memory);
1990 | 
1991 |         this.openModal(modal);
1992 |     }
1993 | 
1994 |     /**
1995 |      * Render memory details HTML
1996 |      */
1997 |     renderMemoryDetails(memory) {
1998 |         const createdDate = new Date(memory.created_at * 1000).toLocaleString();
1999 |         const updatedDate = memory.updated_at ? new Date(memory.updated_at * 1000).toLocaleString() : null;
2000 | 
2001 |         return `
2002 |             <div class="memory-detail">
2003 |                 <div class="memory-meta">
2004 |                     <p><strong>Created:</strong> ${createdDate}</p>
2005 |                     ${updatedDate ? `<p><strong>Updated:</strong> ${updatedDate}</p>` : ''}
2006 |                     <p><strong>Type:</strong> ${memory.memory_type || 'note'}</p>
2007 |                     <p><strong>ID:</strong> ${memory.content_hash}</p>
2008 |                 </div>
2009 | 
2010 |                 <div class="memory-content">
2011 |                     <h4>Content</h4>
2012 |                     <div class="content-text">${this.escapeHtml(memory.content)}</div>
2013 |                 </div>
2014 | 
2015 |                 ${memory.tags && memory.tags.length > 0 ? `
2016 |                     <div class="memory-tags-section">
2017 |                         <h4>Tags</h4>
2018 |                         <div class="memory-tags">
2019 |                             ${memory.tags.map(tag => `<span class="tag">${this.escapeHtml(tag)}</span>`).join('')}
2020 |                         </div>
2021 |                     </div>
2022 |                 ` : ''}
2023 | 
2024 |                 ${memory.metadata ? `
2025 |                     <div class="memory-metadata">
2026 |                         <h4 class="metadata-toggle" onclick="this.parentElement.classList.toggle('expanded')" style="cursor: pointer; user-select: none;">
2027 |                             <span class="toggle-icon">▶</span> Metadata
2028 |                         </h4>
2029 |                         <div class="metadata-content">
2030 |                             ${this.renderMetadata(memory.metadata)}
2031 |                         </div>
2032 |                     </div>
2033 |                 ` : ''}
2034 |             </div>
2035 |         `;
2036 |     }
2037 | 
2038 |     /**
2039 |      * Render metadata in a prettier format
2040 |      */
2041 |     renderMetadata(metadata) {
2042 |         if (!metadata || typeof metadata !== 'object') {
2043 |             return '<p class="metadata-empty">No metadata available</p>';
2044 |         }
2045 | 
2046 |         let html = '<div class="metadata-items">';
2047 | 
2048 |         for (const [key, value] of Object.entries(metadata)) {
2049 |             let displayValue;
2050 | 
2051 |             if (typeof value === 'string') {
2052 |                 displayValue = `<span class="metadata-string">"${this.escapeHtml(value)}"</span>`;
2053 |             } else if (typeof value === 'number') {
2054 |                 displayValue = `<span class="metadata-number">${value}</span>`;
2055 |             } else if (typeof value === 'boolean') {
2056 |                 displayValue = `<span class="metadata-boolean">${value}</span>`;
2057 |             } else if (Array.isArray(value)) {
2058 |                 displayValue = `<span class="metadata-array">[${value.map(v =>
2059 |                     typeof v === 'string' ? `"${this.escapeHtml(v)}"` : v
2060 |                 ).join(', ')}]</span>`;
2061 |             } else {
2062 |                 displayValue = `<span class="metadata-object">${JSON.stringify(value)}</span>`;
2063 |             }
2064 | 
2065 |             html += `
2066 |                 <div class="metadata-item">
2067 |                     <span class="metadata-key">${this.escapeHtml(key)}:</span>
2068 |                     <span class="metadata-value">${displayValue}</span>
2069 |                 </div>
2070 |             `;
2071 |         }
2072 | 
2073 |         html += '</div>';
2074 |         return html;
2075 |     }
2076 | 
2077 |     /**
2078 |      * Delete memory
2079 |      */
2080 |     async deleteMemory(memory) {
2081 |         if (!confirm('Are you sure you want to delete this memory? This action cannot be undone.')) {
2082 |             return;
2083 |         }
2084 | 
2085 |         try {
2086 |             await this.apiCall(`/memories/${memory.content_hash}`, 'DELETE');
2087 |             this.closeModal(document.getElementById('memoryModal'));
2088 |             this.showToast('Memory deleted successfully', 'success');
2089 | 
2090 |             // Refresh current view
2091 |             if (this.currentView === 'dashboard') {
2092 |                 this.loadDashboardData();
2093 |             } else if (this.currentView === 'search') {
2094 |                 this.searchResults = this.searchResults.filter(m => m.memory.content_hash !== memory.content_hash);
2095 |                 this.renderSearchResults(this.searchResults);
2096 |             } else if (this.currentView === 'browse') {
2097 |                 // Refresh browse view (tags cloud)
2098 |                 this.loadBrowseData();
2099 |             }
2100 |         } catch (error) {
2101 |             console.error('Error deleting memory:', error);
2102 |             this.showToast('Failed to delete memory', 'error');
2103 |         }
2104 |     }
2105 | 
2106 |     /**
2107 |      * Edit memory
2108 |      */
2109 |     editMemory(memory) {
2110 |         // Close the memory details modal first
2111 |         this.closeModal(document.getElementById('memoryModal'));
2112 | 
2113 |         // Open the add memory modal with pre-filled data
2114 |         const modal = document.getElementById('addMemoryModal');
2115 |         const title = modal.querySelector('.modal-header h3');
2116 |         const saveBtn = document.getElementById('saveMemoryBtn');
2117 | 
2118 |         // Update modal for editing
2119 |         title.textContent = 'Edit Memory';
2120 |         saveBtn.textContent = 'Update Memory';
2121 | 
2122 |         // Pre-fill the form with existing data
2123 |         document.getElementById('memoryContent').value = memory.content || '';
2124 | 
2125 |         // Handle tags - ensure they're displayed correctly
2126 |         const tagsValue = memory.tags && Array.isArray(memory.tags) ? memory.tags.join(', ') : '';
2127 |         document.getElementById('memoryTags').value = tagsValue;
2128 | 
2129 |         document.getElementById('memoryType').value = memory.memory_type || 'note';
2130 | 
2131 | 
2132 |         // Store the memory being edited
2133 |         this.editingMemory = memory;
2134 | 
2135 |         this.openModal(modal);
2136 | 
2137 |         // Use setTimeout to ensure modal is fully rendered before setting values
2138 |         setTimeout(() => {
2139 |             document.getElementById('memoryContent').focus();
2140 |         }, 100);
2141 |     }
2142 | 
2143 |     /**
2144 |      * Share memory
2145 |      */
2146 |     shareMemory(memory) {
2147 |         // Create shareable data
2148 |         const shareData = {
2149 |             content: memory.content,
2150 |             tags: memory.tags || [],
2151 |             type: memory.memory_type || 'note',
2152 |             created: new Date(memory.created_at * 1000).toISOString(),
2153 |             id: memory.content_hash
2154 |         };
2155 | 
2156 |         // Try to use Web Share API if available
2157 |         if (navigator.share) {
2158 |             navigator.share({
2159 |                 title: 'Memory from MCP Memory Service',
2160 |                 text: memory.content,
2161 |                 url: window.location.href
2162 |             }).catch(err => {
2163 |                 // Share API failed, fall back to clipboard
2164 |                 this.fallbackShare(shareData);
2165 |             });
2166 |         } else {
2167 |             this.fallbackShare(shareData);
2168 |         }
2169 |     }
2170 | 
2171 |     /**
2172 |      * Fallback share method (copy to clipboard)
2173 |      */
2174 |     fallbackShare(shareData) {
2175 |         const shareText = `Memory Content:\n${shareData.content}\n\nTags: ${shareData.tags.join(', ')}\nType: ${shareData.type}\nCreated: ${shareData.created}`;
2176 | 
2177 |         navigator.clipboard.writeText(shareText).then(() => {
2178 |             this.showToast('Memory copied to clipboard', 'success');
2179 |         }).catch(err => {
2180 |             console.error('Could not copy text: ', err);
2181 |             this.showToast('Failed to copy to clipboard', 'error');
2182 |         });
2183 |     }
2184 | 
2185 |     /**
2186 |      * Handle data export
2187 |      */
2188 |     async handleExportData() {
2189 |         try {
2190 |             this.showToast('Preparing export...', 'info');
2191 | 
2192 |             // Fetch all memories using pagination
2193 |             const allMemories = [];
2194 |             const pageSize = 100; // Reasonable batch size
2195 |             let page = 1;
2196 |             let hasMore = true;
2197 |             let totalMemories = 0;
2198 | 
2199 |             while (hasMore) {
2200 |                 const response = await this.apiCall(`/memories?page=${page}&page_size=${pageSize}`);
2201 | 
2202 |                 if (page === 1) {
2203 |                     totalMemories = response.total;
2204 |                 }
2205 | 
2206 |                 if (response.memories && response.memories.length > 0) {
2207 |                     allMemories.push(...response.memories);
2208 |                     hasMore = response.has_more;
2209 |                     page++;
2210 | 
2211 |                     // Update progress
2212 |                     this.showToast(`Fetching memories... (${allMemories.length}/${totalMemories})`, 'info');
2213 |                 } else {
2214 |                     hasMore = false;
2215 |                 }
2216 |             }
2217 | 
2218 |             const data = {
2219 |                 export_date: new Date().toISOString(),
2220 |                 total_memories: totalMemories,
2221 |                 exported_memories: allMemories.length,
2222 |                 memories: allMemories
2223 |             };
2224 | 
2225 |             const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
2226 |             const url = URL.createObjectURL(blob);
2227 |             const a = document.createElement('a');
2228 |             a.href = url;
2229 |             a.download = `mcp-memories-export-${new Date().toISOString().split('T')[0]}.json`;
2230 |             document.body.appendChild(a);
2231 |             a.click();
2232 |             document.body.removeChild(a);
2233 |             URL.revokeObjectURL(url);
2234 | 
2235 |             this.showToast(`Successfully exported ${allMemories.length} memories`, 'success');
2236 |         } catch (error) {
2237 |             console.error('Export error:', error);
2238 |             this.showToast('Failed to export data', 'error');
2239 |         }
2240 |     }
2241 | 
2242 |     /**
2243 |     * Render recent memories
2244 |     */
2245 |     renderRecentMemories(memories) {
2246 |     const container = document.getElementById('recentMemoriesList');
2247 | 
2248 |     if (!container) {
2249 |     console.error('recentMemoriesList container not found');
2250 |     return;
2251 |     }
2252 | 
2253 |     if (!memories || memories.length === 0) {
2254 |     container.innerHTML = '<p class="empty-state">No memories found. <a href="#" onclick="app.handleAddMemory()">Add your first memory</a></p>';
2255 |     return;
2256 |     }
2257 | 
2258 |     // Group document chunks by upload_id
2259 |         const groupedMemories = this.groupMemoriesByUpload(memories);
2260 | 
2261 |     container.innerHTML = groupedMemories.map(group => {
2262 |     if (group.type === 'document') {
2263 |             return this.renderDocumentGroup(group);
2264 |             } else {
2265 |                 return this.renderMemoryCard(group.memory);
2266 |             }
2267 |         }).join('');
2268 | 
2269 |         // Add click handlers for individual memories
2270 |         container.querySelectorAll('.memory-card').forEach((card, index) => {
2271 |             const group = groupedMemories[index];
2272 |             if (group.type === 'single') {
2273 |                 card.addEventListener('click', () => this.handleMemoryClick(group.memory));
2274 |             }
2275 |         });
2276 | 
2277 |         // Add click handlers for document groups
2278 |         container.querySelectorAll('.document-group').forEach((groupEl, index) => {
2279 |             const group = groupedMemories.filter(g => g.type === 'document')[index];
2280 |             if (group) {
2281 |                 groupEl.addEventListener('click', (e) => {
2282 |                     // Don't trigger if clicking on action buttons
2283 |                     if (e.target.closest('.document-actions')) return;
2284 |                     this.showDocumentChunks(group);
2285 |                 });
2286 |             }
2287 |         });
2288 | 
2289 |         // Add click handlers for document action buttons
2290 |         container.querySelectorAll('.document-group').forEach((groupEl, index) => {
2291 |             const group = groupedMemories.filter(g => g.type === 'document')[index];
2292 |             if (group) {
2293 |                 // View chunks button
2294 |                 const viewBtn = groupEl.querySelector('.btn-view-chunks');
2295 |                 if (viewBtn) {
2296 |                     viewBtn.addEventListener('click', (e) => {
2297 |                         e.stopPropagation();
2298 |                         this.showDocumentChunks(group);
2299 |                     });
2300 |                 }
2301 | 
2302 |                 // Remove button
2303 |                 const removeBtn = groupEl.querySelector('.btn-remove');
2304 |                 if (removeBtn) {
2305 |                     removeBtn.addEventListener('click', async (e) => {
2306 |                         e.stopPropagation();
2307 |                         await this.removeDocument(group.upload_id, group.source_file);
2308 |                         // removeDocument() already handles view refresh
2309 |                     });
2310 |                 }
2311 |             }
2312 |         });
2313 |     }
2314 | 
2315 |     /**
2316 |      * Group memories by upload_id for document chunks
2317 |      */
2318 |     groupMemoriesByUpload(memories) {
2319 |         const groups = [];
2320 |         const documentGroups = new Map();
2321 |         const processedHashes = new Set();
2322 | 
2323 |         for (const memory of memories) {
2324 |             // Check if this is a document chunk
2325 |             const isDocumentChunk = memory.metadata && memory.metadata.upload_id;
2326 | 
2327 |             if (isDocumentChunk && !processedHashes.has(memory.content_hash)) {
2328 |                 const uploadId = memory.metadata.upload_id;
2329 |                 const sourceFile = memory.metadata.source_file || 'Unknown file';
2330 | 
2331 |                 if (!documentGroups.has(uploadId)) {
2332 |                     documentGroups.set(uploadId, {
2333 |                         upload_id: uploadId,
2334 |                         source_file: sourceFile,
2335 |                         memories: [],
2336 |                         created_at: memory.created_at,
2337 |                         tags: new Set()
2338 |                     });
2339 |                 }
2340 | 
2341 |                 const group = documentGroups.get(uploadId);
2342 |                 group.memories.push(memory);
2343 |                 group.tags.add(...(memory.tags || []));
2344 |                 processedHashes.add(memory.content_hash);
2345 |             } else if (!processedHashes.has(memory.content_hash)) {
2346 |                 // Regular memory
2347 |                 groups.push({
2348 |                     type: 'single',
2349 |                     memory: memory
2350 |                 });
2351 |                 processedHashes.add(memory.content_hash);
2352 |             }
2353 |         }
2354 | 
2355 |         // Convert document groups to array format
2356 |         for (const group of documentGroups.values()) {
2357 |             groups.push({
2358 |                 type: 'document',
2359 |                 upload_id: group.upload_id,
2360 |                 source_file: group.source_file,
2361 |                 memories: group.memories,
2362 |                 created_at: group.created_at,
2363 |                 tags: Array.from(group.tags)
2364 |             });
2365 |         }
2366 | 
2367 |         // Sort by creation time (most recent first)
2368 |         groups.sort((a, b) => {
2369 |             const timeA = a.type === 'document' ? a.created_at : a.memory.created_at;
2370 |             const timeB = b.type === 'document' ? b.created_at : b.memory.created_at;
2371 |             return timeB - timeA;
2372 |         });
2373 | 
2374 |         return groups;
2375 |     }
2376 | 
2377 |     /**
2378 |      * Render a document group card
2379 |      */
2380 |     renderDocumentGroup(group) {
2381 |         const createdDate = new Date(group.created_at * 1000).toLocaleDateString();
2382 |         const fileName = this.escapeHtml(group.source_file);
2383 |         const chunkCount = group.memories.length;
2384 |         // Filter out metadata tags AND tags that are too long (likely corrupted/malformed)
2385 |         const uniqueTags = [...new Set(group.tags.filter(tag =>
2386 |             !tag.startsWith('upload_id:') &&
2387 |             !tag.startsWith('source_file:') &&
2388 |             !tag.startsWith('file_type:') &&
2389 |             tag.length < 100  // Reject tags longer than 100 chars (likely corrupted metadata)
2390 |         ))];
2391 | 
2392 |         return `
2393 |             <div class="document-group" data-upload-id="${this.escapeHtml(group.upload_id)}">
2394 |                 <div class="document-header">
2395 |                     <div class="document-icon">📄</div>
2396 |                     <div class="document-info">
2397 |                         <div class="document-title">${fileName}</div>
2398 |                         <div class="document-meta">
2399 |                             ${chunkCount} chunks • ${createdDate}
2400 |                         </div>
2401 |                     </div>
2402 |                 </div>
2403 |                 <div class="document-preview">
2404 |                     ${group.memories[0] ? this.escapeHtml(group.memories[0].content.substring(0, 150)) + (group.memories[0].content.length > 150 ? '...' : '') : 'No content preview available'}
2405 |                 </div>
2406 |                 ${uniqueTags.length > 0 ? `
2407 |                     <div class="document-tags">
2408 |                         ${uniqueTags.slice(0, 3).map(tag => `<span class="tag">${this.escapeHtml(tag)}</span>`).join('')}
2409 |                         ${uniqueTags.length > 3 ? `<span class="tag more">+${uniqueTags.length - 3} more</span>` : ''}
2410 |                     </div>
2411 |                 ` : ''}
2412 |                 <div class="document-actions">
2413 |                     <button class="btn-icon btn-view-chunks" title="View all chunks">
2414 |                         <svg width="16" height="16" fill="currentColor" viewBox="0 0 24 24">
2415 |                             <path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/>
2416 |                         </svg>
2417 |                         <span>View Chunks</span>
2418 |                     </button>
2419 |                     <button class="btn-icon btn-remove" title="Remove document">
2420 |                         <svg width="16" height="16" fill="currentColor" viewBox="0 0 24 24">
2421 |                             <path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
2422 |                         </svg>
2423 |                         <span>Remove</span>
2424 |                     </button>
2425 |                 </div>
2426 |             </div>
2427 |         `;
2428 |     }
2429 | 
2430 |     /**
2431 |      * Show document chunks in a modal
2432 |      */
2433 |     showDocumentChunks(group) {
2434 |         const modal = document.createElement('div');
2435 |         modal.className = 'modal-overlay';
2436 |         modal.innerHTML = `
2437 |             <div class="modal-content large-modal">
2438 |                 <div class="modal-header">
2439 |                     <h3>📄 ${this.escapeHtml(group.source_file)}</h3>
2440 |                     <button class="modal-close">&times;</button>
2441 |                 </div>
2442 |                 <div class="modal-body">
2443 |                     <div class="document-chunks">
2444 |                         ${group.memories.map((memory, index) => `
2445 |                             <div class="chunk-item">
2446 |                                 <div class="chunk-header">
2447 |                                     <span class="chunk-number">Chunk ${index + 1}</span>
2448 |                                     <div class="chunk-meta">
2449 |                                         ${memory.metadata && memory.metadata.page ? `Page ${memory.metadata.page} • ` : ''}
2450 |                                         ${memory.content.length} chars
2451 |                                     </div>
2452 |                                 </div>
2453 |                                 <div class="chunk-content">
2454 |                                     ${this.escapeHtml(memory.content)}
2455 |                                 </div>
2456 |                                 ${memory.tags && memory.tags.length > 0 ? `
2457 |                                     <div class="chunk-tags">
2458 |                                         ${memory.tags.map(tag => `<span class="tag">${this.escapeHtml(tag)}</span>`).join('')}
2459 |                                     </div>
2460 |                                 ` : ''}
2461 |                             </div>
2462 |                         `).join('')}
2463 |                     </div>
2464 |                 </div>
2465 |             </div>
2466 |         `;
2467 | 
2468 |         document.body.appendChild(modal);
2469 | 
2470 |         // Add active class to show modal (required for display: flex)
2471 |         setTimeout(() => modal.classList.add('active'), 10);
2472 | 
2473 |         // Add close handlers
2474 |         const closeModal = () => {
2475 |             modal.classList.remove('active');
2476 |             setTimeout(() => document.body.removeChild(modal), 300);
2477 |         };
2478 |         modal.querySelector('.modal-close').addEventListener('click', closeModal);
2479 |         modal.addEventListener('click', (e) => {
2480 |             if (e.target === modal) closeModal();
2481 |         });
2482 |     }
2483 | 
2484 |     /**
2485 |      * Render tags in sidebar
2486 |      */
2487 |     renderTagsSidebar(tags) {
2488 |         const container = document.getElementById('tagsCloudContainer');
2489 | 
2490 |         if (!container) {
2491 |             console.warn('tagsCloudContainer element not found - skipping tags sidebar rendering');
2492 |             return;
2493 |         }
2494 | 
2495 |         if (!tags || tags.length === 0) {
2496 |             container.innerHTML = '<div class="no-tags">No tags found.</div>';
2497 |             return;
2498 |         }
2499 | 
2500 |         // Take top tags for sidebar display
2501 |         const topTags = tags.slice(0, 10);
2502 |         container.innerHTML = topTags.map(tagData => `
2503 |             <div class="tag-item" data-tag="${this.escapeHtml(tagData.tag)}">
2504 |                 <span class="tag-name">${this.escapeHtml(tagData.tag)}</span>
2505 |                 <span class="tag-count">${tagData.count}</span>
2506 |             </div>
2507 |         `).join('');
2508 | 
2509 |         // Add click handlers
2510 |         container.querySelectorAll('.tag-item').forEach(item => {
2511 |             item.addEventListener('click', () => {
2512 |                 const tagName = item.dataset.tag;
2513 |                 const searchInput = document.getElementById('searchInput');
2514 |                 searchInput.value = `#${tagName}`;
2515 |                 this.handleSearch(`#${tagName}`);
2516 |             });
2517 |         });
2518 |     }
2519 | 
2520 |     /**
2521 |      * Render search results
2522 |      */
2523 |     renderSearchResults(results) {
2524 |         const container = document.getElementById('searchResultsList');
2525 | 
2526 |         if (!results || results.length === 0) {
2527 |             container.innerHTML = '<p class="empty-state">No results found. Try a different search term.</p>';
2528 |             return;
2529 |         }
2530 | 
2531 |         container.innerHTML = results.map(result => this.renderMemoryCard(result.memory, result)).join('');
2532 | 
2533 |         // Add click handlers
2534 |         container.querySelectorAll('.memory-card').forEach((card, index) => {
2535 |             card.addEventListener('click', () => this.handleMemoryClick(results[index].memory));
2536 |         });
2537 |     }
2538 | 
2539 |     /**
2540 |     * Render a memory card
2541 |     */
2542 |     renderMemoryCard(memory, searchResult = null) {
2543 |     const createdDate = new Date(memory.created_at * 1000).toLocaleDateString();
2544 |     const relevanceScore = searchResult &&
2545 |     searchResult.similarity_score !== null &&
2546 |     searchResult.similarity_score !== undefined &&
2547 |     !isNaN(searchResult.similarity_score) &&
2548 |     searchResult.similarity_score > 0
2549 |     ? (searchResult.similarity_score * 100).toFixed(1)
2550 |     : null;
2551 | 
2552 |     // Truncate content to 150 characters for preview
2553 |     const truncatedContent = memory.content.length > 150
2554 |     ? memory.content.substring(0, 150) + '...'
2555 |     : memory.content;
2556 | 
2557 |     return `
2558 |     <div class="memory-card" data-memory-id="${memory.content_hash}">
2559 |     <div class="memory-header">
2560 |         <div class="memory-meta">
2561 |                         <span>${createdDate}</span>
2562 |             ${memory.memory_type ? `<span> • ${memory.memory_type}</span>` : ''}
2563 |         ${relevanceScore ? `<span> • ${relevanceScore}% match</span>` : ''}
2564 |         </div>
2565 |                 </div>
2566 | 
2567 |     <div class="memory-content">
2568 |     ${this.escapeHtml(truncatedContent)}
2569 |     </div>
2570 | 
2571 |         ${memory.tags && memory.tags.length > 0 ? `
2572 |                 <div class="memory-tags">
2573 |                         ${memory.tags.map(tag => `<span class="tag">${this.escapeHtml(tag)}</span>`).join('')}
2574 |                     </div>
2575 |                 ` : ''}
2576 |             </div>
2577 |         `;
2578 |     }
2579 | 
2580 |     /**
2581 |      * Update dashboard statistics
2582 |      */
2583 |     updateDashboardStats(stats) {
2584 |         const totalMemoriesEl = document.getElementById('totalMemories');
2585 |         if (totalMemoriesEl) {
2586 |             totalMemoriesEl.textContent = stats.total_memories || '0';
2587 |         }
2588 | 
2589 |         const recentMemoriesEl = document.getElementById('recentMemories');
2590 |         if (recentMemoriesEl) {
2591 |             recentMemoriesEl.textContent = stats.memories_this_week || '0';
2592 |         }
2593 | 
2594 |         const uniqueTagsEl = document.getElementById('uniqueTags');
2595 |         if (uniqueTagsEl) {
2596 |             uniqueTagsEl.textContent = stats.unique_tags || '0';
2597 |         }
2598 | 
2599 |         const storageBackendEl = document.getElementById('storageBackend');
2600 |         if (storageBackendEl) {
2601 |             storageBackendEl.textContent = stats.backend || 'unknown';
2602 |         }
2603 |     }
2604 | 
2605 |     /**
2606 |      * Update search results count
2607 |      */
2608 |     updateResultsCount(count) {
2609 |         const element = document.getElementById('resultsCount');
2610 |         if (element) {
2611 |             element.textContent = `${count} result${count !== 1 ? 's' : ''}`;
2612 |         }
2613 |     }
2614 | 
2615 |     /**
2616 |      * Update active filters display
2617 |      */
2618 |     updateActiveFilters() {
2619 |         const activeFiltersContainer = document.getElementById('activeFilters');
2620 |         const filtersList = document.getElementById('activeFiltersList');
2621 | 
2622 |         if (!activeFiltersContainer || !filtersList) return;
2623 | 
2624 |         const tagFilter = document.getElementById('tagFilter')?.value?.trim();
2625 |         const dateFilter = document.getElementById('dateFilter')?.value;
2626 |         const typeFilter = document.getElementById('typeFilter')?.value;
2627 | 
2628 |         const filters = [];
2629 | 
2630 |         if (tagFilter) {
2631 |             const tags = tagFilter.split(',').map(t => t.trim()).filter(t => t);
2632 |             tags.forEach(tag => {
2633 |                 filters.push({
2634 |                     type: 'tag',
2635 |                     value: tag,
2636 |                     label: `Tag: ${tag}`
2637 |                 });
2638 |             });
2639 |         }
2640 | 
2641 |         if (dateFilter) {
2642 |             const dateLabels = {
2643 |                 'today': 'Today',
2644 |                 'week': 'This week',
2645 |                 'month': 'This month',
2646 |                 'year': 'This year'
2647 |             };
2648 |             filters.push({
2649 |                 type: 'date',
2650 |                 value: dateFilter,
2651 |                 label: `Date: ${dateLabels[dateFilter] || dateFilter}`
2652 |             });
2653 |         }
2654 | 
2655 |         if (typeFilter) {
2656 |             const typeLabels = {
2657 |                 'note': 'Notes',
2658 |                 'code': 'Code',
2659 |                 'reference': 'References',
2660 |                 'idea': 'Ideas'
2661 |             };
2662 |             filters.push({
2663 |                 type: 'type',
2664 |                 value: typeFilter,
2665 |                 label: `Type: ${typeLabels[typeFilter] || typeFilter}`
2666 |             });
2667 |         }
2668 | 
2669 |         if (filters.length === 0) {
2670 |             activeFiltersContainer.style.display = 'none';
2671 |             return;
2672 |         }
2673 | 
2674 |         activeFiltersContainer.style.display = 'block';
2675 |         filtersList.innerHTML = filters.map(filter => `
2676 |             <div class="filter-pill">
2677 |                 ${this.escapeHtml(filter.label)}
2678 |                 <button class="remove-filter" data-filter-type="${this.escapeHtml(filter.type)}" data-filter-value="${this.escapeHtml(filter.value)}">
2679 |                     ×
2680 |                 </button>
2681 |             </div>
2682 |         `).join('');
2683 | 
2684 |         // Add event listeners for filter removal
2685 |         filtersList.addEventListener('click', (e) => {
2686 |             const button = e.target.closest('.remove-filter');
2687 |             if (!button) return;
2688 | 
2689 |             const type = button.dataset.filterType;
2690 |             const value = button.dataset.filterValue;
2691 |             this.removeFilter(type, value);
2692 |         });
2693 |     }
2694 | 
2695 |     /**
2696 |      * Remove a specific filter
2697 |      */
2698 |     removeFilter(type, value) {
2699 |         switch (type) {
2700 |             case 'tag':
2701 |                 const tagInput = document.getElementById('tagFilter');
2702 |                 if (tagInput) {
2703 |                     const tags = tagInput.value.split(',').map(t => t.trim()).filter(t => t && t !== value);
2704 |                     tagInput.value = tags.join(', ');
2705 |                 }
2706 |                 break;
2707 |             case 'date':
2708 |                 const dateSelect = document.getElementById('dateFilter');
2709 |                 if (dateSelect) {
2710 |                     dateSelect.value = '';
2711 |                 }
2712 |                 break;
2713 |             case 'type':
2714 |                 const typeSelect = document.getElementById('typeFilter');
2715 |                 if (typeSelect) {
2716 |                     typeSelect.value = '';
2717 |                 }
2718 |                 break;
2719 |         }
2720 |         this.handleFilterChange();
2721 |     }
2722 | 
2723 |     /**
2724 |      * Clear all filters
2725 |      */
2726 |     clearAllFilters() {
2727 |         const tagFilter = document.getElementById('tagFilter');
2728 |         const dateFilter = document.getElementById('dateFilter');
2729 |         const typeFilter = document.getElementById('typeFilter');
2730 | 
2731 |         if (tagFilter) tagFilter.value = '';
2732 |         if (dateFilter) dateFilter.value = '';
2733 |         if (typeFilter) typeFilter.value = '';
2734 | 
2735 |         this.searchResults = [];
2736 |         this.renderSearchResults([]);
2737 |         this.updateResultsCount(0);
2738 |         this.updateActiveFilters();
2739 | 
2740 |         this.showToast('All filters cleared', 'info');
2741 |     }
2742 | 
2743 |     /**
2744 |      * Handle live search toggle
2745 |      */
2746 |     handleLiveSearchToggle(event) {
2747 |         this.liveSearchEnabled = event.target.checked;
2748 |         const modeText = document.getElementById('searchModeText');
2749 |         if (modeText) {
2750 |             modeText.textContent = this.liveSearchEnabled ? 'Live Search' : 'Manual Search';
2751 |         }
2752 | 
2753 |         // Show a toast to indicate the mode change
2754 |         this.showToast(
2755 |             `Search mode: ${this.liveSearchEnabled ? 'Live (searches as you type)' : 'Manual (click Search button)'}`,
2756 |             'info'
2757 |         );
2758 |     }
2759 | 
2760 |     /**
2761 |      * Handle debounced filter changes for live search
2762 |      */
2763 |     handleDebouncedFilterChange() {
2764 |         // Clear any existing timer
2765 |         if (this.debounceTimer) {
2766 |             clearTimeout(this.debounceTimer);
2767 |         }
2768 | 
2769 |         // Only trigger search if live search is enabled
2770 |         if (this.liveSearchEnabled) {
2771 |             this.debounceTimer = setTimeout(() => {
2772 |                 this.handleFilterChange();
2773 |             }, 300); // 300ms debounce
2774 |         }
2775 |     }
2776 | 
2777 |     /**
2778 |      * Handle memory added via SSE
2779 |      */
2780 |     handleMemoryAdded(memory) {
2781 |         if (this.currentView === 'dashboard') {
2782 |             this.loadDashboardData();
2783 |         }
2784 |     }
2785 | 
2786 |     /**
2787 |      * Handle memory deleted via SSE
2788 |      */
2789 |     handleMemoryDeleted(memoryId) {
2790 |         // Remove from current view
2791 |         const cards = document.querySelectorAll(`[data-memory-id="${memoryId}"]`);
2792 |         cards.forEach(card => card.remove());
2793 | 
2794 |         // Update search results if in search view
2795 |         if (this.currentView === 'search') {
2796 |             this.searchResults = this.searchResults.filter(r => r.memory.content_hash !== memoryId);
2797 |             this.updateResultsCount(this.searchResults.length);
2798 |         }
2799 |     }
2800 | 
2801 |     /**
2802 |      * Handle memory updated via SSE
2803 |      */
2804 |     handleMemoryUpdated(memory) {
2805 |         // Refresh relevant views
2806 |         if (this.currentView === 'dashboard') {
2807 |             this.loadDashboardData();
2808 |         }
2809 |     }
2810 | 
2811 |     /**
2812 |      * Update connection status indicator
2813 |      */
2814 |     updateConnectionStatus(status) {
2815 |         const statusElement = document.getElementById('connectionStatus');
2816 |         if (statusElement) {
2817 |             const indicator = statusElement.querySelector('.status-indicator');
2818 |             const text = statusElement.querySelector('.status-text');
2819 |             if (!indicator || !text) return;
2820 | 
2821 |             // Reset indicator classes
2822 |             indicator.className = 'status-indicator';
2823 | 
2824 |             switch (status) {
2825 |                 case 'connected':
2826 |                     text.textContent = 'Connected';
2827 |                     // Connected uses default green color (no additional class needed)
2828 |                     break;
2829 |                 case 'connecting':
2830 |                     text.textContent = 'Connecting...';
2831 |                     indicator.classList.add('connecting');
2832 |                     break;
2833 |                 case 'disconnected':
2834 |                     text.textContent = 'Disconnected';
2835 |                     indicator.classList.add('disconnected');
2836 |                     break;
2837 |                 default:
2838 |                     text.textContent = 'Unknown';
2839 |                     indicator.classList.add('disconnected');
2840 |             }
2841 |         }
2842 |     }
2843 | 
2844 |     /**
2845 |      * Generic API call wrapper
2846 |      */
2847 |     async apiCall(endpoint, method = 'GET', data = null) {
2848 |         const options = {
2849 |             method: method,
2850 |             headers: {
2851 |                 'Content-Type': 'application/json',
2852 |             }
2853 |         };
2854 | 
2855 |         if (data) {
2856 |             options.body = JSON.stringify(data);
2857 |         }
2858 | 
2859 |         const response = await fetch(`${this.apiBase}${endpoint}`, options);
2860 | 
2861 |         if (!response.ok) {
2862 |             const errorData = await response.json().catch(() => ({}));
2863 |             throw new Error(errorData.detail || `HTTP ${response.status}`);
2864 |         }
2865 | 
2866 |         return await response.json();
2867 |     }
2868 | 
2869 |     /**
2870 |      * Modal management
2871 |      */
2872 |     openModal(modal) {
2873 |         modal.classList.add('active');
2874 |         document.body.style.overflow = 'hidden';
2875 | 
2876 |         // Focus first input
2877 |         const firstInput = modal.querySelector('input, textarea');
2878 |         if (firstInput) {
2879 |             firstInput.focus();
2880 |         }
2881 |     }
2882 | 
2883 |     closeModal(modal) {
2884 |         modal.classList.remove('active');
2885 |         document.body.style.overflow = '';
2886 |     }
2887 | 
2888 |     /**
2889 |      * Loading state management
2890 |      */
2891 |     setLoading(loading) {
2892 |         this.isLoading = loading;
2893 |         const indicator = document.getElementById('loadingOverlay');
2894 |         if (indicator) {
2895 |             if (loading) {
2896 |                 indicator.classList.remove('hidden');
2897 |             } else {
2898 |                 indicator.classList.add('hidden');
2899 |             }
2900 |         }
2901 |     }
2902 | 
2903 |     /**
2904 |      * View document memory chunks
2905 |      */
2906 |     async viewDocumentMemory(uploadId) {
2907 |         try {
2908 |             this.setLoading(true);
2909 |             const response = await this.apiCall(`/documents/search-content/${uploadId}`);
2910 | 
2911 |             if (response.status === 'success' || response.status === 'partial') {
2912 |                 // Show modal
2913 |                 const modal = document.getElementById('memoryViewerModal');
2914 |                 const filename = document.getElementById('memoryViewerFilename');
2915 |                 const stats = document.getElementById('memoryViewerStats');
2916 |                 const chunksList = document.getElementById('memoryChunksList');
2917 | 
2918 |                 filename.textContent = response.filename || 'Document';
2919 |                 stats.textContent = `${response.total_found} chunk${response.total_found !== 1 ? 's' : ''} found`;
2920 | 
2921 |                 // Render chunks
2922 |                 if (response.memories && response.memories.length > 0) {
2923 |                     const chunksHtml = response.memories.map((memory, index) => {
2924 |                         const chunkIndex = memory.chunk_index !== undefined ? memory.chunk_index : index;
2925 |                         const page = memory.page ? ` • Page ${memory.page}` : '';
2926 |                         const contentPreview = memory.content.length > 300
2927 |                             ? memory.content.substring(0, 300) + '...'
2928 |                             : memory.content;
2929 | 
2930 |                         return `
2931 |                             <div class="memory-chunk-item">
2932 |                                 <div class="chunk-header">
2933 |                                     <span class="chunk-number">Chunk ${chunkIndex + 1}${page}</span>
2934 |                                     <span class="chunk-hash" title="${memory.content_hash}">${memory.content_hash.substring(0, 12)}...</span>
2935 |                                 </div>
2936 |                                 <div class="chunk-content">${this.escapeHtml(contentPreview)}</div>
2937 |                                 <div class="chunk-tags">
2938 |                                     ${memory.tags.map(tag => `<span class="tag">${this.escapeHtml(tag)}</span>`).join('')}
2939 |                                 </div>
2940 |                             </div>
2941 |                         `;
2942 |                     }).join('');
2943 | 
2944 |                     chunksList.innerHTML = chunksHtml;
2945 |                 } else {
2946 |                     chunksList.innerHTML = '<p class="text-muted">No memory chunks found for this document.</p>';
2947 |                 }
2948 | 
2949 |                 modal.style.display = 'flex';
2950 | 
2951 |                 if (response.status === 'partial') {
2952 |                     this.showToast(`Found ${response.total_found} chunks (partial results)`, 'warning');
2953 |                 }
2954 |             } else {
2955 |                 this.showToast('Failed to load document memories', 'error');
2956 |             }
2957 |         } catch (error) {
2958 |             console.error('Error viewing document memory:', error);
2959 |             this.showToast('Error loading document memories', 'error');
2960 |         } finally {
2961 |             this.setLoading(false);
2962 |         }
2963 |     }
2964 | 
2965 |     /**
2966 |      * Close memory viewer modal
2967 |      */
2968 |     closeMemoryViewer() {
2969 |         const modal = document.getElementById('memoryViewerModal');
2970 |         if (modal) {
2971 |             modal.style.display = 'none';
2972 |         }
2973 |     }
2974 | 
2975 |     /**
2976 |      * Remove document and its memories
2977 |      */
2978 |     async removeDocument(uploadId, filename) {
2979 |         console.log('removeDocument called with:', { uploadId, filename, currentView: this.currentView });
2980 | 
2981 |         if (!confirm(`Remove "${filename}" and all its memory chunks?\n\nThis action cannot be undone.`)) {
2982 |             console.log('User cancelled removal');
2983 |             return;
2984 |         }
2985 | 
2986 |         try {
2987 |             this.setLoading(true);
2988 |             console.log('Making DELETE request to:', `/documents/remove/${uploadId}`);
2989 | 
2990 |             const response = await this.apiCall(`/documents/remove/${uploadId}`, 'DELETE');
2991 | 
2992 |             console.log('Delete response:', response);
2993 | 
2994 |             if (response.status === 'success') {
2995 |                 this.showToast(`Removed "${filename}" (${response.memories_deleted} memories deleted)`, 'success');
2996 |                 // Refresh the current view (Dashboard or Documents tab)
2997 |                 console.log('Refreshing view:', this.currentView);
2998 |                 if (this.currentView === 'dashboard') {
2999 |                     await this.loadDashboardData();
3000 |                 } else if (this.currentView === 'documents') {
3001 |                     await this.loadUploadHistory();
3002 |                 }
3003 |             } else {
3004 |                 console.error('Removal failed with response:', response);
3005 |                 this.showToast('Failed to remove document', 'error');
3006 |             }
3007 |         } catch (error) {
3008 |             console.error('Error removing document:', error);
3009 |             console.error('Error stack:', error.stack);
3010 |             this.showToast('Error removing document', 'error');
3011 |         } finally {
3012 |             this.setLoading(false);
3013 |         }
3014 |     }
3015 | 
3016 |     /**
3017 |      * Search document content
3018 |      */
3019 |     async searchDocumentContent(query) {
3020 |         try {
3021 |             this.setLoading(true);
3022 |             const resultsContainer = document.getElementById('docSearchResults');
3023 |             const resultsList = document.getElementById('docSearchResultsList');
3024 |             const resultsCount = document.getElementById('docSearchCount');
3025 | 
3026 |             // Use the regular search endpoint but filter for document memories
3027 |             // Higher n_results to ensure we get enough document results after filtering
3028 |             const response = await this.apiCall('/search', 'POST', {
3029 |                 query: query,
3030 |                 n_results: 100
3031 |             });
3032 | 
3033 |             if (response.results) {
3034 |                 // Filter results to only show document-type memories
3035 |                 const documentResults = response.results.filter(r =>
3036 |                     r.memory?.memory_type === 'document' || (r.memory?.tags && r.memory.tags.some(tag => tag.startsWith('upload_id:')))
3037 |                 );
3038 | 
3039 |                 // Limit display to top 20 most relevant document results
3040 |                 const displayResults = documentResults.slice(0, 20);
3041 | 
3042 |                 resultsCount.textContent = `${documentResults.length} result${documentResults.length !== 1 ? 's' : ''}${documentResults.length > 20 ? ' (showing top 20)' : ''}`;
3043 | 
3044 |                 if (displayResults.length > 0) {
3045 |                     const resultsHtml = displayResults.map(result => {
3046 |                         const mem = result.memory;
3047 |                         const uploadIdTag = mem.tags?.find(tag => tag.startsWith('upload_id:'));
3048 |                         const sourceFile = mem.metadata?.source_file || 'Unknown file';
3049 |                         const contentPreview = mem.content.length > 200
3050 |                             ? mem.content.substring(0, 200) + '...'
3051 |                             : mem.content;
3052 | 
3053 |                         return `
3054 |                             <div class="search-result-item">
3055 |                                 <div class="result-header">
3056 |                                     <strong>${this.escapeHtml(sourceFile)}</strong>
3057 |                                     <span class="similarity-score">${Math.round((result.similarity_score || 0) * 100)}% match</span>
3058 |                                 </div>
3059 |                                 <div class="result-content">${this.escapeHtml(contentPreview)}</div>
3060 |                                 <div class="result-tags">
3061 |                                     ${(mem.tags || []).slice(0, 5).map(tag => `<span class="tag">${this.escapeHtml(tag)}</span>`).join('')}
3062 |                                 </div>
3063 |                             </div>
3064 |                         `;
3065 |                     }).join('');
3066 | 
3067 |                     resultsList.innerHTML = resultsHtml;
3068 |                 } else {
3069 |                     resultsList.innerHTML = '<p class="text-muted">No matching document content found. Try different search terms.</p>';
3070 |                 }
3071 | 
3072 |                 resultsContainer.style.display = 'block';
3073 |             } else {
3074 |                 this.showToast('Search failed', 'error');
3075 |             }
3076 |         } catch (error) {
3077 |             console.error('Error searching documents:', error);
3078 |             this.showToast('Error performing search', 'error');
3079 |         } finally {
3080 |             this.setLoading(false);
3081 |         }
3082 |     }
3083 | 
3084 |     /**
3085 |      * Toast notification system
3086 |      */
3087 |     showToast(message, type = 'info', duration = 5000) {
3088 |         const container = document.getElementById('toastContainer');
3089 |         const toast = document.createElement('div');
3090 |         toast.className = `toast ${type}`;
3091 |         toast.textContent = message;
3092 | 
3093 |         container.appendChild(toast);
3094 | 
3095 |         // Auto-remove after duration
3096 |         setTimeout(() => {
3097 |             toast.remove();
3098 |         }, duration);
3099 | 
3100 |         // Click to remove
3101 |         toast.addEventListener('click', () => {
3102 |             toast.remove();
3103 |         });
3104 |     }
3105 | 
3106 |     /**
3107 |      * Utility: Debounce function
3108 |      */
3109 |     debounce(func, wait) {
3110 |         let timeout;
3111 |         return function executedFunction(...args) {
3112 |             const later = () => {
3113 |                 clearTimeout(timeout);
3114 |                 func(...args);
3115 |             };
3116 |             clearTimeout(timeout);
3117 |             timeout = setTimeout(later, wait);
3118 |         };
3119 |     }
3120 | 
3121 |     /**
3122 |      * Load settings from localStorage
3123 |      */
3124 |     loadSettings() {
3125 |         try {
3126 |             const saved = localStorage.getItem('memoryDashboardSettings');
3127 |             if (saved) {
3128 |                 this.settings = { ...this.settings, ...JSON.parse(saved) };
3129 |             }
3130 |         } catch (error) {
3131 |             console.warn('Failed to load settings:', error);
3132 |         }
3133 |     }
3134 | 
3135 |     /**
3136 |      * Save settings to localStorage
3137 |      */
3138 |     saveSettingsToStorage() {
3139 |         try {
3140 |             localStorage.setItem('memoryDashboardSettings', JSON.stringify(this.settings));
3141 |         } catch (error) {
3142 |             console.error('Failed to save settings:', error);
3143 |             this.showToast('Failed to save settings. Your preferences will not be persisted.', 'error');
3144 |         }
3145 |     }
3146 | 
3147 |     /**
3148 |      * Apply theme to the page
3149 |      */
3150 |     applyTheme(theme = this.settings.theme) {
3151 |         const isDark = theme === 'dark';
3152 |         document.body.classList.toggle('dark-mode', isDark);
3153 | 
3154 |         // Toggle icon visibility using CSS classes
3155 |         const sunIcon = document.getElementById('sunIcon');
3156 |         const moonIcon = document.getElementById('moonIcon');
3157 |         if (sunIcon && moonIcon) {
3158 |             sunIcon.classList.toggle('hidden', isDark);
3159 |             moonIcon.classList.toggle('hidden', !isDark);
3160 |         }
3161 |     }
3162 | 
3163 |     /**
3164 |      * Toggle between light and dark theme
3165 |      */
3166 |     toggleTheme() {
3167 |         const newTheme = this.settings.theme === 'dark' ? 'light' : 'dark';
3168 |         this.settings.theme = newTheme;
3169 |         this.applyTheme(newTheme);
3170 |         this.saveSettingsToStorage();
3171 |         this.showToast(`Switched to ${newTheme} mode`, 'success');
3172 |     }
3173 | 
3174 |     /**
3175 |      * Open settings modal
3176 |      */
3177 |     async openSettingsModal() {
3178 |         const modal = document.getElementById('settingsModal');
3179 | 
3180 |         // Populate form with current settings
3181 |         document.getElementById('themeSelect').value = this.settings.theme;
3182 |         document.getElementById('viewDensity').value = this.settings.viewDensity;
3183 |         document.getElementById('previewLines').value = this.settings.previewLines;
3184 | 
3185 |         // Reset system info to loading state
3186 |         this.resetSystemInfoLoadingState();
3187 | 
3188 |         // Load system information and backup status
3189 |         await Promise.all([
3190 |             this.loadSystemInfo(),
3191 |             this.checkBackupStatus()
3192 |         ]);
3193 | 
3194 |         this.openModal(modal);
3195 |     }
3196 | 
3197 |     /**
3198 |      * Reset system info fields to loading state
3199 |      */
3200 |     resetSystemInfoLoadingState() {
3201 |         Object.keys(MemoryDashboard.SYSTEM_INFO_CONFIG).forEach(id => {
3202 |             const element = document.getElementById(id);
3203 |             if (element) {
3204 |                 element.textContent = 'Loading...';
3205 |             }
3206 |         });
3207 |     }
3208 | 
3209 |     /**
3210 |      * Load system information for settings modal
3211 |      */
3212 |     async loadSystemInfo() {
3213 |         try {
3214 |             // Use Promise.allSettled for robust error handling
3215 |             const [healthResult, detailedHealthResult] = await Promise.allSettled([
3216 |                 this.apiCall('/health'),
3217 |                 this.apiCall('/health/detailed')
3218 |             ]);
3219 | 
3220 |             const apiData = {
3221 |                 health: healthResult.status === 'fulfilled' ? healthResult.value : null,
3222 |                 detailedHealth: detailedHealthResult.status === 'fulfilled' ? detailedHealthResult.value : null
3223 |             };
3224 | 
3225 |             // Update fields using configuration
3226 |             Object.entries(MemoryDashboard.SYSTEM_INFO_CONFIG).forEach(([fieldId, config]) => {
3227 |                 const element = document.getElementById(fieldId);
3228 |                 if (!element) return;
3229 | 
3230 |                 let value = null;
3231 |                 for (const source of config.sources) {
3232 |                     const apiResponse = apiData[source.api];
3233 |                     if (apiResponse) {
3234 |                         value = this.getNestedValue(apiResponse, source.path);
3235 |                         if (value !== undefined && value !== null) break;
3236 |                     }
3237 |                 }
3238 | 
3239 |                 element.textContent = config.formatter(value);
3240 |             });
3241 | 
3242 |             // Log warnings for failed API calls
3243 |             if (healthResult.status === 'rejected') {
3244 |                 console.warn('Failed to load health endpoint:', healthResult.reason);
3245 |             }
3246 |             if (detailedHealthResult.status === 'rejected') {
3247 |                 console.warn('Failed to load detailed health endpoint:', detailedHealthResult.reason);
3248 |             }
3249 |         } catch (error) {
3250 |             console.error('Unexpected error loading system info:', error);
3251 |             // Set all system info fields that are still in loading state to error
3252 |             Object.keys(MemoryDashboard.SYSTEM_INFO_CONFIG).forEach(id => {
3253 |                 const element = document.getElementById(id);
3254 |                 if (element && element.textContent === 'Loading...') {
3255 |                     element.textContent = 'Error';
3256 |                 }
3257 |             });
3258 |         }
3259 |     }
3260 | 
3261 |     /**
3262 |      * Get nested object value by path string
3263 |      * @param {Object} obj - Object to traverse
3264 |      * @param {string} path - Dot-separated path (e.g., 'storage.primary_stats.embedding_model')
3265 |      * @returns {*} Value at path or undefined
3266 |      */
3267 |     getNestedValue(obj, path) {
3268 |         return path.split('.').reduce((current, key) => current?.[key], obj);
3269 |     }
3270 | 
3271 |     /**
3272 |      * Format uptime seconds into human readable string
3273 |      * @param {number} seconds - Uptime in seconds
3274 |      * @returns {string} Formatted uptime string
3275 |      */
3276 |     static formatUptime(seconds) {
3277 |         const days = Math.floor(seconds / 86400);
3278 |         const hours = Math.floor((seconds % 86400) / 3600);
3279 |         const minutes = Math.floor((seconds % 3600) / 60);
3280 | 
3281 |         const parts = [];
3282 |         if (days > 0) parts.push(`${days}d`);
3283 |         if (hours > 0) parts.push(`${hours}h`);
3284 |         if (minutes > 0) parts.push(`${minutes}m`);
3285 | 
3286 |         return parts.length > 0 ? parts.join(' ') : '< 1m';
3287 |     }
3288 | 
3289 |     /**
3290 |      * Save settings from modal
3291 |      */
3292 |     saveSettings() {
3293 |         // Get values from form
3294 |         const theme = document.getElementById('themeSelect').value;
3295 |         const viewDensity = document.getElementById('viewDensity').value;
3296 |         const previewLines = parseInt(document.getElementById('previewLines').value, 10);
3297 | 
3298 |         // Update settings
3299 |         this.settings.theme = theme;
3300 |         this.settings.viewDensity = viewDensity;
3301 |         this.settings.previewLines = previewLines;
3302 | 
3303 |         // Apply changes
3304 |         this.applyTheme(theme);
3305 |         this.saveSettingsToStorage();
3306 | 
3307 |         // Close modal and show confirmation
3308 |         this.closeModal(document.getElementById('settingsModal'));
3309 |         this.showToast('Settings saved successfully', 'success');
3310 |     }
3311 | 
3312 |     // ===== MANAGE TAB METHODS =====
3313 | 
3314 |     /**
3315 |      * Load manage tab data
3316 |     */
3317 |     async loadManageData() {
3318 |     try {
3319 |             // Load tag statistics for bulk operations
3320 |             await this.loadTagSelectOptions();
3321 |             await this.loadTagManagementStats();
3322 |         } catch (error) {
3323 |             console.error('Failed to load manage data:', error);
3324 |             this.showToast('Failed to load management data', 'error');
3325 |         }
3326 |     }
3327 | 
3328 |     /**
3329 |      * Load tag options for bulk delete select
3330 |      */
3331 |     async loadTagSelectOptions() {
3332 |         try {
3333 |             const response = await fetch(`${this.apiBase}/manage/tags/stats`);
3334 |             if (!response.ok) throw new Error('Failed to load tags');
3335 | 
3336 |             const data = await response.json();
3337 |             const select = document.getElementById('deleteTagSelect');
3338 |             if (!select) return;
3339 | 
3340 |             // Clear existing options except the first
3341 |             while (select.children.length > 1) {
3342 |                 select.removeChild(select.lastChild);
3343 |             }
3344 | 
3345 |             // Add tag options
3346 |             data.tags.forEach(tagStat => {
3347 |                 const option = document.createElement('option');
3348 |                 option.value = tagStat.tag;
3349 |                 option.textContent = `${tagStat.tag} (${tagStat.count} memories)`;
3350 |                 option.dataset.count = tagStat.count;  // Store count in data attribute
3351 |                 select.appendChild(option);
3352 |             });
3353 |         } catch (error) {
3354 |             console.error('Failed to load tag options:', error);
3355 |         }
3356 |     }
3357 | 
3358 |     /**
3359 |      * Load tag management statistics
3360 |      */
3361 |     async loadTagManagementStats() {
3362 |         const container = document.getElementById('tagManagementContainer');
3363 |         if (!container) return;
3364 | 
3365 |         try {
3366 |             const response = await fetch(`${this.apiBase}/manage/tags/stats`);
3367 |             if (!response.ok) throw new Error('Failed to load tag stats');
3368 | 
3369 |             const data = await response.json();
3370 |             this.renderTagManagementTable(data);
3371 |         } catch (error) {
3372 |             console.error('Failed to load tag management stats:', error);
3373 |             container.innerHTML = '<p class="error">Failed to load tag statistics</p>';
3374 |         }
3375 |     }
3376 | 
3377 |     /**
3378 |      * Render tag management table
3379 |      */
3380 |     renderTagManagementTable(data) {
3381 |         const container = document.getElementById('tagManagementContainer');
3382 |         if (!container) return;
3383 | 
3384 |         let html = '<table class="tag-stats-table">';
3385 |         html += '<thead><tr>';
3386 |         html += '<th>Tag</th>';
3387 |         html += '<th>Count</th>';
3388 |         html += '<th>Actions</th>';
3389 |         html += '</tr></thead><tbody>';
3390 | 
3391 |         data.tags.forEach(tagStat => {
3392 |         html += '<tr>';
3393 |         html += `<td class="tag-name">${tagStat.tag}</td>`;
3394 |         html += `<td class="tag-count">${tagStat.count}</td>`;
3395 |         html += '<td class="tag-actions">';
3396 |         html += `<button class="tag-action-btn" data-action="rename-tag" data-tag="${this.escapeHtml(tagStat.tag)}">Rename</button>`;
3397 |         html += `<button class="tag-action-btn danger" data-action="delete-tag" data-tag="${this.escapeHtml(tagStat.tag)}" data-count="${tagStat.count}">Delete</button>`;
3398 |         html += '</td></tr>';
3399 |         });
3400 | 
3401 |         html += '</tbody></table>';
3402 |         container.innerHTML = html;
3403 | 
3404 |         // Add event listeners for tag actions
3405 |         container.addEventListener('click', (e) => {
3406 |             const button = e.target.closest('[data-action]');
3407 |             if (!button) return;
3408 | 
3409 |             const action = button.dataset.action;
3410 |             const tag = button.dataset.tag;
3411 | 
3412 |             if (action === 'rename-tag') {
3413 |                 this.renameTag(tag);
3414 |             } else if (action === 'delete-tag') {
3415 |                 const count = parseInt(button.dataset.count, 10);
3416 |                 this.deleteTag(tag, count);
3417 |             }
3418 |         });
3419 |     }
3420 | 
3421 |     /**
3422 |      * Handle bulk delete by tag
3423 |      */
3424 |     async handleBulkDeleteByTag() {
3425 |         const select = document.getElementById('deleteTagSelect');
3426 |         const tag = select.value;
3427 | 
3428 |         if (!tag) {
3429 |             this.showToast('Please select a tag to delete', 'warning');
3430 |             return;
3431 |         }
3432 | 
3433 |         // Extract count from data attribute
3434 |         const option = select.querySelector(`option[value="${tag}"]`);
3435 |         const count = parseInt(option.dataset.count, 10) || 0;
3436 | 
3437 |         if (!await this.confirmBulkOperation(`Delete ${count} memories with tag "${tag}"?`)) {
3438 |             return;
3439 |         }
3440 | 
3441 |         this.setLoading(true);
3442 |         try {
3443 |             const response = await fetch(`${this.apiBase}/manage/bulk-delete`, {
3444 |                 method: 'POST',
3445 |                 headers: { 'Content-Type': 'application/json' },
3446 |                 body: JSON.stringify({
3447 |                     tag: tag,
3448 |                     confirm_count: count
3449 |                 })
3450 |             });
3451 | 
3452 |             const result = await response.json();
3453 |             if (result.success) {
3454 |                 this.showToast(result.message, 'success');
3455 |                 await this.loadManageData(); // Refresh data
3456 |                 await this.loadDashboardData(); // Refresh dashboard stats
3457 |             } else {
3458 |                 this.showToast(result.message, 'error');
3459 |             }
3460 |         } catch (error) {
3461 |             console.error('Bulk delete failed:', error);
3462 |             this.showToast('Bulk delete operation failed', 'error');
3463 |         } finally {
3464 |             this.setLoading(false);
3465 |         }
3466 |     }
3467 | 
3468 |     /**
3469 |      * Handle cleanup duplicates
3470 |      */
3471 |     async handleCleanupDuplicates() {
3472 |         if (!await this.confirmBulkOperation('Remove all duplicate memories?')) {
3473 |             return;
3474 |         }
3475 | 
3476 |         this.setLoading(true);
3477 |         try {
3478 |             const response = await fetch(`${this.apiBase}/manage/cleanup-duplicates`, {
3479 |                 method: 'POST'
3480 |             });
3481 | 
3482 |             const result = await response.json();
3483 |             if (result.success) {
3484 |                 this.showToast(result.message, 'success');
3485 |                 await this.loadManageData();
3486 |                 await this.loadDashboardData();
3487 |             } else {
3488 |                 this.showToast(result.message, 'error');
3489 |             }
3490 |         } catch (error) {
3491 |             console.error('Cleanup duplicates failed:', error);
3492 |             this.showToast('Cleanup operation failed', 'error');
3493 |         } finally {
3494 |             this.setLoading(false);
3495 |         }
3496 |     }
3497 | 
3498 |     /**
3499 |      * Handle bulk delete by date
3500 |      */
3501 |     async handleBulkDeleteByDate() {
3502 |         const dateInput = document.getElementById('deleteDateInput');
3503 |         const date = dateInput.value;
3504 | 
3505 |         if (!date) {
3506 |             this.showToast('Please select a date', 'warning');
3507 |             return;
3508 |         }
3509 | 
3510 |         if (!await this.confirmBulkOperation(`Delete all memories before ${date}?`)) {
3511 |             return;
3512 |         }
3513 | 
3514 |         this.setLoading(true);
3515 |         try {
3516 |             const response = await fetch(`${this.apiBase}/manage/bulk-delete`, {
3517 |                 method: 'POST',
3518 |                 headers: { 'Content-Type': 'application/json' },
3519 |                 body: JSON.stringify({
3520 |                     before_date: date
3521 |                 })
3522 |             });
3523 | 
3524 |             const result = await response.json();
3525 |             if (result.success) {
3526 |                 this.showToast(result.message, 'success');
3527 |                 await this.loadManageData();
3528 |                 await this.loadDashboardData();
3529 |             } else {
3530 |                 this.showToast(result.message, 'error');
3531 |             }
3532 |         } catch (error) {
3533 |             console.error('Bulk delete by date failed:', error);
3534 |             this.showToast('Bulk delete operation failed', 'error');
3535 |         } finally {
3536 |             this.setLoading(false);
3537 |         }
3538 |     }
3539 | 
3540 |     /**
3541 |      * Handle database optimization
3542 |      */
3543 |     async handleOptimizeDatabase() {
3544 |         this.showToast('Database optimization not yet implemented', 'warning');
3545 |     }
3546 | 
3547 |     /**
3548 |      * Handle index rebuild
3549 |      */
3550 |     async handleRebuildIndex() {
3551 |         this.showToast('Index rebuild not yet implemented', 'warning');
3552 |     }
3553 | 
3554 |     /**
3555 |      * Rename a tag
3556 |      */
3557 |     async renameTag(oldTag) {
3558 |         const newTag = prompt(`Rename tag "${oldTag}" to:`, oldTag);
3559 |         if (!newTag || newTag === oldTag) return;
3560 | 
3561 |         this.showToast('Tag renaming not yet implemented', 'warning');
3562 |     }
3563 | 
3564 |     /**
3565 |      * Delete a tag
3566 |      */
3567 |     async deleteTag(tag, count) {
3568 |         if (!await this.confirmBulkOperation(`Delete tag "${tag}" from ${count} memories?`)) {
3569 |             return;
3570 |         }
3571 | 
3572 |         this.showToast('Tag deletion not yet implemented', 'warning');
3573 |     }
3574 | 
3575 |     // ===== ANALYTICS TAB METHODS =====
3576 | 
3577 |     /**
3578 |      * Load analytics tab data
3579 |      */
3580 |     async loadAnalyticsData() {
3581 |         try {
3582 |             await Promise.all([
3583 |                 this.loadAnalyticsOverview(),
3584 |                 this.loadMemoryGrowthChart(),
3585 |                 this.loadTagUsageChart(),
3586 |                 this.loadMemoryTypesChart(),
3587 |                 this.loadActivityHeatmapChart(),
3588 |                 this.loadTopTagsReport(),
3589 |                 this.loadRecentActivityReport(),
3590 |                 this.loadStorageReport()
3591 |             ]);
3592 |         } catch (error) {
3593 |             console.error('Failed to load analytics data:', error);
3594 |             this.showToast('Failed to load analytics data', 'error');
3595 |         }
3596 |     }
3597 | 
3598 |     /**
3599 |      * Load analytics overview metrics
3600 |      */
3601 |     async loadAnalyticsOverview() {
3602 |         try {
3603 |             const response = await fetch(`${this.apiBase}/analytics/overview`);
3604 |             if (!response.ok) throw new Error('Failed to load overview');
3605 | 
3606 |             const data = await response.json();
3607 | 
3608 |             // Update metric cards
3609 |             this.updateElementText('analyticsTotalMemories', data.total_memories || 0);
3610 |             this.updateElementText('analyticsThisWeek', data.memories_this_week || 0);
3611 |             this.updateElementText('analyticsUniqueTags', data.unique_tags || 0);
3612 |             this.updateElementText('analyticsDbSize', data.database_size_mb ?
3613 |                 `${data.database_size_mb.toFixed(1)} MB` : 'N/A');
3614 |         } catch (error) {
3615 |             console.error('Failed to load analytics overview:', error);
3616 |         }
3617 |     }
3618 | 
3619 |     /**
3620 |      * Load memory growth chart
3621 |      */
3622 |     async loadMemoryGrowthChart() {
3623 |         const container = document.getElementById('memoryGrowthChart');
3624 |         const period = document.getElementById('growthPeriodSelect').value;
3625 | 
3626 |         if (!container) return;
3627 | 
3628 |         try {
3629 |             const response = await fetch(`${this.apiBase}/analytics/memory-growth?period=${period}`);
3630 |             if (!response.ok) throw new Error('Failed to load growth data');
3631 | 
3632 |             const data = await response.json();
3633 |             this.renderMemoryGrowthChart(container, data);
3634 |         } catch (error) {
3635 |             console.error('Failed to load memory growth:', error);
3636 |             container.innerHTML = '<p class="error">Failed to load growth chart</p>';
3637 |         }
3638 |     }
3639 | 
3640 |     /**
3641 |      * Render memory growth chart
3642 |      */
3643 |     renderMemoryGrowthChart(container, data) {
3644 |         if (!data.data_points || data.data_points.length === 0) {
3645 |             container.innerHTML = '<p>No growth data available</p>';
3646 |             return;
3647 |         }
3648 | 
3649 |         // Find max count for scaling
3650 |         const recentPoints = data.data_points.slice(-10);
3651 |         const maxCount = Math.max(...recentPoints.map(p => p.count), 1);
3652 | 
3653 |         let html = '<div class="simple-chart">';
3654 | 
3655 |         recentPoints.forEach(point => {
3656 |             // Normalize bar width relative to max, then convert to pixels (200px scale)
3657 |             const barWidthPx = (point.count / maxCount) * 200;
3658 |             const displayCount = point.count || 0;
3659 |             const displayCumulative = point.cumulative || 0;
3660 |             // Use label if available, otherwise fall back to date for backward compatibility
3661 |             const displayLabel = point.label || point.date;
3662 | 
3663 |             html += `<div class="chart-row">
3664 |                 <div class="chart-bar" style="width: ${barWidthPx}px"></div>
3665 |                 <span class="chart-value">+${displayCount} <small>(${displayCumulative} total)</small></span>
3666 |                 <span class="chart-label">${displayLabel}</span>
3667 |             </div>`;
3668 |         });
3669 | 
3670 |         html += '</div>';
3671 |         container.innerHTML = html;
3672 |     }
3673 | 
3674 |     /**
3675 |      * Load tag usage chart
3676 |      */
3677 |     async loadTagUsageChart() {
3678 |         const container = document.getElementById('tagUsageChart');
3679 |         if (!container) return;
3680 | 
3681 |         try {
3682 |             const response = await fetch(`${this.apiBase}/analytics/tag-usage`);
3683 |             if (!response.ok) throw new Error('Failed to load tag usage');
3684 | 
3685 |             const data = await response.json();
3686 |             this.renderTagUsageChart(container, data);
3687 |         } catch (error) {
3688 |             console.error('Failed to load tag usage:', error);
3689 |             container.innerHTML = '<p class="error">Failed to load tag usage chart</p>';
3690 |         }
3691 |     }
3692 | 
3693 |     /**
3694 |      * Render tag usage chart
3695 |      */
3696 |     renderTagUsageChart(container, data) {
3697 |         if (!data.tags || data.tags.length === 0) {
3698 |             container.innerHTML = '<p>No tags found</p>';
3699 |             return;
3700 |         }
3701 | 
3702 |         // Filter tags with >10 memories, aggregate the rest
3703 |         const significantTags = data.tags.filter(t => t.count > 10);
3704 |         const minorTags = data.tags.filter(t => t.count <= 10);
3705 | 
3706 |         let html = '<div class="simple-chart">';
3707 | 
3708 |         // Render significant tags
3709 |         significantTags.forEach(tag => {
3710 |             const barWidthPx = (tag.percentage / 100) * 200; // Convert percentage to pixels (200px scale)
3711 |             html += `<div class="chart-row">
3712 |                 <div class="chart-bar" style="width: ${barWidthPx}px"></div>
3713 |                 <span class="chart-value">${tag.count} (${tag.percentage}%)</span>
3714 |                 <span class="chart-label">${tag.tag}</span>
3715 |             </div>`;
3716 |         });
3717 | 
3718 |         // Add "diverse" category if there are minor tags
3719 |         if (minorTags.length > 0) {
3720 |             const diverseCount = minorTags.reduce((sum, t) => sum + t.count, 0);
3721 |             const diversePercentage = minorTags.reduce((sum, t) => sum + t.percentage, 0);
3722 |             const barWidthPx = (diversePercentage / 100) * 200;
3723 |             html += `<div class="chart-row">
3724 |                 <div class="chart-bar" style="width: ${barWidthPx}px"></div>
3725 |                 <span class="chart-value">${diverseCount} (${diversePercentage.toFixed(1)}%)</span>
3726 |                 <span class="chart-label" title="${minorTags.length} tags with ≤10 memories each">diverse (${minorTags.length} tags)</span>
3727 |             </div>`;
3728 |         }
3729 | 
3730 |         html += '</div>';
3731 |         container.innerHTML = html;
3732 |     }
3733 | 
3734 |     /**
3735 |      * Load memory types chart
3736 |      */
3737 |     async loadMemoryTypesChart() {
3738 |         const container = document.getElementById('memoryTypesChart');
3739 |         if (!container) return;
3740 | 
3741 |         try {
3742 |             const response = await fetch(`${this.apiBase}/analytics/memory-types`);
3743 |             if (!response.ok) throw new Error('Failed to load memory types');
3744 | 
3745 |             const data = await response.json();
3746 |             this.renderMemoryTypesChart(container, data);
3747 |         } catch (error) {
3748 |             console.error('Failed to load memory types:', error);
3749 |             container.innerHTML = '<p class="error">Failed to load memory types chart</p>';
3750 |         }
3751 |     }
3752 | 
3753 |     /**
3754 |      * Render memory types chart
3755 |      */
3756 |     renderMemoryTypesChart(container, data) {
3757 |         if (!data.types || data.types.length === 0) {
3758 |             container.innerHTML = '<p>No memory types found</p>';
3759 |             return;
3760 |         }
3761 | 
3762 |         // Filter types with >10 memories, aggregate the rest
3763 |         const significantTypes = data.types.filter(t => t.count > 10);
3764 |         const minorTypes = data.types.filter(t => t.count <= 10);
3765 | 
3766 |         let html = '<div class="simple-chart">';
3767 | 
3768 |         // Render significant types
3769 |         significantTypes.forEach(type => {
3770 |             const barWidthPx = (type.percentage / 100) * 200; // Convert percentage to pixels (200px scale)
3771 |             const typeName = type.memory_type || 'untyped';
3772 |             html += `<div class="chart-row">
3773 |                 <div class="chart-bar" style="width: ${barWidthPx}px"></div>
3774 |                 <span class="chart-value">${type.count} (${type.percentage.toFixed(1)}%)</span>
3775 |                 <span class="chart-label" title="${typeName}">${typeName}</span>
3776 |             </div>`;
3777 |         });
3778 | 
3779 |         // Add "diverse" category if there are minor types
3780 |         if (minorTypes.length > 0) {
3781 |             const diverseCount = minorTypes.reduce((sum, t) => sum + t.count, 0);
3782 |             const diversePercentage = minorTypes.reduce((sum, t) => sum + t.percentage, 0);
3783 |             const barWidthPx = (diversePercentage / 100) * 200;
3784 |             html += `<div class="chart-row">
3785 |                 <div class="chart-bar" style="width: ${barWidthPx}px"></div>
3786 |                 <span class="chart-value">${diverseCount} (${diversePercentage.toFixed(1)}%)</span>
3787 |                 <span class="chart-label" title="${minorTypes.length} types with ≤10 memories each">diverse (${minorTypes.length} types)</span>
3788 |             </div>`;
3789 |         }
3790 | 
3791 |         html += '</div>';
3792 |         container.innerHTML = html;
3793 |     }
3794 | 
3795 |     /**
3796 |     * Load top tags report
3797 |     */
3798 |     async loadTopTagsReport() {
3799 |     const container = document.getElementById('topTagsList');
3800 |     const period = document.getElementById('topTagsPeriodSelect')?.value || '30d';
3801 |         if (!container) return;
3802 | 
3803 |     try {
3804 |     const response = await fetch(`${this.apiBase}/analytics/top-tags?period=${period}`);
3805 |             if (!response.ok) throw new Error('Failed to load top tags');
3806 | 
3807 |     const data = await response.json();
3808 |         this.renderTopTagsReport(container, data);
3809 |     } catch (error) {
3810 |     console.error('Failed to load top tags:', error);
3811 |         container.innerHTML = '<p class="error">Failed to load top tags</p>';
3812 |         }
3813 |     }
3814 | 
3815 |     /**
3816 |     * Render top tags report
3817 |     */
3818 |     renderTopTagsReport(container, data) {
3819 |     if (!data.tags || data.tags.length === 0) {
3820 |     container.innerHTML = '<p>No tags found</p>';
3821 |     return;
3822 |     }
3823 | 
3824 |     let html = '<div class="enhanced-tags-report">';
3825 |     html += `<div class="report-period">Period: ${data.period}</div>`;
3826 |     html += '<ul class="tags-list">';
3827 |     data.tags.slice(0, 10).forEach(tag => {
3828 |         const trendIcon = tag.trending ? '📈' : '';
3829 |             const growthText = tag.growth_rate !== null ? ` (${tag.growth_rate > 0 ? '+' : ''}${tag.growth_rate}%)` : '';
3830 |         html += `<li>
3831 |                 <div class="tag-header">
3832 |                     <strong>${tag.tag}</strong>${trendIcon}
3833 |                     <span class="tag-count">${tag.count} memories (${tag.percentage}%)${growthText}</span>
3834 |                 </div>`;
3835 |             if (tag.co_occurring_tags && tag.co_occurring_tags.length > 0) {
3836 |                 html += '<div class="tag-cooccurrence">Often with: ';
3837 |                 html += tag.co_occurring_tags.slice(0, 3).map(co => `${co.tag} (${co.strength.toFixed(2)})`).join(', ');
3838 |                 html += '</div>';
3839 |             }
3840 |             html += '</li>';
3841 |         });
3842 |         html += '</ul></div>';
3843 | 
3844 |         container.innerHTML = html;
3845 |     }
3846 | 
3847 |     /**
3848 |     * Load recent activity report
3849 |     */
3850 |     async loadRecentActivityReport() {
3851 |     const container = document.getElementById('recentActivityList');
3852 |     const granularity = document.getElementById('activityGranularitySelect')?.value || 'daily';
3853 |         if (!container) return;
3854 | 
3855 |     try {
3856 |     const response = await fetch(`${this.apiBase}/analytics/activity-breakdown?granularity=${granularity}`);
3857 |     if (!response.ok) throw new Error('Failed to load activity breakdown');
3858 | 
3859 |     const data = await response.json();
3860 |     this.renderRecentActivityReport(container, data);
3861 |     } catch (error) {
3862 |     console.error('Failed to load recent activity:', error);
3863 |     container.innerHTML = '<p class="error">Failed to load recent activity</p>';
3864 |     }
3865 |     }
3866 | 
3867 |     /**
3868 |     * Render recent activity report
3869 |     */
3870 |     renderRecentActivityReport(container, data) {
3871 |     let html = '<div class="activity-breakdown">';
3872 | 
3873 |     // Summary stats
3874 |     html += '<div class="activity-summary">';
3875 |         html += `<div class="activity-stat"><strong>Active Days:</strong> ${data.active_days}/${data.total_days}</div>`;
3876 |     html += `<div class="activity-stat"><strong>Current Streak:</strong> ${data.current_streak} days</div>`;
3877 |     html += `<div class="activity-stat"><strong>Longest Streak:</strong> ${data.longest_streak} days</div>`;
3878 |     html += '</div>';
3879 | 
3880 |     if (data.peak_times && data.peak_times.length > 0) {
3881 |     html += '<div class="peak-times">';
3882 |         html += '<strong>Peak Times:</strong> ';
3883 |         html += data.peak_times.join(', ');
3884 |             html += '</div>';
3885 |     }
3886 | 
3887 |         // Activity breakdown chart
3888 |         if (data.breakdown && data.breakdown.length > 0) {
3889 |             html += '<div class="activity-chart">';
3890 |             // Calculate total count for percentage-based distribution
3891 |             const totalCount = data.breakdown.reduce((sum, d) => sum + d.count, 0);
3892 |             const maxCount = Math.max(...data.breakdown.map(d => d.count));
3893 | 
3894 |             data.breakdown.forEach(item => {
3895 |                 // Show percentage of total activity, with minimum width for visibility
3896 |                 const percentage = totalCount > 0 ? (item.count / totalCount * 100) : 0;
3897 |                 // Use percentage for bar width, but scale up for better visualization
3898 |                 // Use max count for scaling to ensure largest bar reaches reasonable width
3899 |                 const barWidth = totalCount > 0 ? (item.count / maxCount * 100) : 0;
3900 | 
3901 |                 html += `<div class="activity-bar-row">
3902 |                     <span class="activity-label">${item.label}</span>
3903 |                     <div class="activity-bar" style="width: ${barWidth}%" title="${item.count} memories (${percentage.toFixed(1)}%)"></div>
3904 |                     <span class="activity-count">${item.count} (${percentage.toFixed(1)}%)</span>
3905 |                 </div>`;
3906 |             });
3907 | 
3908 |             html += '</div>';
3909 |         }
3910 | 
3911 |         html += '</div>';
3912 |         container.innerHTML = html;
3913 |     }
3914 | 
3915 |     /**
3916 |     * Load activity heatmap chart
3917 |     */
3918 |     async loadActivityHeatmapChart() {
3919 |     const container = document.getElementById('activityHeatmapChart');
3920 |         const period = document.getElementById('heatmapPeriodSelect').value;
3921 | 
3922 |         if (!container) return;
3923 | 
3924 |         try {
3925 |             const response = await fetch(`${this.apiBase}/analytics/activity-heatmap?days=${period}`);
3926 |             if (!response.ok) throw new Error('Failed to load heatmap data');
3927 | 
3928 |             const data = await response.json();
3929 |             this.renderActivityHeatmapChart(container, data);
3930 |         } catch (error) {
3931 |             console.error('Failed to load activity heatmap:', error);
3932 |             container.innerHTML = '<p class="error">Failed to load activity heatmap</p>';
3933 |         }
3934 |     }
3935 | 
3936 |     /**
3937 |      * Render activity heatmap chart
3938 |      */
3939 |     renderActivityHeatmapChart(container, data) {
3940 |         if (!data.data || data.data.length === 0) {
3941 |             container.innerHTML = '<p>No activity data available</p>';
3942 |             return;
3943 |         }
3944 | 
3945 |         // Create calendar grid
3946 |         let html = '<div class="activity-heatmap">';
3947 |         html += '<div class="heatmap-stats">';
3948 |         html += `<span>${data.total_days} active days</span>`;
3949 |         html += `<span>Max: ${data.max_count} memories/day</span>`;
3950 |         html += '</div>';
3951 | 
3952 |         // Group by months
3953 |         const months = {};
3954 |         data.data.forEach(day => {
3955 |             const date = new Date(day.date);
3956 |             const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
3957 |             if (!months[monthKey]) {
3958 |                 months[monthKey] = [];
3959 |             }
3960 |             months[monthKey].push(day);
3961 |         });
3962 | 
3963 |         // Render each month
3964 |         Object.keys(months).sort().reverse().forEach(monthKey => {
3965 |             const [year, month] = monthKey.split('-');
3966 |             const monthName = new Date(year, month - 1).toLocaleString('default', { month: 'short' });
3967 | 
3968 |             html += `<div class="heatmap-month">`;
3969 |             html += `<div class="month-label">${monthName} ${year}</div>`;
3970 |             html += '<div class="month-grid">';
3971 | 
3972 |             // Create 7x6 grid (weeks x days)
3973 |             const monthData = months[monthKey];
3974 |             // Parse date in local timezone to avoid day-of-week shifting near timezone boundaries
3975 |             const [fYear, fMonth, fDay] = monthData[0].date.split('-').map(Number);
3976 |             const firstDay = new Date(fYear, fMonth - 1, fDay).getDay();
3977 | 
3978 |             // Add empty cells for days before month starts
3979 |             for (let i = 0; i < firstDay; i++) {
3980 |                 html += '<div class="heatmap-cell empty"></div>';
3981 |             }
3982 | 
3983 |             // Add cells for each day
3984 |             monthData.forEach(day => {
3985 |                 const level = day.level;
3986 |                 const tooltip = `${day.date}: ${day.count} memories`;
3987 |                 html += `<div class="heatmap-cell level-${level}" title="${tooltip}"></div>`;
3988 |             });
3989 | 
3990 |             html += '</div></div>';
3991 |         });
3992 | 
3993 |         // Legend
3994 |         html += '<div class="heatmap-legend">';
3995 |         html += '<span>Less</span>';
3996 |         for (let i = 0; i <= 4; i++) {
3997 |             html += `<div class="legend-cell level-${i}"></div>`;
3998 |         }
3999 |         html += '<span>More</span>';
4000 |         html += '</div>';
4001 | 
4002 |         html += '</div>';
4003 |         container.innerHTML = html;
4004 |     }
4005 | 
4006 |     /**
4007 |      * Handle heatmap period change
4008 |      */
4009 |     async handleHeatmapPeriodChange() {
4010 |         await this.loadActivityHeatmapChart();
4011 |     }
4012 | 
4013 |     /**
4014 |      * Handle top tags period change
4015 |      */
4016 |     async handleTopTagsPeriodChange() {
4017 |         await this.loadTopTagsReport();
4018 |     }
4019 | 
4020 |     /**
4021 |      * Handle activity granularity change
4022 |      */
4023 |     async handleActivityGranularityChange() {
4024 |         await this.loadRecentActivityReport();
4025 |     }
4026 | 
4027 |     /**
4028 |      * Load storage report
4029 |      */
4030 |     async loadStorageReport() {
4031 |         const container = document.getElementById('storageReport');
4032 |         if (!container) return;
4033 | 
4034 |         try {
4035 |             const response = await fetch(`${this.apiBase}/analytics/storage-stats`);
4036 |             if (!response.ok) throw new Error('Failed to load storage stats');
4037 | 
4038 |             const data = await response.json();
4039 |             this.renderStorageReport(container, data);
4040 |         } catch (error) {
4041 |             console.error('Failed to load storage report:', error);
4042 |             container.innerHTML = '<p class="error">Failed to load storage report</p>';
4043 |         }
4044 |     }
4045 | 
4046 |     /**
4047 |      * Render storage report
4048 |      */
4049 |     renderStorageReport(container, data) {
4050 |         let html = '<div class="storage-report">';
4051 | 
4052 |         // Summary stats
4053 |         html += '<div class="storage-summary">';
4054 |         html += `<div class="storage-stat"><strong>Total Size:</strong> ${data.total_size_mb} MB</div>`;
4055 |         html += `<div class="storage-stat"><strong>Average Memory:</strong> ${data.average_memory_size} chars</div>`;
4056 |         html += `<div class="storage-stat"><strong>Efficiency:</strong> ${data.storage_efficiency}%</div>`;
4057 |         html += '</div>';
4058 | 
4059 |         // Largest memories
4060 |         if (data.largest_memories && data.largest_memories.length > 0) {
4061 |             html += '<h4>Largest Memories</h4>';
4062 |             html += '<ul class="largest-memories">';
4063 |             data.largest_memories.slice(0, 5).forEach(memory => {
4064 |                 // Backend provides created_at as ISO string, not timestamp
4065 |                 const date = memory.created_at ? new Date(memory.created_at).toLocaleDateString() : 'Unknown';
4066 |                 // Backend provides size_kb and preview (not size and content_preview)
4067 |                 const sizeDisplay = memory.size_kb ? `${memory.size_kb} KB` : `${memory.size_bytes || 0} bytes`;
4068 |                 html += `<li>
4069 |                     <div class="memory-size">${sizeDisplay}</div>
4070 |                     <div class="memory-preview">${this.escapeHtml(memory.preview || '')}</div>
4071 |                     <div class="memory-meta">${date} • Tags: ${memory.tags.join(', ') || 'none'}</div>
4072 |                 </li>`;
4073 |             });
4074 |             html += '</ul>';
4075 |         }
4076 | 
4077 |         html += '</div>';
4078 |         container.innerHTML = html;
4079 |     }
4080 | 
4081 |     /**
4082 |      * Handle growth period change
4083 |      */
4084 |     async handleGrowthPeriodChange() {
4085 |         await this.loadMemoryGrowthChart();
4086 |     }
4087 | 
4088 |     // ===== UTILITY METHODS =====
4089 | 
4090 |     /**
4091 |      * Show confirmation dialog for bulk operations
4092 |      */
4093 |     async confirmBulkOperation(message) {
4094 |         return confirm(`⚠️ WARNING: ${message}
4095 | 
4096 | This action cannot be undone. Are you sure?`);
4097 |     }
4098 | 
4099 |     /**
4100 |      * Update element text content
4101 |      */
4102 |     updateElementText(elementId, text) {
4103 |         const element = document.getElementById(elementId);
4104 |         if (element) {
4105 |             element.textContent = text;
4106 |         }
4107 |     }
4108 | 
4109 |     /**
4110 |      * Cleanup when page unloads
4111 |      */
4112 |     destroy() {
4113 |         if (this.eventSource) {
4114 |             this.eventSource.close();
4115 |         }
4116 |     }
4117 | }
4118 | 
4119 | // Initialize the application when DOM is ready
4120 | console.log('⚡ Registering DOMContentLoaded listener');
4121 | document.addEventListener('DOMContentLoaded', () => {
4122 |     console.log('⚡ DOMContentLoaded fired - Creating MemoryDashboard instance');
4123 |     window.app = new MemoryDashboard();
4124 |     console.log('⚡ MemoryDashboard created, window.app =', window.app);
4125 | });
4126 | 
4127 | // Cleanup on page unload
4128 | window.addEventListener('beforeunload', () => {
4129 |     if (window.app) {
4130 |         window.app.destroy();
4131 |     }
4132 | });
```
Page 45/47FirstPrevNextLast