Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.250.107"
VERSION = "0.250.108"
IS_DEVELOPMENT = is_development_env_enabled()

SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax')
Expand Down
63 changes: 48 additions & 15 deletions application/single_app/functions_data_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@
DATA_MANAGEMENT_MIGRATION_MODE_DELTA_UPSERT,
DATA_MANAGEMENT_MIGRATION_MODE_MIRROR,
}
DATA_MANAGEMENT_MIRROR_CONFIRMATION = "MIRROR WITH DELETIONS"
DATA_MANAGEMENT_MIRROR_CONFIRMATION = "MAKE DESTINATION MATCH SOURCE"
DATA_MANAGEMENT_RESTORE_POLICY_CREATE_ONLY = "create_only"
DATA_MANAGEMENT_RESTORE_POLICY_OVERWRITE = "overwrite_existing"
DATA_MANAGEMENT_RESTORE_POLICIES = {
Expand Down Expand Up @@ -1107,29 +1107,62 @@
normalized_settings,
normalized_plan,
)
capacity = None
if normalized_settings.get("migration_temporary_destination_ru_enabled"):
inspected_capacity = _inspect_target_cosmos_migration_capacity(
normalized_settings,
normalized_plan,
)
capacity = {
"target_ru": inspected_capacity.get("target_ru"),
"database_mode": inspected_capacity.get("database_mode"),
"database_current_ru": inspected_capacity.get("database_current_ru"),
"targets": inspected_capacity.get("targets"),
}
result = {
"success": True,
"target": "cosmos",
"database_name": properties.get("id") or DATA_MANAGEMENT_TARGET_COSMOS_DATABASE_NAME,
"authentication_type": normalized_settings.get("target_cosmos_authentication_type"),
"migration_access": migration_access,
"capacity": capacity,
}
return result


def test_target_cosmos_capacity_management(settings=None, migration_plan=None):

Check warning on line 1120 in application/single_app/functions_data_management.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
"""Validate destination Cosmos ARM throughput permissions for RU Boost."""

Check warning on line 1121 in application/single_app/functions_data_management.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.

Check warning on line 1121 in application/single_app/functions_data_management.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
normalized_settings = _normalize_data_management_settings_from_payload(settings)
normalized_plan = normalize_data_management_migration_plan({
"migration_plan": migration_plan if isinstance(migration_plan, dict) else {},
})
inspected_capacity = _inspect_target_cosmos_migration_capacity(

Check warning on line 1126 in application/single_app/functions_data_management.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
normalized_settings,
normalized_plan,
)
write_results = []
for target in inspected_capacity.get("targets") or []:
current_ru = _safe_int(target.get("current_ru"), default=0, minimum=0)
if not current_ru:
raise DataManagementSettingsValidationError(
"Destination Cosmos RU Boost test could not determine the current RU/s value."

Check warning on line 1135 in application/single_app/functions_data_management.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
)
scale_result = set_database_throughput(
inspected_capacity["management_settings"],
current_ru,
initiated_by="data_management_ru_boost_test",
reason="validate_data_management_ru_boost_permissions",

Check warning on line 1141 in application/single_app/functions_data_management.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
decision={
"scope": target.get("scope"),
"container_name": target.get("container_name") or "",
"target_mode": target.get("mode"),
},
)
write_results.append({
"scope": target.get("scope"),
"container_name": target.get("container_name") or "",
"mode": target.get("mode"),
"current_ru": current_ru,
"write_verified": True,
"verified_ru": scale_result.get("to_ru", current_ru),
})
return {
"success": True,
"target": "cosmos_ru_boost",

Check warning on line 1158 in application/single_app/functions_data_management.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
"target_ru": inspected_capacity.get("target_ru"),
"database_mode": inspected_capacity.get("database_mode"),
"database_current_ru": inspected_capacity.get("database_current_ru"),
"targets": write_results,
}


