Compare commits
2
Commits
67f55e5192
...
42f6d33efd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42f6d33efd | ||
|
|
4009af8dd3 |
@@ -1,6 +1,18 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once __DIR__ . '/../../connect.php';
|
require_once __DIR__ . '/../../connect.php';
|
||||||
|
|
||||||
|
// حارس الصلاحيات: هذه النقطة تحذف سجلاً نهائياً من قاعدة البيانات.
|
||||||
|
// connect.php يتحقق من صحة التوكن فقط، فبدون هذا الفحص كان أي توكن صالح
|
||||||
|
// (سائق أو راكب) قادراً على حذف السائقين.
|
||||||
|
if ($role !== 'super_admin') {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'failure',
|
||||||
|
'message' => 'Forbidden. Super Admin access required.',
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
$driver_id = filterRequest("driver_id");
|
$driver_id = filterRequest("driver_id");
|
||||||
$phone = filterRequest("phone");
|
$phone = filterRequest("phone");
|
||||||
$reason = filterRequest("reason"); // يمكن أن يأتي من البارامتر أو نخليه افتراضي
|
$reason = filterRequest("reason"); // يمكن أن يأتي من البارامتر أو نخليه افتراضي
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once __DIR__ . '/../../connect.php';
|
require_once __DIR__ . '/../../connect.php';
|
||||||
|
|
||||||
|
// حارس الصلاحيات: رفع الحظر عملية إدارية، وكانت هذه النقطة بلا أي فحص دور.
|
||||||
|
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'failure',
|
||||||
|
'message' => 'Forbidden. Admin access required.',
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
$phone = filterRequest("phone");
|
$phone = filterRequest("phone");
|
||||||
|
|
||||||
if (empty($phone)) {
|
if (empty($phone)) {
|
||||||
|
|||||||
@@ -109,6 +109,22 @@ try {
|
|||||||
$dispatchedPassengers = [];
|
$dispatchedPassengers = [];
|
||||||
$fcmErrors = [];
|
$fcmErrors = [];
|
||||||
|
|
||||||
|
// 5.5 وضع المعاينة: يُرجع ما ستفعله الحملة (النص، الكود، حجم الجمهور)
|
||||||
|
// دون إنشاء كود ترويجي ودون إرسال أي إشعار. الحملة تُنشئ خصماً حقيقياً
|
||||||
|
// وتصل كل ركاب الدولة، فوجود معاينة قبل الإطلاق ضروري.
|
||||||
|
if (filterRequest('dry_run') === '1') {
|
||||||
|
jsonSuccess([
|
||||||
|
'dry_run' => true,
|
||||||
|
'campaign_created' => false,
|
||||||
|
'promo_code' => $promoCode,
|
||||||
|
'discount_percent' => $discountVal,
|
||||||
|
'region' => $regionName,
|
||||||
|
'country_code' => strtoupper($countryCode),
|
||||||
|
'audience_size' => count($targets),
|
||||||
|
'ai_analysis' => $aiCampaign,
|
||||||
|
], 'Preview only — no promo code was created and no notification was sent.');
|
||||||
|
}
|
||||||
|
|
||||||
// 6. Save broadcast promo for this campaign (Option 1 - promos table adjustment)
|
// 6. Save broadcast promo for this campaign (Option 1 - promos table adjustment)
|
||||||
$sqlPromo = "INSERT INTO promos
|
$sqlPromo = "INSERT INTO promos
|
||||||
(promo_code, amount, description, passengerID, source, validity_start_date, validity_end_date)
|
(promo_code, amount, description, passengerID, source, validity_start_date, validity_end_date)
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once __DIR__ . '/../../connect.php';
|
require_once __DIR__ . '/../../connect.php';
|
||||||
|
|
||||||
|
// حارس الصلاحيات: رفع الحظر عملية إدارية، وكانت هذه النقطة بلا أي فحص دور.
|
||||||
|
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'failure',
|
||||||
|
'message' => 'Forbidden. Admin access required.',
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
function normalize_phone($s) { return preg_replace('/\D+/', '', (string)$s); }
|
function normalize_phone($s) { return preg_replace('/\D+/', '', (string)$s); }
|
||||||
|
|
||||||
$phone = filterRequest("phone");
|
$phone = filterRequest("phone");
|
||||||
|
|||||||
@@ -1,26 +1,53 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once __DIR__ . '/../connect.php';
|
require_once __DIR__ . '/../connect.php';
|
||||||
|
|
||||||
// Get 'id' and 'version' from the request
|
// رقم الإصدار يقرره العميل: كل تطبيقات سيرو تقارن نفسها به وتفرض التحديث،
|
||||||
$id = filterRequest("id");
|
// فتغييره يؤثر على كل المستخدمين. connect.php يتحقق من صحة التوكن فقط.
|
||||||
|
if ($role !== 'admin' && $role !== 'super_admin') {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'failure',
|
||||||
|
'message' => 'Forbidden. Admin access required.',
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = filterRequest("id");
|
||||||
$version = filterRequest("version");
|
$version = filterRequest("version");
|
||||||
|
|
||||||
|
if (empty($id) || empty($version)) {
|
||||||
|
jsonError("Both id and version are required.", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// شكل الإصدار: أرقام ونقاط فقط (مثل 1.2.3) — يمنع أي محتوى آخر.
|
||||||
|
if (!preg_match('/^\d+(\.\d+){0,3}$/', $version)) {
|
||||||
|
jsonError("Invalid version format. Use digits separated by dots, e.g. 1.4.2", 400);
|
||||||
|
}
|
||||||
|
|
||||||
$sql = "UPDATE `packageInfo` SET `version` ='$version' WHERE `id` = '$id'";
|
/**
|
||||||
|
* سابقاً كان الاستعلام يُبنى بدمج القيم مباشرة:
|
||||||
|
* "UPDATE packageInfo SET version ='$version' WHERE id = '$id'"
|
||||||
|
* وهو حقن SQL مباشر — أي مستخدم يملك توكناً صالحاً كان يستطيع تنفيذ ما يشاء
|
||||||
|
* على قاعدة البيانات عبر حقل الإصدار. الآن القيم مرتبطة كمعاملات.
|
||||||
|
*/
|
||||||
|
$stmt = $con->prepare("UPDATE `packageInfo` SET `version` = :version WHERE `id` = :id");
|
||||||
|
$stmt->execute([
|
||||||
|
':version' => $version,
|
||||||
|
':id' => $id,
|
||||||
|
]);
|
||||||
|
|
||||||
// Prepare and execute the statement
|
if (function_exists('logAudit')) {
|
||||||
$stmt = $con->prepare($sql);
|
try {
|
||||||
$stmt->execute();
|
logAudit($con, (string) ($user_id ?? 'unknown'), 'تحديث إصدار التطبيق', 'packageInfo', $id, [
|
||||||
error_log("Updating package: ID = $sql, Version = $version");
|
'version' => $version,
|
||||||
|
]);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
error_log("[updatePackages] audit failed: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Debugging: Check if the query affected any rows
|
|
||||||
if ($stmt->rowCount() > 0) {
|
if ($stmt->rowCount() > 0) {
|
||||||
// If rows were affected, print success
|
jsonSuccess(['id' => $id, 'version' => $version], "Package version updated successfully for ID $id");
|
||||||
echo json_encode(['status' => 'success', 'message' => "Package version updated successfully for ID $id"]);
|
|
||||||
} else {
|
} else {
|
||||||
// If no rows were affected, print failure and debug the query
|
jsonError("No package row was updated — check that ID $id exists and the version differs.", 404);
|
||||||
echo json_encode(['status' => 'failure', 'message' => "Failed to update package version. No rows affected. ID: $id, Version: $version"]);
|
|
||||||
}
|
}
|
||||||
?>
|
|
||||||
@@ -12,7 +12,10 @@
|
|||||||
<script src="https://unpkg.com/@phosphor-icons/web"></script>
|
<script src="https://unpkg.com/@phosphor-icons/web"></script>
|
||||||
|
|
||||||
<!-- Styles -->
|
<!-- Styles -->
|
||||||
<link rel="stylesheet" href="css/main.css">
|
<!-- ?v= must be bumped whenever css/main.css or js/app.js changes: the files
|
||||||
|
are served straight off a bind mount, so without it browsers keep
|
||||||
|
running the previously cached build after a deploy. -->
|
||||||
|
<link rel="stylesheet" href="css/main.css?v=2026-07-25-2">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
@@ -601,6 +604,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="js/app.js"></script>
|
<script src="js/app.js?v=2026-07-25-2"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -8,6 +8,11 @@
|
|||||||
(() => {
|
(() => {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
// Bump together with the ?v= query in index.html. Shown in the UI and in the
|
||||||
|
// diagnostics report so "the deploy did nothing" can be answered with a fact
|
||||||
|
// rather than a guess about caching.
|
||||||
|
const BUILD = '2026-07-25-2';
|
||||||
|
|
||||||
const SESSION_KEY = 'siro_admin_user';
|
const SESSION_KEY = 'siro_admin_user';
|
||||||
const FP_KEY = 'siro_web_fp';
|
const FP_KEY = 'siro_web_fp';
|
||||||
const API_BASE_KEY = 'siro_api_base';
|
const API_BASE_KEY = 'siro_api_base';
|
||||||
@@ -785,6 +790,7 @@
|
|||||||
['Token expires', expiresAt.toLocaleString()],
|
['Token expires', expiresAt.toLocaleString()],
|
||||||
['Device fingerprint', deviceFingerprint.slice(0, 24) + '…'],
|
['Device fingerprint', deviceFingerprint.slice(0, 24) + '…'],
|
||||||
['API endpoint', location.origin + API_BASE],
|
['API endpoint', location.origin + API_BASE],
|
||||||
|
['Console build', BUILD],
|
||||||
];
|
];
|
||||||
el.sessionInfo.innerHTML = rows.map(([k, v]) =>
|
el.sessionInfo.innerHTML = rows.map(([k, v]) =>
|
||||||
`<div class="kv-row"><span>${k}</span><strong>${esc(String(v))}</strong></div>`).join('');
|
`<div class="kv-row"><span>${k}</span><strong>${esc(String(v))}</strong></div>`).join('');
|
||||||
@@ -876,9 +882,9 @@
|
|||||||
panels: [{ title: 'Invoice totals', path: '/Admin/adminUser/invoice_total.php' }],
|
panels: [{ title: 'Invoice totals', path: '/Admin/adminUser/invoice_total.php' }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'quality', group: 'Quality', icon: 'ph-prohibit', title: 'Blacklist',
|
id: 'quality', superOnly: true, group: 'Quality', icon: 'ph-prohibit', title: 'Blacklist & Removal',
|
||||||
subtitle: 'Blocked captains and passengers',
|
subtitle: 'Blocked accounts, permanent removals and lifting a block',
|
||||||
panels: [{ title: 'Blacklist', path: '/Admin/v2/quality/blacklist_manager.php', params: { action_type: 'get_all' } }],
|
custom: renderBlacklist,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'scorecard', group: 'Quality', icon: 'ph-medal', title: 'Driver Scorecard',
|
id: 'scorecard', group: 'Quality', icon: 'ph-medal', title: 'Driver Scorecard',
|
||||||
@@ -913,6 +919,18 @@
|
|||||||
subtitle: 'Draft routes submitted by organisations, awaiting a decision',
|
subtitle: 'Draft routes submitted by organisations, awaiting a decision',
|
||||||
custom: renderRouteApprovals,
|
custom: renderRouteApprovals,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'campaigns', superOnly: true, group: 'Growth & Pricing', icon: 'ph-rocket-launch',
|
||||||
|
title: 'Campaign Launcher',
|
||||||
|
subtitle: 'Generate an AI pricing campaign, preview it, then dispatch',
|
||||||
|
custom: renderCampaigns,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'appVersion', superOnly: true, group: 'Administration', icon: 'ph-device-mobile',
|
||||||
|
title: 'App Versions',
|
||||||
|
subtitle: 'The version each Siro app checks itself against',
|
||||||
|
custom: renderAppVersions,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'broadcast', superOnly: true, group: 'Administration', icon: 'ph-megaphone-simple',
|
id: 'broadcast', superOnly: true, group: 'Administration', icon: 'ph-megaphone-simple',
|
||||||
title: 'Broadcast Notification',
|
title: 'Broadcast Notification',
|
||||||
@@ -1058,6 +1076,209 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Campaign launcher ────────────────────────────────────────────────────
|
||||||
|
// trigger_campaign.php asks Gemini for a campaign, writes a promo code valid
|
||||||
|
// for seven days, and pushes it to every passenger in the country. The
|
||||||
|
// preview (dry_run=1) runs the same analysis and stops before both.
|
||||||
|
function renderCampaigns(host) {
|
||||||
|
host.innerHTML = `
|
||||||
|
<div class="card notice-card notice-danger">
|
||||||
|
<i class="ph-fill ph-warning"></i>
|
||||||
|
<span><strong>Launching creates a real discount code and notifies every passenger in the selected country.</strong>
|
||||||
|
The promo stays valid for seven days. Always preview first.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header"><h3 class="card-title">Campaign parameters</h3></div>
|
||||||
|
<div class="tariff-grid">
|
||||||
|
<label class="tariff-field">
|
||||||
|
<span class="tariff-label">Country</span>
|
||||||
|
<select class="select-input" id="cmpCountry">
|
||||||
|
<option value="JO">Jordan</option>
|
||||||
|
<option value="SY">Syria</option>
|
||||||
|
<option value="EG">Egypt</option>
|
||||||
|
<option value="IQ">Iraq</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="tariff-field">
|
||||||
|
<span class="tariff-label">Region <em>defaults to the capital</em></span>
|
||||||
|
<input type="text" class="form-input" id="cmpRegion" placeholder="Amman">
|
||||||
|
</label>
|
||||||
|
<label class="tariff-field">
|
||||||
|
<span class="tariff-label">Siro base price</span>
|
||||||
|
<input type="number" step="any" class="form-input" id="cmpBasePrice" value="1.25">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="api-base-row" style="margin-top:1rem;">
|
||||||
|
<button class="btn btn-secondary btn-sm" id="cmpPreview"><i class="ph ph-eye"></i> <span>Preview</span></button>
|
||||||
|
<button class="btn btn-primary btn-sm" id="cmpLaunch" disabled><i class="ph ph-rocket-launch"></i> <span>Launch campaign</span></button>
|
||||||
|
<span class="stamp" id="cmpStatus">Preview first to enable launching.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" id="cmpResult">
|
||||||
|
<div class="table-msg">No analysis run yet.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" id="cmpLog"><div class="table-msg">Loading campaign history…</div></div>`;
|
||||||
|
|
||||||
|
$('cmpPreview').addEventListener('click', () => runCampaign(true));
|
||||||
|
$('cmpLaunch').addEventListener('click', () => runCampaign(false));
|
||||||
|
loadCampaignLog();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A launch is only allowed for parameters that were previewed, so an edit
|
||||||
|
// after previewing disarms the button again.
|
||||||
|
let previewedCampaign = null;
|
||||||
|
|
||||||
|
function campaignParams() {
|
||||||
|
return {
|
||||||
|
country_code: $('cmpCountry').value,
|
||||||
|
region_name: $('cmpRegion').value.trim(),
|
||||||
|
siro_base_price: $('cmpBasePrice').value.trim(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCampaign(isPreview) {
|
||||||
|
const params = campaignParams();
|
||||||
|
const signature = JSON.stringify(params);
|
||||||
|
|
||||||
|
if (!isPreview) {
|
||||||
|
if (signature !== previewedCampaign) {
|
||||||
|
toast('Parameters changed since the preview — preview again before launching.', 'warning');
|
||||||
|
$('cmpLaunch').disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!confirm(
|
||||||
|
`Launch this campaign in ${params.country_code}?\n\n` +
|
||||||
|
'It creates a discount code valid for 7 days and pushes a notification to every passenger there.\n\n' +
|
||||||
|
'This cannot be undone.'
|
||||||
|
)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const btn = isPreview ? $('cmpPreview') : $('cmpLaunch');
|
||||||
|
busy(btn, true, isPreview ? 'Analysing…' : 'Launching…');
|
||||||
|
$('cmpResult').innerHTML = '<div class="table-msg">Running market analysis…</div>';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = await api('/Admin/marketing/trigger_campaign.php', {
|
||||||
|
params: isPreview ? { ...params, dry_run: '1' } : params,
|
||||||
|
});
|
||||||
|
|
||||||
|
$('cmpResult').innerHTML = `<div class="card-header"><h3 class="card-title">${isPreview ? 'Preview' : 'Launch result'}</h3></div><div class="panel-body"></div>`;
|
||||||
|
renderPayload($('cmpResult').querySelector('.panel-body'), payload);
|
||||||
|
|
||||||
|
if (isPreview) {
|
||||||
|
const opportunity = payload?.ai_analysis?.opportunity_detected ?? payload?.campaign_created;
|
||||||
|
previewedCampaign = signature;
|
||||||
|
$('cmpLaunch').disabled = false;
|
||||||
|
$('cmpStatus').textContent = opportunity === false
|
||||||
|
? 'The AI found no opportunity — launching would still send.'
|
||||||
|
: `Previewed ${params.country_code}. Launch is now enabled.`;
|
||||||
|
} else {
|
||||||
|
previewedCampaign = null;
|
||||||
|
$('cmpLaunch').disabled = true;
|
||||||
|
$('cmpStatus').textContent = `Launched at ${new Date().toLocaleTimeString()}`;
|
||||||
|
toast('Campaign dispatched.', 'success');
|
||||||
|
loadCampaignLog();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (!handleApiError(err, 'campaign')) {
|
||||||
|
$('cmpResult').innerHTML = `<div class="table-msg is-error">${esc(err.message)}</div>`;
|
||||||
|
toast(err.message, 'danger');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
busy(btn, false, isPreview ? 'Preview' : 'Launch campaign');
|
||||||
|
// busy() clears `disabled`, so re-apply the arming rule afterwards:
|
||||||
|
// launching stays locked until the current parameters are previewed.
|
||||||
|
$('cmpLaunch').disabled = previewedCampaign !== JSON.stringify(campaignParams());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCampaignLog() {
|
||||||
|
const panel = $('cmpLog');
|
||||||
|
try {
|
||||||
|
const payload = await api('/Admin/marketing/get_campaigns_log.php');
|
||||||
|
panel.innerHTML = '<div class="card-header"><h3 class="card-title">Campaign history</h3></div><div class="panel-body"></div>';
|
||||||
|
renderPayload(panel.querySelector('.panel-body'), payload);
|
||||||
|
} catch (err) {
|
||||||
|
if (handleApiError(err, 'campaign-log')) return;
|
||||||
|
panel.innerHTML = `<div class="card-header"><h3 class="card-title">Campaign history</h3></div><div class="table-msg is-error">${esc(err.message)}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── App versions ─────────────────────────────────────────────────────────
|
||||||
|
async function renderAppVersions(host) {
|
||||||
|
host.innerHTML = '<div class="card"><div class="table-msg">Loading package versions…</div></div>';
|
||||||
|
|
||||||
|
let packages = [];
|
||||||
|
try {
|
||||||
|
packages = normaliseRows(await api('/serviceapp/getPackages.php'));
|
||||||
|
} catch (err) {
|
||||||
|
if (handleApiError(err, 'packages')) return;
|
||||||
|
host.innerHTML = `<div class="card"><div class="table-msg is-error">${esc(err.message)}</div></div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!packages.length) {
|
||||||
|
host.innerHTML = '<div class="card"><div class="table-msg">No package rows configured.</div></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
host.innerHTML = `
|
||||||
|
<div class="card notice-card">
|
||||||
|
<i class="ph-fill ph-info"></i>
|
||||||
|
<span>Each app compares its own build against this number on launch. Raising it can force every
|
||||||
|
user of that app to update before they can continue.</span>
|
||||||
|
</div>
|
||||||
|
${packages.map((pkg, index) => `
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">
|
||||||
|
${esc(pkg.name || pkg.packageName || pkg.app_name || `Package #${pkg.id}`)}
|
||||||
|
<span class="card-sub">row #${esc(pkg.id)}</span>
|
||||||
|
</h3>
|
||||||
|
<button class="btn btn-primary btn-sm" data-pkg-save="${index}"><i class="ph ph-floppy-disk"></i> <span>Update version</span></button>
|
||||||
|
</div>
|
||||||
|
<div class="tariff-grid">
|
||||||
|
<label class="tariff-field">
|
||||||
|
<span class="tariff-label">Current version</span>
|
||||||
|
<input type="text" class="form-input" data-pkg-version="${index}" value="${esc(pkg.version ?? '')}">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>`).join('')}`;
|
||||||
|
|
||||||
|
host.querySelectorAll('[data-pkg-save]').forEach((btn) =>
|
||||||
|
btn.addEventListener('click', () => saveVersion(packages[Number(btn.dataset.pkgSave)], Number(btn.dataset.pkgSave), host)));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveVersion(pkg, index, host) {
|
||||||
|
const input = host.querySelector(`[data-pkg-version="${index}"]`);
|
||||||
|
const version = input.value.trim();
|
||||||
|
|
||||||
|
if (version === String(pkg.version ?? '')) {
|
||||||
|
toast('Version unchanged.', 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Mirrors the server-side check so a typo is caught before the request.
|
||||||
|
if (!/^\d+(\.\d+){0,3}$/.test(version)) {
|
||||||
|
toast('Use digits separated by dots, e.g. 1.4.2', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!confirm(
|
||||||
|
`Set ${pkg.name || `package #${pkg.id}`} to version ${version} (was ${pkg.version ?? '—'})?\n\n` +
|
||||||
|
'Users on an older build may be prompted or forced to update.'
|
||||||
|
)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api('/serviceapp/updatePackages.php', { params: { id: pkg.id, version } });
|
||||||
|
toast(`Version set to ${version}.`, 'success');
|
||||||
|
renderAppVersions(host);
|
||||||
|
} catch (err) {
|
||||||
|
if (!handleApiError(err, 'package-save')) toast(err.message, 'danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Driver document review ───────────────────────────────────────────────
|
// ── Driver document review ───────────────────────────────────────────────
|
||||||
// The list is paged server-side (limit/offset). Activation posts
|
// The list is paged server-side (limit/offset). Activation posts
|
||||||
// status=active to Admin/driver/updateDriverFromAdmin.php, exactly as the
|
// status=active to Admin/driver/updateDriverFromAdmin.php, exactly as the
|
||||||
@@ -1846,6 +2067,7 @@
|
|||||||
|
|
||||||
const lines = [
|
const lines = [
|
||||||
`Siro Admin diagnostics — ${new Date().toISOString()}`,
|
`Siro Admin diagnostics — ${new Date().toISOString()}`,
|
||||||
|
`Console build: ${BUILD}`,
|
||||||
`Page origin : ${location.origin}`,
|
`Page origin : ${location.origin}`,
|
||||||
`API base : ${API_BASE}`,
|
`API base : ${API_BASE}`,
|
||||||
`Fingerprint : ${deviceFingerprint.slice(0, 20)}…`,
|
`Fingerprint : ${deviceFingerprint.slice(0, 20)}…`,
|
||||||
|
|||||||
Reference in New Issue
Block a user