MetaList 0.3.6__tar.gz → 0.3.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {metalist-0.3.6 → metalist-0.3.8}/MANIFEST.in +1 -0
- {metalist-0.3.6/metalist.egg-info → metalist-0.3.8}/PKG-INFO +1 -1
- {metalist-0.3.6 → metalist-0.3.8}/app/api/middleware/auth.py +2 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/api/routes/auth.py +90 -1
- {metalist-0.3.6 → metalist-0.3.8}/app/api/routes/backups.py +56 -12
- {metalist-0.3.6 → metalist-0.3.8}/app/main.py +88 -32
- {metalist-0.3.6 → metalist-0.3.8}/app/server_runtime.py +5 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/namespace_deletion_worker.py +32 -3
- metalist-0.3.8/app/services/namespace_rename_jobs.py +115 -0
- metalist-0.3.8/app/services/namespace_rename_worker.py +160 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/namespace_switcher.py +341 -26
- {metalist-0.3.6 → metalist-0.3.8}/app/static/config/command_palette_tags.json +4 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/command-palette/command-palette-controller.js +15 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/command-palette/endpoint-registry.js +7 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/config.js +2 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/modals/delete-namespace-modal.js +53 -4
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/modals/delete-namespace-validation.js +10 -3
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/modals/namespace-modals.js +165 -0
- metalist-0.3.8/app/static/js/modules/modals/namespace-rename-validation.js +32 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/namespace-deleted.js +29 -3
- metalist-0.3.8/app/static/js/namespace-renamed.js +116 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/templates/namespace_deleted.html +1 -1
- metalist-0.3.8/app/templates/namespace_renamed.html +67 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/version.py +1 -1
- {metalist-0.3.6 → metalist-0.3.8}/main.py +23 -24
- {metalist-0.3.6 → metalist-0.3.8/metalist.egg-info}/PKG-INFO +1 -1
- {metalist-0.3.6 → metalist-0.3.8}/metalist.egg-info/SOURCES.txt +5 -0
- {metalist-0.3.6 → metalist-0.3.8}/pyproject.toml +2 -0
- {metalist-0.3.6 → metalist-0.3.8}/README.md +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/__init__.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/api/__init__.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/api/deps.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/api/dev.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/api/request_auth.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/api/routes/files.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/api/routes/mcp.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/api/routes/memory.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/api/routes/notes.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/api/routes/ontology.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/api/routes/reminders.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/api/routes/sounds.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/api/routes/test.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/api/transactions.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/config.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/db/__init__.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/db/backup_settings_sql.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/db/engine.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/db/file_schema.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/db/file_session.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/db/files_sql.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/db/link_titles_sql.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/db/migrations.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/db/notes_sql.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/db/ontology_rules_sql.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/db/reminders_sql.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/db/schema.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/db/search_history_sql.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/db/session.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/db/settings_sql.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/db/sounds_sql.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/db/tab_state_sql.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/encryption_audit.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/mcp/__init__.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/mcp/__main__.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/mcp/errors.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/mcp/policy.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/mcp/read_service.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/mcp/server.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/mcp/tools.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/models/commands.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/models/database.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/models/entities.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/models/enums.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/models/linked_list.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/models/list_operations.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/models/list_traversal.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/models/note_crud.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/models/utils.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/presentation/__init__.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/presentation/render/__init__.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/presentation/render/note_renderer.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/presentation/templates.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/security/encryption.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/__init__.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/auth.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/auth_cache_state.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/auth_service.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/backlinks.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/backup_service.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/backup_settings_service.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/base_service.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/client_state_service.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/content_cache.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/content_formatting.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/date_filtering.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/dependencies.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/diagnostics.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/embedded_references.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/encryption.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/exception_capture.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/file_registry.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/file_storage.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/html_export.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/html_unformatting.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/hydration_state.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/integrity.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/latex_rendering.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/link_titles.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/login_rate_limit.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/maintenance_mode.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/markdown_rendering.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/memory.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/memory_service.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/namespace_deletion_jobs.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/note_image_tags.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/note_service.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/note_store.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/ontology_rules_store.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/query_service.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/reminders.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/root_sorting.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/runtime_hardening.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/search_history.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/search_index.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/search_query.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/search_text.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/session_timeout_service.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/shell_session_service.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/snapshot.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/sound_storage.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/store.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/sync.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/sync_state.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/tab_state.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/tag_case.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/tag_ontology.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/tag_rename.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/tag_suggestions.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/tag_term_matching.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/test_reset.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/tokens.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/transaction_manager.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/undo_service.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/undo_state.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/view_cache.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/view_diff.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/view_state.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/services/windows_process_control.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/startup_js_sanity.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/startup_sanity.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/startup_sanity_config.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/css/forms.css +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/css/main.css +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/img/favicon.ico +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/locked.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/main.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/activity-tracker.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/api-client.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/async-result.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/auth.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/client-state-api.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/client-state-migration.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/command-palette/click-activation-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/command-palette/preferences-store.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/command-palette/tag-config-loader.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/command-palette/usage-store.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/connectivity-monitor.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/context-menu/context-menu-registry.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/context-menu/context-menu-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/context-menu/input-caret-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/dom-utils-fixed.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/dom-utils.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/editor-commands.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/editor-selection.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/editor-toolbar.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/error-handler.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/error-overlay.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/location-flags.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/login-namespace-picker.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/modals/alphabetize-root-notes-modal.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/modals/backup-restore-modal.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/modals/backup-result-modal.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/modals/backup-retention-modal.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/modals/backup-settings-modal.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/modals/base-modal.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/modals/help-modal.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/modals/memory-modal.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/modals/namespace-loading-page.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/modals/namespace-ports-profile.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/modals/ontology-modal.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/modals/password-modal.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/modals/prioritize-modal.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/modals/random-password-modal.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/modals/reminder-modal.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/modals/reset-updated-at-modal.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/modals/session-timeout-modal.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/modals/sound-manager-modal.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/modals/version-info-modal.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/actions/content-actions.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/actions/history-actions.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/actions/note-actions.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/actions/search-actions.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/actions/selection-actions.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/actions/ui-actions.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/events/context-menu-events.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/events/focus-events.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/events/inactivity-events.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/events/input-events.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/events/keyboard-events.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/events/mouse-events.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/events/search-events.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/mode-context.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/mode-logger.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/mode-manager-controller.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/animated-scroll-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/backlinks-panel-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/clipboard-shortcut-policy-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/collapse-affordance-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/collapse-editing-policy-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/command-gate-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/context-menu-target-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/date-filter-indicator-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/date-filter-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/differential-view-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/edit-session-collapse-policy-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/edit-session-collapse-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/embedded-image-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/file-image-preview-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/file-reference-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/filtered-refresh-selection-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/html-paste-sanitizer-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/image-context-menu-action-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/image-file-insert-choice-modal-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/infinite-scroll-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/link-context-menu-action-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/markdown-render-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/note-click-target-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/note-clipboard-write-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/note-collapse-animation-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/note-context-menu-action-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/note-drag-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/note-reposition-animation-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/note-split-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/password-clipboard-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/polling-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/rhs-panel-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/root-date-separator-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/root-sort-indicator-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/root-sort-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/scroll-anchor-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/scroll-restoration-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/scroll-to-top-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/search-contexts-overlay-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/search-debounce-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/search-input-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/search-interaction-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/search-redaction-reveal-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/search-suggestions-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/search-syntax-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/shell-output-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/tab-dom-cache-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/tab-duplication-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/tab-state-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/tag-bar-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/tag-suggestions-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/tag-syntax-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/todo-toggle-editing-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/mode-manager/services/view-mode-search-shortcut-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/password-generator.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/reminder-store.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/reminder-surface-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/selection-markers.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/session-auth.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/sound-service.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/tag-token.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/modules/uuid.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/js/vendor/markdown-it.min.js +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/static/video/login-startup.mp4 +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/templates/base.html +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/templates/index.html +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/templates/locked.html +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/templates/maintenance.html +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/templates/notes_list.html +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/__init__.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/alphabetize_root_notes.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/base.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/collapse.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/copy_note.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/create_child.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/create_note.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/create_sibling.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/delete_subtree.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/expand.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/indent.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/lock.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/move.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/move_to_top.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/outdent.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/paste_child.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/paste_sibling.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/prioritize.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/record_edit_mode.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/redo.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/rename_tag.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/reset_updated_at.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/run_shell.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/search_comment_autofill.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/search_context_tags.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/set_collapse_bulk.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/set_collapse_in_context.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/split_note.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/toggle_reference_mode.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/toggle_todo_done.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/undo.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/unformat_content.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/update_content.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/usecases/view.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/utils/__init__.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/utils/encryption.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/app/utils/text_utils.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/convert-from-legacy.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/mcp_client.py +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/metalist.egg-info/dependency_links.txt +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/metalist.egg-info/entry_points.txt +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/metalist.egg-info/requires.txt +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/metalist.egg-info/top_level.txt +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/scripts/generate-lan-cert.sh +0 -0
- {metalist-0.3.6 → metalist-0.3.8}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: metalist
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.8
|
|
4
4
|
Summary: A minimalist single-user note-taking app with SSR, fast in-memory tree operations, and efficient sync.
|
|
5
5
|
Project-URL: Homepage, https://github.com/evolvingstuff/metalist
|
|
6
6
|
Project-URL: Repository, https://github.com/evolvingstuff/metalist
|
|
@@ -20,10 +20,12 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|
|
20
20
|
f"{API_PREFIX}/auth/status",
|
|
21
21
|
f"{API_PREFIX}/auth/session",
|
|
22
22
|
f"{API_PREFIX}/auth/namespaces/delete-jobs/",
|
|
23
|
+
f"{API_PREFIX}/auth/namespaces/rename-jobs/",
|
|
23
24
|
f"{API_PREFIX}/mcp",
|
|
24
25
|
"/mcp-client-v2",
|
|
25
26
|
"/mcp-client",
|
|
26
27
|
"/namespace-deleted",
|
|
28
|
+
"/namespace-renamed",
|
|
27
29
|
"/static/", # CSS/JS files needed for login page
|
|
28
30
|
"/favicon.ico",
|
|
29
31
|
"/locked",
|
|
@@ -47,8 +47,10 @@ from app.services.namespace_switcher import delete_current_namespace
|
|
|
47
47
|
from app.services.namespace_switcher import delete_namespace
|
|
48
48
|
from app.services.namespace_switcher import open_login_namespace
|
|
49
49
|
from app.services.namespace_switcher import open_or_launch_namespace
|
|
50
|
+
from app.services.namespace_switcher import rename_current_namespace
|
|
50
51
|
from app.services.namespace_switcher import save_namespace_port_profiles
|
|
51
52
|
from app.services.namespace_deletion_jobs import load_namespace_deletion_job
|
|
53
|
+
from app.services.namespace_rename_jobs import load_namespace_rename_job
|
|
52
54
|
from app.server_runtime import NamespaceLaunchProfile
|
|
53
55
|
from app.server_runtime import resolve_namespace_directory
|
|
54
56
|
from app.server_runtime import resolve_namespaced_database_path
|
|
@@ -590,6 +592,15 @@ def _require_string_field(payload: dict[str, object], field_name: str) -> str:
|
|
|
590
592
|
return value
|
|
591
593
|
|
|
592
594
|
|
|
595
|
+
def _require_string_field_allow_empty(payload: dict[str, object], field_name: str) -> str:
|
|
596
|
+
if field_name not in payload:
|
|
597
|
+
raise HTTPException(status_code=400, detail=f"{field_name} is required")
|
|
598
|
+
value = payload[field_name]
|
|
599
|
+
if not isinstance(value, str):
|
|
600
|
+
raise HTTPException(status_code=400, detail=f"{field_name} must be a string")
|
|
601
|
+
return value
|
|
602
|
+
|
|
603
|
+
|
|
593
604
|
def _require_int_field(payload: dict[str, object], field_name: str) -> int:
|
|
594
605
|
if field_name not in payload:
|
|
595
606
|
raise HTTPException(status_code=400, detail=f"{field_name} is required")
|
|
@@ -968,7 +979,8 @@ def save_namespace_ports(
|
|
|
968
979
|
def _delete_namespace_from_body(*, body: dict[str, object], target_namespace: str) -> dict[str, object]:
|
|
969
980
|
normalized_target_namespace = validate_namespace(namespace=target_namespace)
|
|
970
981
|
confirmed_namespace = _require_string_field(body, "confirmed_namespace")
|
|
971
|
-
current_password =
|
|
982
|
+
current_password = _require_string_field_allow_empty(body, "current_password")
|
|
983
|
+
redirect_namespace = _require_string_field(body, "redirect_namespace")
|
|
972
984
|
|
|
973
985
|
if _namespace_requires_password(namespace=normalized_target_namespace):
|
|
974
986
|
_verify_namespace_password(
|
|
@@ -989,6 +1001,7 @@ def _delete_namespace_from_body(*, body: dict[str, object], target_namespace: st
|
|
|
989
1001
|
current_namespace=ACTIVE_NAMESPACE,
|
|
990
1002
|
target_namespace=normalized_target_namespace,
|
|
991
1003
|
confirmed_namespace=confirmed_namespace,
|
|
1004
|
+
redirect_namespace=redirect_namespace,
|
|
992
1005
|
)
|
|
993
1006
|
if delete_capture.captured_exception is not None:
|
|
994
1007
|
exc = delete_capture.captured_exception
|
|
@@ -1019,11 +1032,33 @@ def namespace_delete_preflight(
|
|
|
1019
1032
|
target_requires_password = False
|
|
1020
1033
|
if target_exists:
|
|
1021
1034
|
target_requires_password = _namespace_requires_password(namespace=target_namespace)
|
|
1035
|
+
catalog = build_namespace_catalog(
|
|
1036
|
+
environ=os.environ,
|
|
1037
|
+
current_namespace=ACTIVE_NAMESPACE,
|
|
1038
|
+
)
|
|
1039
|
+
raw_namespaces = catalog["namespaces"]
|
|
1040
|
+
if not isinstance(raw_namespaces, list):
|
|
1041
|
+
raise RuntimeError("Namespace catalog missing namespaces")
|
|
1042
|
+
redirect_namespaces: list[str] = []
|
|
1043
|
+
for raw_entry in raw_namespaces:
|
|
1044
|
+
if not isinstance(raw_entry, dict):
|
|
1045
|
+
raise RuntimeError("Namespace catalog entry must be an object")
|
|
1046
|
+
namespace = raw_entry["namespace"]
|
|
1047
|
+
if not isinstance(namespace, str) or namespace == "":
|
|
1048
|
+
raise RuntimeError("Namespace catalog entry missing namespace")
|
|
1049
|
+
if namespace != target_namespace:
|
|
1050
|
+
redirect_namespaces.append(namespace)
|
|
1051
|
+
redirect_namespaces.sort()
|
|
1052
|
+
recreates_default = target_namespace == ACTIVE_NAMESPACE and len(redirect_namespaces) == 0
|
|
1053
|
+
if recreates_default:
|
|
1054
|
+
redirect_namespaces.append("default")
|
|
1022
1055
|
return {
|
|
1023
1056
|
"target_namespace": target_namespace,
|
|
1024
1057
|
"target_exists": target_exists,
|
|
1025
1058
|
"target_requires_password": target_requires_password,
|
|
1026
1059
|
"is_current_namespace": target_namespace == ACTIVE_NAMESPACE,
|
|
1060
|
+
"redirect_namespaces": redirect_namespaces,
|
|
1061
|
+
"recreates_default": recreates_default,
|
|
1027
1062
|
}
|
|
1028
1063
|
|
|
1029
1064
|
|
|
@@ -1054,6 +1089,41 @@ def delete_active_namespace(
|
|
|
1054
1089
|
)
|
|
1055
1090
|
|
|
1056
1091
|
|
|
1092
|
+
@router.post("/namespaces/rename-current")
|
|
1093
|
+
@transactional_route
|
|
1094
|
+
def rename_active_namespace(
|
|
1095
|
+
payload: dict[str, object],
|
|
1096
|
+
token: Annotated[str, Depends(_require_auth)],
|
|
1097
|
+
):
|
|
1098
|
+
body = _require_body_object(payload)
|
|
1099
|
+
target_namespace = _require_string_field(body, "target_namespace")
|
|
1100
|
+
rename_capture = CapturedExceptionContext(
|
|
1101
|
+
RuntimeError,
|
|
1102
|
+
ValueError,
|
|
1103
|
+
TypeError,
|
|
1104
|
+
FileNotFoundError,
|
|
1105
|
+
)
|
|
1106
|
+
result = None
|
|
1107
|
+
with rename_capture:
|
|
1108
|
+
result = rename_current_namespace(
|
|
1109
|
+
environ=os.environ,
|
|
1110
|
+
current_namespace=ACTIVE_NAMESPACE,
|
|
1111
|
+
target_namespace=target_namespace,
|
|
1112
|
+
)
|
|
1113
|
+
if rename_capture.captured_exception is not None:
|
|
1114
|
+
exc = rename_capture.captured_exception
|
|
1115
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
1116
|
+
if result is None:
|
|
1117
|
+
raise RuntimeError("Namespace rename did not return a result")
|
|
1118
|
+
return {
|
|
1119
|
+
"source_namespace": result.source_namespace,
|
|
1120
|
+
"target_namespace": result.target_namespace,
|
|
1121
|
+
"redirect_url": result.redirect_url,
|
|
1122
|
+
"rename_job_id": result.rename_job_id,
|
|
1123
|
+
"message": result.message,
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
|
|
1057
1127
|
@router.get("/namespaces/delete-jobs/{job_id}")
|
|
1058
1128
|
def namespace_delete_job_status(
|
|
1059
1129
|
job_id: str,
|
|
@@ -1075,6 +1145,25 @@ def namespace_delete_job_status(
|
|
|
1075
1145
|
return job_record
|
|
1076
1146
|
|
|
1077
1147
|
|
|
1148
|
+
@router.get("/namespaces/rename-jobs/{job_id}")
|
|
1149
|
+
def namespace_rename_job_status(job_id: str):
|
|
1150
|
+
job_record_capture = CapturedExceptionContext(
|
|
1151
|
+
RuntimeError,
|
|
1152
|
+
TypeError,
|
|
1153
|
+
ValueError,
|
|
1154
|
+
FileNotFoundError,
|
|
1155
|
+
)
|
|
1156
|
+
job_record: dict[str, object] | None = None
|
|
1157
|
+
with job_record_capture:
|
|
1158
|
+
job_record = load_namespace_rename_job(job_id=job_id)
|
|
1159
|
+
if job_record_capture.captured_exception is not None:
|
|
1160
|
+
exc = job_record_capture.captured_exception
|
|
1161
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
1162
|
+
if job_record is None:
|
|
1163
|
+
raise HTTPException(status_code=404, detail=f"Namespace rename job not found: {job_id}")
|
|
1164
|
+
return job_record
|
|
1165
|
+
|
|
1166
|
+
|
|
1078
1167
|
@router.post("/hydrate", response_model=HydrationStatusResponse)
|
|
1079
1168
|
@transactional_route
|
|
1080
1169
|
def hydrate_cache(
|
|
@@ -717,6 +717,32 @@ def _required_existing_target_launch_profile(*, target_namespace: str) -> Backup
|
|
|
717
717
|
)
|
|
718
718
|
|
|
719
719
|
|
|
720
|
+
def _resolve_same_name_restore_launch_profile(
|
|
721
|
+
*,
|
|
722
|
+
backup_path: Path,
|
|
723
|
+
target_namespace: str,
|
|
724
|
+
target_exists: bool,
|
|
725
|
+
) -> BackupLaunchProfile:
|
|
726
|
+
if target_exists:
|
|
727
|
+
return _required_existing_target_launch_profile(
|
|
728
|
+
target_namespace=target_namespace,
|
|
729
|
+
)
|
|
730
|
+
restored_profile = read_backup_launch_profile(
|
|
731
|
+
backup_path,
|
|
732
|
+
expected_namespace=target_namespace,
|
|
733
|
+
)
|
|
734
|
+
suggested_profile = _suggest_import_launch_profile(
|
|
735
|
+
target_namespace=target_namespace,
|
|
736
|
+
restored_profile=restored_profile,
|
|
737
|
+
)
|
|
738
|
+
return BackupLaunchProfile(
|
|
739
|
+
namespace=target_namespace,
|
|
740
|
+
port=suggested_profile.port,
|
|
741
|
+
https_port=suggested_profile.https_port,
|
|
742
|
+
mcp_port=suggested_profile.mcp_port,
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
|
|
720
746
|
def _backup_profile_from_request(
|
|
721
747
|
*,
|
|
722
748
|
namespace: str,
|
|
@@ -920,22 +946,22 @@ def restore_backup_preflight(
|
|
|
920
946
|
target_exists = _restore_target_exists(target_namespace=target_namespace)
|
|
921
947
|
suggested_profile = None
|
|
922
948
|
port_conflicts: list[str] = []
|
|
923
|
-
if
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
949
|
+
if target_exists:
|
|
950
|
+
target_profile = _required_existing_target_launch_profile(
|
|
951
|
+
target_namespace=target_namespace,
|
|
952
|
+
)
|
|
953
|
+
suggested_profile = _response_profile_from_backup_profile(profile=target_profile)
|
|
954
|
+
assert suggested_profile is not None
|
|
955
|
+
else:
|
|
956
|
+
if restored_profile is not None:
|
|
931
957
|
port_conflicts = _collect_import_profile_port_conflicts(
|
|
932
958
|
target_namespace=target_namespace,
|
|
933
959
|
launch_profile=restored_profile,
|
|
934
960
|
)
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
961
|
+
suggested_profile = _suggest_import_launch_profile(
|
|
962
|
+
target_namespace=target_namespace,
|
|
963
|
+
restored_profile=restored_profile,
|
|
964
|
+
)
|
|
939
965
|
|
|
940
966
|
return BackupRestorePreflightResponse(
|
|
941
967
|
backup_namespace=backup_namespace,
|
|
@@ -983,10 +1009,22 @@ def restore_backup(
|
|
|
983
1009
|
backup_path = folder_directory / folder_filename
|
|
984
1010
|
if not backup_path.exists():
|
|
985
1011
|
raise HTTPException(status_code=404, detail=f"Backup not found: {folder_filename}")
|
|
1012
|
+
target_exists = _restore_target_exists(target_namespace=target_namespace)
|
|
1013
|
+
launch_profile = _resolve_same_name_restore_launch_profile(
|
|
1014
|
+
backup_path=backup_path,
|
|
1015
|
+
target_namespace=target_namespace,
|
|
1016
|
+
target_exists=target_exists,
|
|
1017
|
+
)
|
|
986
1018
|
if target_namespace == ACTIVE_NAMESPACE:
|
|
987
1019
|
maintenance_service.enter_maintenance("Restoring backup")
|
|
988
1020
|
try:
|
|
989
1021
|
restore_backup_to_paths(backup_path, target_database_path)
|
|
1022
|
+
save_namespace_launch_profile(
|
|
1023
|
+
namespace=target_namespace,
|
|
1024
|
+
port=launch_profile.port,
|
|
1025
|
+
https_port=launch_profile.https_port,
|
|
1026
|
+
mcp_port=launch_profile.mcp_port,
|
|
1027
|
+
)
|
|
990
1028
|
_reset_runtime_state_after_restore()
|
|
991
1029
|
finally:
|
|
992
1030
|
maintenance_service.exit_maintenance()
|
|
@@ -995,6 +1033,12 @@ def restore_backup(
|
|
|
995
1033
|
open_namespace_suggested = False
|
|
996
1034
|
else:
|
|
997
1035
|
restore_backup_to_paths(backup_path, target_database_path)
|
|
1036
|
+
save_namespace_launch_profile(
|
|
1037
|
+
namespace=target_namespace,
|
|
1038
|
+
port=launch_profile.port,
|
|
1039
|
+
https_port=launch_profile.https_port,
|
|
1040
|
+
mcp_port=launch_profile.mcp_port,
|
|
1041
|
+
)
|
|
998
1042
|
return BackupRestoreResponse(
|
|
999
1043
|
backup_id=payload.backup_id,
|
|
1000
1044
|
source=payload.source,
|
|
@@ -34,8 +34,10 @@ from app.security.encryption import set_encryption_required
|
|
|
34
34
|
from app.server_runtime import resolve_mcp_agent_public_origin
|
|
35
35
|
from app.server_runtime import resolve_https_redirect_url
|
|
36
36
|
from app.server_runtime import resolve_request_host_for_https_redirect
|
|
37
|
+
from app.server_runtime import validate_namespace
|
|
37
38
|
from app.services.exception_capture import CapturedExceptionContext
|
|
38
39
|
from app.services.namespace_deletion_jobs import load_namespace_deletion_job
|
|
40
|
+
from app.services.namespace_rename_jobs import load_namespace_rename_job
|
|
39
41
|
from app.services.namespace_switcher import build_namespace_catalog
|
|
40
42
|
from app.services.namespace_switcher import open_or_launch_namespace
|
|
41
43
|
from app.services.diagnostics import configure_process_diagnostics
|
|
@@ -435,36 +437,17 @@ async def maintenance_page(request: Request):
|
|
|
435
437
|
)
|
|
436
438
|
|
|
437
439
|
|
|
438
|
-
def _build_namespace_deleted_links(*, job_id: str,
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
raise RuntimeError("Namespace catalog entry must be an object")
|
|
450
|
-
namespace = entry["namespace"]
|
|
451
|
-
if not isinstance(namespace, str) or namespace == "":
|
|
452
|
-
raise RuntimeError("Namespace catalog entry missing namespace")
|
|
453
|
-
if namespace == deleted_namespace:
|
|
454
|
-
continue
|
|
455
|
-
query = urlencode({"job": job_id, "namespace": namespace})
|
|
456
|
-
link_meta = "Open namespace"
|
|
457
|
-
if namespace == ACTIVE_NAMESPACE:
|
|
458
|
-
link_meta = "Already running"
|
|
459
|
-
links.append(
|
|
460
|
-
{
|
|
461
|
-
"namespace": namespace,
|
|
462
|
-
"label": namespace,
|
|
463
|
-
"meta": link_meta,
|
|
464
|
-
"href": f"/namespace-deleted/open?{query}",
|
|
465
|
-
}
|
|
466
|
-
)
|
|
467
|
-
return links
|
|
440
|
+
def _build_namespace_deleted_links(*, job_id: str, redirect_namespace: str) -> list[dict[str, str]]:
|
|
441
|
+
normalized_redirect_namespace = validate_namespace(namespace=redirect_namespace)
|
|
442
|
+
query = urlencode({"job": job_id, "namespace": normalized_redirect_namespace})
|
|
443
|
+
return [
|
|
444
|
+
{
|
|
445
|
+
"namespace": normalized_redirect_namespace,
|
|
446
|
+
"label": normalized_redirect_namespace,
|
|
447
|
+
"meta": "Selected destination",
|
|
448
|
+
"href": f"/namespace-deleted/open?{query}",
|
|
449
|
+
}
|
|
450
|
+
]
|
|
468
451
|
|
|
469
452
|
|
|
470
453
|
def _resolve_catalog_profile(*, namespace: str) -> tuple[int, int | None, int]:
|
|
@@ -543,7 +526,7 @@ async def namespace_deleted_page(request: Request):
|
|
|
543
526
|
template = templates.get_template("namespace_deleted.html")
|
|
544
527
|
namespace_links = _build_namespace_deleted_links(
|
|
545
528
|
job_id=job_record["job_id"],
|
|
546
|
-
|
|
529
|
+
redirect_namespace=job_record["redirect_namespace"],
|
|
547
530
|
)
|
|
548
531
|
page_state = json.dumps(
|
|
549
532
|
{
|
|
@@ -551,6 +534,7 @@ async def namespace_deleted_page(request: Request):
|
|
|
551
534
|
"deletedNamespace": job_record["deleted_namespace"],
|
|
552
535
|
"initialStatus": job_record["status"],
|
|
553
536
|
"initialError": job_record["error"],
|
|
537
|
+
"redirectNamespace": job_record["redirect_namespace"],
|
|
554
538
|
}
|
|
555
539
|
)
|
|
556
540
|
return template.render(
|
|
@@ -588,7 +572,10 @@ async def namespace_deleted_open_page(request: Request):
|
|
|
588
572
|
return HTMLResponse(f"Namespace deletion job not found: {job_id}", status_code=404)
|
|
589
573
|
if job_record["status"] == "pending":
|
|
590
574
|
return HTMLResponse("Namespace deletion is still in progress", status_code=409)
|
|
591
|
-
if
|
|
575
|
+
if (
|
|
576
|
+
namespace == job_record["deleted_namespace"]
|
|
577
|
+
and namespace != job_record["redirect_namespace"]
|
|
578
|
+
):
|
|
592
579
|
return HTMLResponse("Deleted namespace is unavailable", status_code=400)
|
|
593
580
|
|
|
594
581
|
launch_capture = CapturedExceptionContext(
|
|
@@ -618,6 +605,75 @@ async def namespace_deleted_open_page(request: Request):
|
|
|
618
605
|
return response
|
|
619
606
|
|
|
620
607
|
|
|
608
|
+
@app.get("/namespace-renamed", response_class=HTMLResponse)
|
|
609
|
+
async def namespace_renamed_page(request: Request):
|
|
610
|
+
job_id = request.query_params.get("job")
|
|
611
|
+
if not isinstance(job_id, str) or job_id.strip() == "":
|
|
612
|
+
return HTMLResponse("Missing namespace rename job", status_code=400)
|
|
613
|
+
job_record_capture = CapturedExceptionContext(
|
|
614
|
+
RuntimeError,
|
|
615
|
+
TypeError,
|
|
616
|
+
ValueError,
|
|
617
|
+
FileNotFoundError,
|
|
618
|
+
)
|
|
619
|
+
job_record: dict[str, object] | None = None
|
|
620
|
+
with job_record_capture:
|
|
621
|
+
job_record = load_namespace_rename_job(job_id=job_id)
|
|
622
|
+
if job_record_capture.captured_exception is not None:
|
|
623
|
+
exc = job_record_capture.captured_exception
|
|
624
|
+
return HTMLResponse(str(exc), status_code=400)
|
|
625
|
+
if job_record is None:
|
|
626
|
+
return HTMLResponse(f"Namespace rename job not found: {job_id}", status_code=404)
|
|
627
|
+
|
|
628
|
+
page_state = json.dumps(
|
|
629
|
+
{
|
|
630
|
+
"jobId": job_record["job_id"],
|
|
631
|
+
"sourceNamespace": job_record["source_namespace"],
|
|
632
|
+
"targetNamespace": job_record["target_namespace"],
|
|
633
|
+
"initialStatus": job_record["status"],
|
|
634
|
+
"initialError": job_record["error"],
|
|
635
|
+
}
|
|
636
|
+
)
|
|
637
|
+
template = templates.get_template("namespace_renamed.html")
|
|
638
|
+
return template.render(
|
|
639
|
+
request=request,
|
|
640
|
+
version=VERSION,
|
|
641
|
+
asset_version=ASSET_VERSION,
|
|
642
|
+
page_title="MetaList - Namespace Renamed",
|
|
643
|
+
source_namespace=job_record["source_namespace"],
|
|
644
|
+
target_namespace=job_record["target_namespace"],
|
|
645
|
+
page_state_json=page_state,
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
@app.get("/namespace-renamed/open")
|
|
650
|
+
async def namespace_renamed_open_page(request: Request):
|
|
651
|
+
job_id = request.query_params.get("job")
|
|
652
|
+
if not isinstance(job_id, str) or job_id.strip() == "":
|
|
653
|
+
return HTMLResponse("Missing namespace rename job", status_code=400)
|
|
654
|
+
job_record_capture = CapturedExceptionContext(
|
|
655
|
+
RuntimeError,
|
|
656
|
+
TypeError,
|
|
657
|
+
ValueError,
|
|
658
|
+
FileNotFoundError,
|
|
659
|
+
)
|
|
660
|
+
job_record: dict[str, object] | None = None
|
|
661
|
+
with job_record_capture:
|
|
662
|
+
job_record = load_namespace_rename_job(job_id=job_id)
|
|
663
|
+
if job_record_capture.captured_exception is not None:
|
|
664
|
+
exc = job_record_capture.captured_exception
|
|
665
|
+
return HTMLResponse(str(exc), status_code=400)
|
|
666
|
+
if job_record is None:
|
|
667
|
+
return HTMLResponse(f"Namespace rename job not found: {job_id}", status_code=404)
|
|
668
|
+
if job_record["status"] != "succeeded":
|
|
669
|
+
return HTMLResponse("Namespace rename is not complete", status_code=409)
|
|
670
|
+
if ACTIVE_NAMESPACE != job_record["target_namespace"]:
|
|
671
|
+
return HTMLResponse("Renamed namespace is not running here", status_code=409)
|
|
672
|
+
response = RedirectResponse(url="/?force_reauth=1", status_code=307)
|
|
673
|
+
clear_auth_cookie(response=response)
|
|
674
|
+
return response
|
|
675
|
+
|
|
676
|
+
|
|
621
677
|
@app.get("/locked", response_class=HTMLResponse)
|
|
622
678
|
async def locked_page(request: Request, db: Annotated[SafeSession, Depends(get_db)]):
|
|
623
679
|
"""Render a dedicated locked screen when a session is invalidated."""
|
|
@@ -32,6 +32,7 @@ _DEFAULT_KEY_PATH = _DEFAULT_DATABASE_DIRECTORY / "certs" / "metalist-key.pem"
|
|
|
32
32
|
_DEFAULT_RUNTIME_DIRECTORY = Path(tempfile.gettempdir()) / "metalist-runtime"
|
|
33
33
|
_DEFAULT_NAMESPACES_DIRECTORY_NAME = "namespaces"
|
|
34
34
|
_DEFAULT_NAMESPACE_DELETE_JOBS_DIRECTORY_NAME = "namespace-delete-jobs"
|
|
35
|
+
_DEFAULT_NAMESPACE_RENAME_JOBS_DIRECTORY_NAME = "namespace-rename-jobs"
|
|
35
36
|
_DEFAULT_LOGS_DIRECTORY_NAME = "logs"
|
|
36
37
|
_DEFAULT_NAMESPACE = "default"
|
|
37
38
|
_DEFAULT_HTTP_PORT = 8000
|
|
@@ -126,6 +127,10 @@ def resolve_namespace_delete_jobs_directory() -> Path:
|
|
|
126
127
|
return _DEFAULT_RUNTIME_DIRECTORY / _DEFAULT_NAMESPACE_DELETE_JOBS_DIRECTORY_NAME
|
|
127
128
|
|
|
128
129
|
|
|
130
|
+
def resolve_namespace_rename_jobs_directory() -> Path:
|
|
131
|
+
return _DEFAULT_RUNTIME_DIRECTORY / _DEFAULT_NAMESPACE_RENAME_JOBS_DIRECTORY_NAME
|
|
132
|
+
|
|
133
|
+
|
|
129
134
|
def resolve_runtime_logs_directory() -> Path:
|
|
130
135
|
return _DEFAULT_DATABASE_DIRECTORY / _DEFAULT_LOGS_DIRECTORY_NAME
|
|
131
136
|
|
|
@@ -10,11 +10,13 @@ import time
|
|
|
10
10
|
import traceback
|
|
11
11
|
|
|
12
12
|
import app.server_runtime as server_runtime
|
|
13
|
+
from app.server_runtime import save_namespace_launch_profile
|
|
13
14
|
from app.server_runtime import resolve_namespace_directory
|
|
14
15
|
from app.server_runtime import validate_namespace
|
|
15
16
|
from app.services.exception_capture import CapturedExceptionContext
|
|
16
17
|
from app.services.namespace_deletion_jobs import mark_namespace_deletion_job_failed
|
|
17
18
|
from app.services.namespace_deletion_jobs import mark_namespace_deletion_job_succeeded
|
|
19
|
+
from app.services.namespace_switcher import open_or_launch_namespace
|
|
18
20
|
from app.services.windows_process_control import is_process_running as is_windows_process_running
|
|
19
21
|
from app.services.windows_process_control import stop_process as stop_windows_process
|
|
20
22
|
|
|
@@ -30,6 +32,11 @@ def _parse_args() -> argparse.Namespace:
|
|
|
30
32
|
parser.add_argument("--pid", type=int, required=True)
|
|
31
33
|
parser.add_argument("--namespace", required=True)
|
|
32
34
|
parser.add_argument("--job-id", required=True)
|
|
35
|
+
parser.add_argument("--replacement-namespace", required=True)
|
|
36
|
+
parser.add_argument("--replacement-port", type=int, required=True)
|
|
37
|
+
parser.add_argument("--replacement-https-port", type=int, required=True)
|
|
38
|
+
parser.add_argument("--replacement-mcp-port", type=int, required=True)
|
|
39
|
+
parser.add_argument("--recreate-default", action="store_true")
|
|
33
40
|
return parser.parse_args()
|
|
34
41
|
|
|
35
42
|
|
|
@@ -123,16 +130,38 @@ def _delete_namespace_directory(*, namespace: str) -> None:
|
|
|
123
130
|
shutil.rmtree(namespace_directory)
|
|
124
131
|
|
|
125
132
|
|
|
133
|
+
def _recreate_default_namespace(*, args: argparse.Namespace) -> None:
|
|
134
|
+
replacement_namespace = validate_namespace(namespace=args.replacement_namespace)
|
|
135
|
+
if replacement_namespace != server_runtime._DEFAULT_NAMESPACE:
|
|
136
|
+
raise RuntimeError("Final namespace deletion must recreate default")
|
|
137
|
+
https_port = args.replacement_https_port
|
|
138
|
+
if https_port == 0:
|
|
139
|
+
https_port = None
|
|
140
|
+
save_namespace_launch_profile(
|
|
141
|
+
namespace=replacement_namespace,
|
|
142
|
+
port=args.replacement_port,
|
|
143
|
+
https_port=https_port,
|
|
144
|
+
mcp_port=args.replacement_mcp_port,
|
|
145
|
+
)
|
|
146
|
+
open_or_launch_namespace(
|
|
147
|
+
environ=os.environ,
|
|
148
|
+
current_namespace=None,
|
|
149
|
+
namespace=replacement_namespace,
|
|
150
|
+
port=args.replacement_port,
|
|
151
|
+
https_port=https_port,
|
|
152
|
+
mcp_port=args.replacement_mcp_port,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
126
156
|
def main() -> None:
|
|
127
157
|
args = _parse_args()
|
|
128
158
|
main_capture = CapturedExceptionContext(Exception)
|
|
129
159
|
with main_capture:
|
|
130
160
|
normalized_namespace = validate_namespace(namespace=args.namespace)
|
|
131
|
-
if normalized_namespace == server_runtime._DEFAULT_NAMESPACE:
|
|
132
|
-
raise RuntimeError("Default namespace cannot be deleted")
|
|
133
|
-
|
|
134
161
|
_stop_process(pid=args.pid)
|
|
135
162
|
_delete_namespace_directory(namespace=normalized_namespace)
|
|
163
|
+
if args.recreate_default:
|
|
164
|
+
_recreate_default_namespace(args=args)
|
|
136
165
|
mark_namespace_deletion_job_succeeded(job_id=args.job_id)
|
|
137
166
|
if main_capture.captured_exception is not None:
|
|
138
167
|
mark_namespace_deletion_job_failed(
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import re
|
|
6
|
+
import uuid
|
|
7
|
+
|
|
8
|
+
from app.server_runtime import resolve_namespace_rename_jobs_directory
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_JOB_ID_PATTERN = re.compile(r"^[a-f0-9-]{36}$")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def create_namespace_rename_job(
|
|
15
|
+
*,
|
|
16
|
+
source_namespace: str,
|
|
17
|
+
target_namespace: str,
|
|
18
|
+
) -> dict[str, str]:
|
|
19
|
+
if not isinstance(source_namespace, str) or source_namespace == "":
|
|
20
|
+
raise TypeError("source_namespace must be a non-empty string")
|
|
21
|
+
if not isinstance(target_namespace, str) or target_namespace == "":
|
|
22
|
+
raise TypeError("target_namespace must be a non-empty string")
|
|
23
|
+
|
|
24
|
+
job_id = str(uuid.uuid4())
|
|
25
|
+
job_record = {
|
|
26
|
+
"job_id": job_id,
|
|
27
|
+
"status": "pending",
|
|
28
|
+
"source_namespace": source_namespace,
|
|
29
|
+
"target_namespace": target_namespace,
|
|
30
|
+
"error": "",
|
|
31
|
+
}
|
|
32
|
+
_write_job_record(job_id=job_id, job_record=job_record)
|
|
33
|
+
return job_record
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def load_namespace_rename_job(*, job_id: str) -> dict[str, str] | None:
|
|
37
|
+
normalized_job_id = _validate_job_id(job_id=job_id)
|
|
38
|
+
job_path = _resolve_job_path(job_id=normalized_job_id)
|
|
39
|
+
if not job_path.is_file():
|
|
40
|
+
return None
|
|
41
|
+
payload = json.loads(job_path.read_text(encoding="utf-8"))
|
|
42
|
+
if not isinstance(payload, dict):
|
|
43
|
+
raise RuntimeError(f"Namespace rename job is not an object: {job_path}")
|
|
44
|
+
return _coerce_job_record(payload=payload, job_id=normalized_job_id)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def mark_namespace_rename_job_succeeded(*, job_id: str) -> None:
|
|
48
|
+
job_record = _require_existing_job(job_id=job_id)
|
|
49
|
+
job_record["status"] = "succeeded"
|
|
50
|
+
job_record["error"] = ""
|
|
51
|
+
_write_job_record(job_id=job_id, job_record=job_record)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def mark_namespace_rename_job_failed(*, job_id: str, error: str) -> None:
|
|
55
|
+
if not isinstance(error, str) or error.strip() == "":
|
|
56
|
+
raise TypeError("error must be a non-empty string")
|
|
57
|
+
job_record = _require_existing_job(job_id=job_id)
|
|
58
|
+
job_record["status"] = "failed"
|
|
59
|
+
job_record["error"] = error.strip()
|
|
60
|
+
_write_job_record(job_id=job_id, job_record=job_record)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _require_existing_job(*, job_id: str) -> dict[str, str]:
|
|
64
|
+
job_record = load_namespace_rename_job(job_id=job_id)
|
|
65
|
+
if job_record is None:
|
|
66
|
+
raise RuntimeError(f"Namespace rename job not found: {job_id}")
|
|
67
|
+
return job_record
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _validate_job_id(*, job_id: str) -> str:
|
|
71
|
+
if not isinstance(job_id, str):
|
|
72
|
+
raise TypeError(f"job_id must be a string, got {type(job_id)}")
|
|
73
|
+
normalized_job_id = job_id.strip()
|
|
74
|
+
if _JOB_ID_PATTERN.fullmatch(normalized_job_id) is None:
|
|
75
|
+
raise RuntimeError(f"Invalid namespace rename job id: {job_id!r}")
|
|
76
|
+
return normalized_job_id
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _resolve_job_path(*, job_id: str) -> Path:
|
|
80
|
+
jobs_directory = resolve_namespace_rename_jobs_directory()
|
|
81
|
+
jobs_directory.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
return jobs_directory / f"{job_id}.json"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _coerce_job_record(*, payload: dict[str, object], job_id: str) -> dict[str, str]:
|
|
86
|
+
required_fields = ("status", "source_namespace", "target_namespace", "error")
|
|
87
|
+
for field_name in required_fields:
|
|
88
|
+
if field_name not in payload:
|
|
89
|
+
raise RuntimeError(f"Namespace rename job is missing {field_name}: {payload}")
|
|
90
|
+
status = payload["status"]
|
|
91
|
+
source_namespace = payload["source_namespace"]
|
|
92
|
+
target_namespace = payload["target_namespace"]
|
|
93
|
+
error = payload["error"]
|
|
94
|
+
if status not in {"pending", "succeeded", "failed"}:
|
|
95
|
+
raise RuntimeError(f"Namespace rename job has invalid status: {payload}")
|
|
96
|
+
if not isinstance(source_namespace, str) or source_namespace == "":
|
|
97
|
+
raise RuntimeError(f"Namespace rename job is missing source_namespace: {payload}")
|
|
98
|
+
if not isinstance(target_namespace, str) or target_namespace == "":
|
|
99
|
+
raise RuntimeError(f"Namespace rename job is missing target_namespace: {payload}")
|
|
100
|
+
if not isinstance(error, str):
|
|
101
|
+
raise RuntimeError(f"Namespace rename job has invalid error field: {payload}")
|
|
102
|
+
return {
|
|
103
|
+
"job_id": job_id,
|
|
104
|
+
"status": status,
|
|
105
|
+
"source_namespace": source_namespace,
|
|
106
|
+
"target_namespace": target_namespace,
|
|
107
|
+
"error": error,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _write_job_record(*, job_id: str, job_record: dict[str, str]) -> None:
|
|
112
|
+
normalized_job_id = _validate_job_id(job_id=job_id)
|
|
113
|
+
serialized = json.dumps(job_record, indent=2, sort_keys=True)
|
|
114
|
+
job_path = _resolve_job_path(job_id=normalized_job_id)
|
|
115
|
+
job_path.write_text(serialized, encoding="utf-8")
|