def test_target_search_connection(settings=None):
normalized_settings = _normalize_data_management_settings_from_payload(settings)
endpoint = _safe_text(normalized_settings.get("target_ai_search_endpoint"))
Expand Down Expand Up @@ -1786,7 +1819,7 @@
source_cutoff_at = _safe_text(candidate_state.get("source_cutoff_at"))
if _parse_iso_datetime(source_cutoff_at) is None:
raise DataManagementSettingsValidationError(
"Incremental migration baseline does not contain a valid source watermark."
"Previous migration does not contain a valid source checkpoint."
)
return source_cutoff_at

Expand Down
52 changes: 52 additions & 0 deletions application/single_app/route_backend_data_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
summarize_data_management_migration_plan,
submit_data_management_job,
test_backup_storage_connection,
test_target_cosmos_capacity_management,

Check warning on line 57 in application/single_app/route_backend_data_management.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
test_target_cosmos_connection,
test_target_enhanced_citation_storage_connection,
test_target_search_connection,
Expand Down Expand Up @@ -260,6 +261,38 @@
return jsonify({"success": False, "error": "Target Cosmos connection test failed."}), 400
return jsonify(result), 200

@bp.route("/api/admin/data-management/target/cosmos/ru-boost/test", methods=["POST"])

Check warning on line 264 in application/single_app/route_backend_data_management.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
@swagger_route(security=get_auth_security())

Check warning on line 265 in application/single_app/route_backend_data_management.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
@login_required
@admin_required
def test_admin_data_management_target_cosmos_ru_boost():
payload = request.get_json(silent=True) or {}
settings_payload = payload.get("settings") if isinstance(payload.get("settings"), dict) else None
migration_plan = payload.get("migration_plan") if isinstance(payload.get("migration_plan"), dict) else None
try:
result = test_target_cosmos_capacity_management(
settings=settings_payload,
migration_plan=migration_plan,
)
except DataManagementSettingsValidationError as exc:
log_event(
"[DataManagement] Target Cosmos RU Boost permission test validation failed.",
{"error": str(exc)},
level=logging.WARNING,
)
return jsonify({
"success": False,
"error": "Target Cosmos RU Boost permission test request is invalid.",
}), 400
except Exception as exc:
log_event(
"[DataManagement] Target Cosmos RU Boost permission test failed.",
{"error": str(exc)},
level=logging.WARNING,
)
return jsonify({"success": False, "error": "Target Cosmos RU Boost permission test failed."}), 400
return jsonify(result), 200

@bp.route("/api/admin/data-management/target/search/test", methods=["POST"])
@swagger_route(security=get_auth_security())
@login_required
Expand Down Expand Up @@ -736,6 +769,25 @@
return jsonify({"success": False, "error": str(exc)}), 400
return jsonify({"success": True, **catalog}), 200

@bp.route("/api/admin/data-management/restore/review", methods=["POST"])
@swagger_route(security=get_auth_security())
@login_required
@admin_required
def review_admin_data_management_restore():
payload = request.get_json(silent=True) or {}
restore_plan = payload.get("restore_plan") if isinstance(payload.get("restore_plan"), dict) else {}
try:
review = review_data_management_restore(restore_plan)
except Exception as exc:
log_event(
"[DataManagement] Restore review failed.",
{"error": str(exc)},
level=logging.ERROR,
exceptionTraceback=True,
)
return jsonify({"success": False, "error": "Restore review could not be completed."}), 400
return jsonify({"success": True, "review": review}), 200

@bp.route("/api/admin/data-management/migration/summary", methods=["POST"])
@swagger_route(security=get_auth_security())
@login_required
Expand Down
11 changes: 11 additions & 0 deletions application/single_app/static/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1732,6 +1732,17 @@ main {
border-top: 1px solid var(--bs-border-color);
}

.restore-stepper {
grid-template-columns: repeat(5, minmax(8rem, 1fr));
margin: 0;
border: 1px solid var(--bs-border-color);
border-radius: var(--bs-border-radius-lg);
}

.restore-stage {
min-height: 24rem;
}

@media (max-width: 991.98px) {
.migration-stepper {
grid-auto-flow: column;
Expand Down
65 changes: 52 additions & 13 deletions application/single_app/static/js/admin/admin_data_management.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const backupStorageAuthManagedIdentity = "managed_identity";
const backupStorageAuthConnectionString = "connection_string";
const targetCosmosDatabaseName = "SimpleChat";
const cosmosEditorConfirmationPhrase = "I understand this can damage system data";
const migrationMirrorConfirmationPhrase = "MIRROR WITH DELETIONS";
const migrationMirrorConfirmationPhrase = "MAKE DESTINATION MATCH SOURCE";
const restoreOverwriteConfirmationPhrase = "RESTORE WITH OVERWRITE";
const elements = {};
let dataManagementModified = false;
Expand Down Expand Up @@ -67,7 +67,6 @@ const migrationWorkflowState = {
submissionAccepted: false,
currentJob: null,
};

document.addEventListener("DOMContentLoaded", () => {
bindElements();
if (!elements.tabPane) {
Expand Down Expand Up @@ -152,6 +151,7 @@ function bindElements() {
"data_management_target_cosmos_subscription_id",
"data_management_target_cosmos_resource_group",
"data-management-test-target-cosmos-btn",
"data-management-test-target-cosmos-ru-boost-btn",
"data-management-target-ai-search-section",
"data_management_target_ai_search_auth",
"data_management_target_ai_search_endpoint",
Expand Down Expand Up @@ -330,6 +330,7 @@ function bindEvents() {
elements.dataManagementGenerateKeyBtn?.addEventListener("click", generateEncryptionKey);
elements.dataManagementTestStorageBtn?.addEventListener("click", testBackupStorage);
elements.dataManagementTestTargetCosmosBtn?.addEventListener("click", testTargetCosmos);
elements.dataManagementTestTargetCosmosRuBoostBtn?.addEventListener("click", testTargetCosmosRuBoost);
elements.dataManagementTestMigrationAccessBtn?.addEventListener("click", testMigrationAccess);
elements.dataManagementTestTargetSearchBtn?.addEventListener("click", testTargetSearch);
elements.dataManagementTestTargetEcStorageBtn?.addEventListener("click", testTargetEnhancedCitationStorage);
Expand Down Expand Up @@ -844,9 +845,16 @@ function setMigrationTargetVisibility() {
function updateMigrationCapacityVisibility() {
const enabled = Boolean(elements.datamanagementmigrationtemporarydestinationruenabled?.checked);
setElementVisible(elements.dataManagementMigrationTemporaryRuField, enabled);
if (elements.datamanagementmigrationtemporarydestinationru) {
elements.datamanagementmigrationtemporarydestinationru.disabled = !enabled;
}
[
elements.datamanagementmigrationtemporarydestinationru,
elements.datamanagementtargetcosmossubscriptionid,
elements.datamanagementtargetcosmosresourcegroup,
elements.dataManagementTestTargetCosmosRuBoostBtn,
].forEach((element) => {
if (element) {
element.disabled = !enabled;
}
});
}

function updateBackupCapacityVisibility() {
Expand Down Expand Up @@ -1544,8 +1552,7 @@ async function testTargetCosmos() {
});
const verifiedCount = Number(data.migration_access?.container_count || 0);
const accessText = verifiedCount ? ` Verified ${formatNumber(verifiedCount)} planned container(s).` : "";
const capacityText = data.capacity?.target_ru ? ` Temporary capacity can reach ${formatNumber(data.capacity.target_ru)} RU/s.` : "";
setStatus(`Target Cosmos connection succeeded. Database: ${data.database_name || targetCosmosDatabaseName}.${accessText}${capacityText}`, "success");
setStatus(`Target Cosmos data access succeeded. Database: ${data.database_name || targetCosmosDatabaseName}.${accessText}`, "success");
showToast("Target Cosmos connection succeeded.", "success");
} catch (error) {
setStatus(error.message || "Target Cosmos connection test failed.", "danger");
Expand All @@ -1555,6 +1562,24 @@ async function testTargetCosmos() {
}
}

async function testTargetCosmosRuBoost() {
setBusy(elements.dataManagementTestTargetCosmosRuBoostBtn, true, "Testing...");
try {
const data = await requestJson("/api/admin/data-management/target/cosmos/ru-boost/test", {
method: "POST",
body: JSON.stringify({ settings: collectSettings(), migration_plan: buildMigrationPlan() }),
});
const targetCount = Array.isArray(data.targets) ? data.targets.length : 0;
setStatus(`RU Boost permission test succeeded. Verified ${formatNumber(targetCount)} destination capacity target(s) up to ${formatNumber(data.target_ru || 0)} RU/s.`, "success");
showToast("RU Boost permission test succeeded.", "success");
} catch (error) {
setStatus(error.message || "RU Boost permission test failed.", "danger");
showToast(error.message || "RU Boost permission test failed.", "danger");
} finally {
setBusy(elements.dataManagementTestTargetCosmosRuBoostBtn, false);
}
}

async function testMigrationAccess() {
setBusy(elements.dataManagementTestMigrationAccessBtn, true, "Validating...");
try {
Expand All @@ -1563,8 +1588,7 @@ async function testMigrationAccess() {
body: JSON.stringify({ settings: collectSettings(), migration_plan: buildMigrationPlan() }),
});
const verifiedCount = Number(data.migration_access?.container_count || 0);
const capacityText = data.capacity?.target_ru ? ` Destination capacity management is ready up to ${formatNumber(data.capacity.target_ru)} RU/s.` : "";
setStatus(`Cosmos migration access validation succeeded. ${formatNumber(verifiedCount)} planned Cosmos container(s) can be read and written.${capacityText}`, "success");
setStatus(`Cosmos data-copy access validation succeeded. ${formatNumber(verifiedCount)} planned Cosmos container(s) can be read and written. Use Test RU Boost for destination capacity permissions.`, "success");
showToast("Cosmos migration access validation succeeded.", "success");
} catch (error) {
setStatus(error.message || "Cosmos migration access validation failed.", "danger");
Expand Down Expand Up @@ -1699,7 +1723,7 @@ async function queueMigration(dryRun) {
migrationPlan.migration_mode === "mirror_with_deletions" &&
migrationPlan.mirror_confirmation !== migrationMirrorConfirmationPhrase
) {
const message = `Type ${migrationMirrorConfirmationPhrase} before running a mirror migration.`;
const message = `Type ${migrationMirrorConfirmationPhrase} before running this destination cleanup.`;
elements.datamanagementmigrationmirrorconfirmationphrase?.classList.add("is-invalid");
elements.datamanagementmigrationmirrorconfirmationphrase?.focus();
setStatus(message, "danger");
Expand Down Expand Up @@ -2241,8 +2265,8 @@ function updateMigrationModeVisibility() {
elements.dataManagementMigrationMirrorConfirmation?.classList.toggle("d-none", !isMirror);
const descriptions = {
new_only: "Copies source items that are absent from the destination. Existing destination data is never updated or deleted.",
delta_upsert: "Copies new items and updates changed migration-owned items since the prior successful watermark. Destination-only data is retained.",
mirror_with_deletions: "Runs delta/upsert, then deletes destination-only items that carry successful SimpleChat migration ownership. Unowned data is retained.",
delta_upsert: "Copies new items and updates changed migration-owned items from the previous completed migration. Destination-only data is retained.",
mirror_with_deletions: "Catches up changed items, then removes destination-only items that were created by SimpleChat migration. Unowned data is retained.",
};
if (elements.dataManagementMigrationModeDescription) {
elements.dataManagementMigrationModeDescription.textContent = descriptions[migrationMode] || descriptions.new_only;
Expand Down Expand Up @@ -2398,6 +2422,13 @@ function createMigrationSummaryCard(targetType, targetSummary) {

function renderMigrationReviewChecks(checks) {
const container = elements.dataManagementMigrationReviewChecks;
if (!container) {
return;
}
renderDataManagementReviewChecks(container, checks);
}

function renderDataManagementReviewChecks(container, checks) {
if (!container) {
return;
}
Expand Down Expand Up @@ -3086,7 +3117,7 @@ function createBackupActionCell(backup) {
if (canRestore) {
const restoreButton = document.createElement("button");
restoreButton.type = "button";
restoreButton.className = "btn btn-outline-danger btn-sm ms-1";
restoreButton.className = "btn btn-outline-warning btn-sm ms-1";
restoreButton.append(createIcon("bi bi-arrow-counterclockwise me-1"), document.createTextNode("Restore"));
restoreButton.addEventListener("click", () => openRestoreModal(backup));
cell.appendChild(restoreButton);
Expand Down Expand Up @@ -4469,6 +4500,14 @@ function formatStatusLabel(value) {
}

function formatActivityLabel(value) {
const friendlyLabels = {
new_only: "Copy Missing Items Only",
delta_upsert: "Catch Up Changed Items",
mirror_with_deletions: "Make Destination Match Source",
};
if (friendlyLabels[value]) {
return friendlyLabels[value];
}
return String(value || "")
.replace(/[_-]/g, " ")
.replace(/\b\w/g, (letter) => letter.toUpperCase());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ function scrollToSection(sectionId) {
// Security tab sections
'keyvault-section': 'keyvault-section',
// Data Management tab sections
'data-management-readiness-section': 'data-management-readiness-section',
'data-management-backup-section': 'data-management-backup-section',
'data-management-schedule-section': 'data-management-schedule-section',
'data-management-storage-section': 'data-management-storage-section',
Expand Down
9 changes: 7 additions & 2 deletions application/single_app/templates/_sidebar_nav.html
Original file line number Diff line number Diff line change
Expand Up @@ -669,9 +669,14 @@
</li>
<li class="nav-item">
<a class="nav-link d-flex align-items-center admin-nav-tab" href="#" data-tab="data-management">
<i class="bi bi-database-check me-2"></i><span class="nav-text">Data Management</span>
<i class="bi bi-database-check me-2"></i><span class="nav-text">Backup, Migrate &amp; Restore</span>
</a>
<ul class="nav flex-column ms-3" style="display: none;" id="data-management-submenu">
<li class="nav-item">
<a class="nav-link d-flex align-items-center admin-nav-section" href="#" data-tab="data-management" data-section="data-management-readiness-section">
<i class="bi bi-compass me-2" style="font-size: 0.8em;"></i><span class="nav-text">Start Here</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link d-flex align-items-center admin-nav-section" href="#" data-tab="data-management" data-section="data-management-backup-section">
<i class="bi bi-archive me-2" style="font-size: 0.8em;"></i><span class="nav-text">Backup</span>
Expand Down Expand Up @@ -704,7 +709,7 @@
</li>
<li class="nav-item">
<a class="nav-link d-flex align-items-center admin-nav-section" href="#" data-tab="data-management" data-section="data-management-backup-inventory-section">
<i class="bi bi-box-seam me-2" style="font-size: 0.8em;"></i><span class="nav-text">Backup Inventory</span>
<i class="bi bi-box-seam me-2" style="font-size: 0.8em;"></i><span class="nav-text">Backup Inventory &amp; Restore</span>
</a>
</li>
<li class="nav-item">
Expand Down
Loading
Loading