Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eb19265586 |
@@ -77,7 +77,21 @@ jobs:
|
|||||||
sleep 3
|
sleep 3
|
||||||
gh release create kiosk-latest \
|
gh release create kiosk-latest \
|
||||||
--title "EverShelf Kiosk Latest" \
|
--title "EverShelf Kiosk Latest" \
|
||||||
--notes "Alias automatico → kiosk-${{ steps.version.outputs.name }} (versionCode ${{ steps.version.outputs.code }})" \
|
--notes "Auto alias → kiosk-${{ steps.version.outputs.name }} (versionCode ${{ steps.version.outputs.code }})" \
|
||||||
--prerelease \
|
--prerelease \
|
||||||
artifacts/evershelf-kiosk.apk
|
artifacts/evershelf-kiosk.apk
|
||||||
|
|
||||||
|
- name: Publish APK to releases/ for LAN OTA
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.WORKFLOW_PAT || secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
cp artifacts/evershelf-kiosk.apk releases/evershelf-kiosk.apk
|
||||||
|
printf '{"version":"%s","version_code":%s}\n' \
|
||||||
|
"${{ steps.version.outputs.name }}" "${{ steps.version.outputs.code }}" \
|
||||||
|
> releases/kiosk-version.json
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add releases/evershelf-kiosk.apk releases/kiosk-version.json
|
||||||
|
git diff --staged --quiet || git commit -m "chore(kiosk): publish APK v${{ steps.version.outputs.name }} for LAN OTA"
|
||||||
|
git push origin HEAD:${{ github.ref_name }}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,18 @@ jobs:
|
|||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Build Docker image
|
- name: Build Docker image
|
||||||
run: docker build -t evershelf-test .
|
run: |
|
||||||
|
set -e
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
echo "Docker build attempt $attempt/3..."
|
||||||
|
if docker build -t evershelf-test .; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Attempt $attempt failed — retrying in 20s..."
|
||||||
|
sleep 20
|
||||||
|
done
|
||||||
|
echo "Docker build failed after 3 attempts"
|
||||||
|
exit 1
|
||||||
|
|
||||||
- name: Test container starts
|
- name: Test container starts
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -55,6 +55,12 @@ if (($_GET['action'] ?? '') === 'ping') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Kiosk OTA metadata (LAN self-host; no DB required) ───────────────────────
|
||||||
|
if (($_GET['action'] ?? '') === 'kiosk_update') {
|
||||||
|
getKioskUpdate();
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
// ── App bootstrap — same-origin browsers receive API token automatically ───────
|
// ── App bootstrap — same-origin browsers receive API token automatically ───────
|
||||||
if (($_GET['action'] ?? '') === 'app_bootstrap') {
|
if (($_GET['action'] ?? '') === 'app_bootstrap') {
|
||||||
$required = evershelfApiTokenRequired();
|
$required = evershelfApiTokenRequired();
|
||||||
@@ -4923,6 +4929,34 @@ function getConsumptionPredictions(PDO $db): void {
|
|||||||
|
|
||||||
// ===== SETTINGS =====
|
// ===== SETTINGS =====
|
||||||
|
|
||||||
|
function getKioskUpdate(): void {
|
||||||
|
$root = dirname(__DIR__);
|
||||||
|
$jsonPath = $root . '/releases/kiosk-version.json';
|
||||||
|
$apkPath = $root . '/releases/evershelf-kiosk.apk';
|
||||||
|
if (!is_file($jsonPath) || !is_file($apkPath)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'not_available']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$meta = json_decode((string)file_get_contents($jsonPath), true);
|
||||||
|
if (!is_array($meta)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'invalid_metadata']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|
||||||
|
|| strtolower((string)($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')) === 'https'
|
||||||
|
? 'https' : 'http';
|
||||||
|
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||||
|
$script = $_SERVER['SCRIPT_NAME'] ?? '/api/index.php';
|
||||||
|
$basePath = preg_replace('#/api/index\.php$#', '', $script) ?: '';
|
||||||
|
$defaultApkUrl = $scheme . '://' . $host . $basePath . '/releases/evershelf-kiosk.apk';
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'version' => (string)($meta['version'] ?? ''),
|
||||||
|
'version_code' => (int)($meta['version_code'] ?? 0),
|
||||||
|
'apk_url' => (string)($meta['apk_url'] ?? $defaultApkUrl),
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
}
|
||||||
|
|
||||||
function getServerSettings(): void {
|
function getServerSettings(): void {
|
||||||
EverLog::debug('getServerSettings');
|
EverLog::debug('getServerSettings');
|
||||||
$geminiKey = env('GEMINI_API_KEY');
|
$geminiKey = env('GEMINI_API_KEY');
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId = "it.dadaloop.evershelf.kiosk"
|
applicationId = "it.dadaloop.evershelf.kiosk"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 19
|
versionCode = 20
|
||||||
versionName = "1.7.18"
|
versionName = "1.7.19"
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
|||||||
@@ -643,6 +643,79 @@ class KioskActivity : AppCompatActivity() {
|
|||||||
webView.evaluateJavascript("$jsCallback($escaped)", null)
|
webView.evaluateJavascript("$jsCallback($escaped)", null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val currentKiosk = try {
|
||||||
|
packageManager.getPackageInfo(packageName, 0).versionName ?: ""
|
||||||
|
} catch (_: Exception) { "" }
|
||||||
|
|
||||||
|
val installedVc: Long = try {
|
||||||
|
val pi = packageManager.getPackageInfo(packageName, 0)
|
||||||
|
if (Build.VERSION.SDK_INT >= 28) pi.longVersionCode
|
||||||
|
else @Suppress("DEPRECATION") pi.versionCode.toLong()
|
||||||
|
} catch (_: Exception) { -1L }
|
||||||
|
|
||||||
|
fun semverNewer(remote: String, local: String): Boolean {
|
||||||
|
val r = remote.split(".").map { it.filter(Char::isDigit).toIntOrNull() ?: 0 }
|
||||||
|
val l = local.split(".").map { it.filter(Char::isDigit).toIntOrNull() ?: 0 }
|
||||||
|
for (i in 0 until maxOf(r.size, l.size)) {
|
||||||
|
val rv = r.getOrElse(i) { 0 }
|
||||||
|
val lv = l.getOrElse(i) { 0 }
|
||||||
|
if (rv != lv) return rv > lv
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun needsUpdate(remoteVersion: String, remoteVc: Long): Boolean = when {
|
||||||
|
remoteVc > 0 && installedVc >= 0 -> remoteVc > installedVc
|
||||||
|
currentKiosk.isNotEmpty() && remoteVersion.matches(Regex("\\d+\\.\\d+.*")) ->
|
||||||
|
semverNewer(remoteVersion, currentKiosk)
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun applyUpdate(remoteVersion: String, apkUrl: String) {
|
||||||
|
val result = JSONObject()
|
||||||
|
.put("has_update", true)
|
||||||
|
.put("current", currentKiosk)
|
||||||
|
.put("latest", remoteVersion)
|
||||||
|
.put("apk_url", apkUrl)
|
||||||
|
notifyJs(result)
|
||||||
|
prefs.edit()
|
||||||
|
.putString(KEY_PENDING_UPDATE_VERSION, remoteVersion)
|
||||||
|
.putString(KEY_PENDING_UPDATE_URL, apkUrl)
|
||||||
|
.apply()
|
||||||
|
runOnUiThread { showNativeUpdateBanner("🔄 Kiosk $currentKiosk → $remoteVersion", apkUrl) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Prefer LAN/self-hosted update (no GitHub required)
|
||||||
|
val baseUrl = (prefs.getString(KEY_URL, "") ?: "").trim().trimEnd('/')
|
||||||
|
if (baseUrl.isNotEmpty()) {
|
||||||
|
try {
|
||||||
|
val localApi = "$baseUrl/api/index.php?action=kiosk_update"
|
||||||
|
val conn = openTrustedConnection(localApi)
|
||||||
|
conn.connectTimeout = 5000
|
||||||
|
conn.readTimeout = 5000
|
||||||
|
if (conn.responseCode == 200) {
|
||||||
|
val localJson = JSONObject(conn.inputStream.bufferedReader().readText())
|
||||||
|
conn.disconnect()
|
||||||
|
if (localJson.optBoolean("success")) {
|
||||||
|
val remoteVersion = localJson.optString("version", "")
|
||||||
|
val remoteVc = localJson.optLong("version_code", -1L)
|
||||||
|
val apkUrl = localJson.optString("apk_url", "")
|
||||||
|
if (apkUrl.isNotEmpty() && needsUpdate(remoteVersion, remoteVc)) {
|
||||||
|
applyUpdate(remoteVersion, apkUrl)
|
||||||
|
return@Thread
|
||||||
|
}
|
||||||
|
if (!needsUpdate(remoteVersion, remoteVc)) {
|
||||||
|
notifyJs(JSONObject().put("has_update", false).put("source", "local"))
|
||||||
|
prefs.edit().remove(KEY_PENDING_UPDATE_VERSION).remove(KEY_PENDING_UPDATE_URL).apply()
|
||||||
|
return@Thread
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else conn.disconnect()
|
||||||
|
} catch (_: Exception) { /* fall through to GitHub */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) GitHub release fallback (requires internet)
|
||||||
try {
|
try {
|
||||||
val conn = URL(GITHUB_RELEASES_API).openConnection() as java.net.HttpURLConnection
|
val conn = URL(GITHUB_RELEASES_API).openConnection() as java.net.HttpURLConnection
|
||||||
conn.setRequestProperty("Accept", "application/vnd.github+json")
|
conn.setRequestProperty("Accept", "application/vnd.github+json")
|
||||||
@@ -657,51 +730,16 @@ class KioskActivity : AppCompatActivity() {
|
|||||||
val body = conn.inputStream.bufferedReader().readText()
|
val body = conn.inputStream.bufferedReader().readText()
|
||||||
conn.disconnect()
|
conn.disconnect()
|
||||||
val json = JSONObject(body)
|
val json = JSONObject(body)
|
||||||
val latestTag = json.optString("tag_name", "")
|
|
||||||
if (latestTag.isEmpty()) {
|
|
||||||
notifyJs(JSONObject().put("has_update", false).put("error", "no tag"))
|
|
||||||
return@Thread
|
|
||||||
}
|
|
||||||
|
|
||||||
val currentKiosk = try {
|
|
||||||
packageManager.getPackageInfo(packageName, 0).versionName ?: ""
|
|
||||||
} catch (_: Exception) { "" }
|
|
||||||
|
|
||||||
val installedVc: Long = try {
|
|
||||||
val pi = packageManager.getPackageInfo(packageName, 0)
|
|
||||||
if (Build.VERSION.SDK_INT >= 28) pi.longVersionCode
|
|
||||||
else @Suppress("DEPRECATION") pi.versionCode.toLong()
|
|
||||||
} catch (_: Exception) { -1L }
|
|
||||||
|
|
||||||
// The kiosk-latest release uses a non-semver tag ("kiosk-latest").
|
|
||||||
// Extract the actual kiosk version from the release body text.
|
|
||||||
// Body format: "Alias automatico → kiosk-X.Y.Z (versionCode N)".
|
|
||||||
val bodyText = json.optString("body", "")
|
val bodyText = json.optString("body", "")
|
||||||
val norm = { v: String -> v.replace(Regex("^[^0-9]*"), "") }
|
val norm = { v: String -> v.replace(Regex("^[^0-9]*"), "") }
|
||||||
val remoteKioskVersion = Regex("""kiosk-v?(\d+\.\d+(?:\.\d+)?)""")
|
val remoteKioskVersion = Regex("""kiosk-v?(\d+\.\d+(?:\.\d+)?)""")
|
||||||
.find(bodyText)?.groupValues?.get(1)
|
.find(bodyText)?.groupValues?.get(1)
|
||||||
?.takeIf { it.isNotEmpty() }
|
?.takeIf { it.isNotEmpty() }
|
||||||
?: norm(latestTag)
|
?: norm(json.optString("tag_name", ""))
|
||||||
|
|
||||||
val remoteVc = Regex("""versionCode[=:\s(]+(\d+)""", RegexOption.IGNORE_CASE)
|
val remoteVc = Regex("""versionCode[=:\s(]+(\d+)""", RegexOption.IGNORE_CASE)
|
||||||
.find(bodyText)?.groupValues?.get(1)?.toLongOrNull() ?: -1L
|
.find(bodyText)?.groupValues?.get(1)?.toLongOrNull() ?: -1L
|
||||||
|
|
||||||
// Compare semver: returns true if `remote` is strictly greater than `local`
|
|
||||||
fun semverNewer(remote: String, local: String): Boolean {
|
|
||||||
val r = remote.split(".").map { it.filter(Char::isDigit).toIntOrNull() ?: 0 }
|
|
||||||
val l = local.split(".").map { it.filter(Char::isDigit).toIntOrNull() ?: 0 }
|
|
||||||
val len = maxOf(r.size, l.size)
|
|
||||||
for (i in 0 until len) {
|
|
||||||
val rv = r.getOrElse(i) { 0 }
|
|
||||||
val lv = l.getOrElse(i) { 0 }
|
|
||||||
if (rv != lv) return rv > lv
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val isSemver = remoteKioskVersion.matches(Regex("\\d+\\.\\d+.*"))
|
|
||||||
|
|
||||||
// Get APK URL from assets; fall back to the hardcoded KIOSK_DOWNLOAD_URL
|
|
||||||
val assets = json.optJSONArray("assets")
|
val assets = json.optJSONArray("assets")
|
||||||
var kioskApkUrl = ""
|
var kioskApkUrl = ""
|
||||||
if (assets != null) {
|
if (assets != null) {
|
||||||
@@ -715,39 +753,35 @@ class KioskActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
if (kioskApkUrl.isEmpty()) kioskApkUrl = KIOSK_DOWNLOAD_URL
|
if (kioskApkUrl.isEmpty()) kioskApkUrl = KIOSK_DOWNLOAD_URL
|
||||||
|
|
||||||
val kioskNeedsUpdate = when {
|
if (!needsUpdate(remoteKioskVersion, remoteVc)) {
|
||||||
remoteVc > 0 && installedVc >= 0 -> remoteVc > installedVc
|
notifyJs(JSONObject().put("has_update", false))
|
||||||
currentKiosk.isNotEmpty() && isSemver -> semverNewer(remoteKioskVersion, currentKiosk)
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
val result = JSONObject()
|
|
||||||
.put("has_update", kioskNeedsUpdate)
|
|
||||||
.put("current", currentKiosk)
|
|
||||||
.put("latest", remoteKioskVersion)
|
|
||||||
.put("apk_url", kioskApkUrl)
|
|
||||||
|
|
||||||
notifyJs(result)
|
|
||||||
|
|
||||||
if (!kioskNeedsUpdate) {
|
|
||||||
// Clear any stale pending update if the current version is now up to date
|
|
||||||
prefs.edit().remove(KEY_PENDING_UPDATE_VERSION).remove(KEY_PENDING_UPDATE_URL).apply()
|
prefs.edit().remove(KEY_PENDING_UPDATE_VERSION).remove(KEY_PENDING_UPDATE_URL).apply()
|
||||||
return@Thread
|
return@Thread
|
||||||
}
|
}
|
||||||
|
applyUpdate(remoteKioskVersion, kioskApkUrl)
|
||||||
// Persist the pending update so the banner reappears after a crash/restart
|
|
||||||
prefs.edit()
|
|
||||||
.putString(KEY_PENDING_UPDATE_VERSION, remoteKioskVersion)
|
|
||||||
.putString(KEY_PENDING_UPDATE_URL, kioskApkUrl)
|
|
||||||
.apply()
|
|
||||||
|
|
||||||
runOnUiThread { showNativeUpdateBanner("🔄 Kiosk $currentKiosk → $remoteKioskVersion", kioskApkUrl) }
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
notifyJs(JSONObject().put("has_update", false).put("error", e.message ?: "network error"))
|
notifyJs(JSONObject().put("has_update", false).put("error", e.message ?: "network error"))
|
||||||
}
|
}
|
||||||
}.start()
|
}.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** HTTPS with self-signed cert support (LAN servers). */
|
||||||
|
private fun openTrustedConnection(urlStr: String): java.net.HttpURLConnection {
|
||||||
|
val conn = URL(urlStr).openConnection()
|
||||||
|
if (conn is javax.net.ssl.HttpsURLConnection) {
|
||||||
|
val trustAll = arrayOf<javax.net.ssl.TrustManager>(object : javax.net.ssl.X509TrustManager {
|
||||||
|
override fun checkClientTrusted(c: Array<java.security.cert.X509Certificate>?, t: String?) {}
|
||||||
|
override fun checkServerTrusted(c: Array<java.security.cert.X509Certificate>?, t: String?) {}
|
||||||
|
override fun getAcceptedIssuers(): Array<java.security.cert.X509Certificate> = arrayOf()
|
||||||
|
})
|
||||||
|
val sc = javax.net.ssl.SSLContext.getInstance("TLS")
|
||||||
|
sc.init(null, trustAll, java.security.SecureRandom())
|
||||||
|
conn.sslSocketFactory = sc.socketFactory
|
||||||
|
conn.hostnameVerifier = javax.net.ssl.HostnameVerifier { _, _ -> true }
|
||||||
|
}
|
||||||
|
return conn as java.net.HttpURLConnection
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* On resume: if a previous session detected an available update and saved it to prefs,
|
* On resume: if a previous session detected an available update and saved it to prefs,
|
||||||
* restore the update banner immediately without a network round-trip.
|
* restore the update banner immediately without a network round-trip.
|
||||||
|
|||||||
@@ -540,6 +540,11 @@ class SetupActivity : AppCompatActivity() {
|
|||||||
// Cancel auto-discover when leaving server step
|
// Cancel auto-discover when leaving server step
|
||||||
if (step != 3) discoverCancelled.set(true)
|
if (step != 3) discoverCancelled.set(true)
|
||||||
|
|
||||||
|
// Auto-discover when entering server step (empty URL only)
|
||||||
|
if (step == 3 && urlEdit.text.toString().trim().isEmpty()) {
|
||||||
|
autoDiscover()
|
||||||
|
}
|
||||||
|
|
||||||
// Scroll to top
|
// Scroll to top
|
||||||
try { findViewById<ScrollView>(R.id.setupScrollView).scrollTo(0, 0) } catch (_: Exception) {}
|
try { findViewById<ScrollView>(R.id.setupScrollView).scrollTo(0, 0) } catch (_: Exception) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"version": "1.7.19",
|
||||||
|
"version_code": 20
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
/** Delete all comments on open feature/enhancement backlog issues (English-only tracker policy). */
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
define('CRON_MODE', true);
|
||||||
|
require_once __DIR__ . '/../api/bootstrap.php';
|
||||||
|
require_once __DIR__ . '/../api/lib/github.php';
|
||||||
|
require_once __DIR__ . '/../api/lib/constants.php';
|
||||||
|
|
||||||
|
$token = _ghToken();
|
||||||
|
if ($token === '') {
|
||||||
|
fwrite(STDERR, "ERROR: GH_ISSUE_TOKEN not configured\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ghRequest(string $token, string $method, string $url, ?array $body = null): array {
|
||||||
|
$ch = curl_init($url);
|
||||||
|
$headers = [
|
||||||
|
'Authorization: token ' . $token,
|
||||||
|
'Accept: application/vnd.github+json',
|
||||||
|
'X-GitHub-Api-Version: 2022-11-28',
|
||||||
|
'User-Agent: EverShelf-Triage/1.0',
|
||||||
|
];
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_HTTPHEADER => $headers,
|
||||||
|
CURLOPT_TIMEOUT => 30,
|
||||||
|
]);
|
||||||
|
if ($method === 'DELETE') {
|
||||||
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
|
||||||
|
} elseif ($method === 'GET') {
|
||||||
|
// default
|
||||||
|
}
|
||||||
|
if ($body !== null) {
|
||||||
|
$headers[] = 'Content-Type: application/json';
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
|
||||||
|
}
|
||||||
|
$raw = curl_exec($ch);
|
||||||
|
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
return ['code' => $code, 'body' => $raw];
|
||||||
|
}
|
||||||
|
|
||||||
|
$issues = [122, 121, 120, 119, 118, 117, 116, 115, 114, 106, 105, 104, 103, 102, 101, 97, 93, 81, 80, 79, 69, 67, 65];
|
||||||
|
$deleted = 0;
|
||||||
|
|
||||||
|
foreach ($issues as $num) {
|
||||||
|
$page = 1;
|
||||||
|
while (true) {
|
||||||
|
$url = 'https://api.github.com/repos/' . GH_REPO . "/issues/$num/comments?per_page=100&page=$page";
|
||||||
|
$r = ghRequest($token, 'GET', $url);
|
||||||
|
if ($r['code'] !== 200) {
|
||||||
|
fwrite(STDERR, "#$num list comments HTTP {$r['code']}\n");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$comments = json_decode($r['body'], true);
|
||||||
|
if (!is_array($comments) || empty($comments)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
foreach ($comments as $c) {
|
||||||
|
$id = (int)($c['id'] ?? 0);
|
||||||
|
if ($id <= 0) continue;
|
||||||
|
$dr = ghRequest($token, 'DELETE', 'https://api.github.com/repos/' . GH_REPO . "/issues/comments/$id");
|
||||||
|
if ($dr['code'] === 204) {
|
||||||
|
$deleted++;
|
||||||
|
echo "deleted comment $id on #$num\n";
|
||||||
|
} else {
|
||||||
|
fwrite(STDERR, "FAIL delete comment $id on #$num HTTP {$dr['code']}\n");
|
||||||
|
}
|
||||||
|
usleep(200000);
|
||||||
|
}
|
||||||
|
if (count($comments) < 100) break;
|
||||||
|
$page++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Done. Deleted $deleted comments.\n";
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
#!/usr/bin/env php
|
#!/usr/bin/env php
|
||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* One-shot triage: comment + close resolved auto-report bugs; reply on #200 (keep open).
|
* Triage resolved auto-report bugs only (English comments).
|
||||||
|
* Feature/enhancement backlog issues are never bulk-closed here.
|
||||||
* Usage: php scripts/triage-open-issues.php [--dry-run]
|
* Usage: php scripts/triage-open-issues.php [--dry-run]
|
||||||
*/
|
*/
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
@@ -75,66 +76,23 @@ function closeIssue(string $token, string $repo, int $num, bool $dryRun): bool {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── #200: reply only, keep OPEN ─────────────────────────────────────────────
|
|
||||||
$body200 = <<<'MD'
|
|
||||||
Ciao Marco, grazie per la segnalazione dettagliata.
|
|
||||||
|
|
||||||
Il messaggio **«Impossibile contattare il server»** compare quando il browser **non riesce a completare** la richiesta a `api/index.php?action=health_check`. Quindi phpinfo funziona, ma **l'endpoint API no** (404, redirect, TLS, path sbagliato, ecc.).
|
|
||||||
|
|
||||||
### Check rapidi (dalla macchina dove apri il browser)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -sv "https://TUO-DOMINIO/api/index.php?action=ping"
|
|
||||||
curl -sv "https://TUO-DOMINIO/api/index.php?action=health_check"
|
|
||||||
```
|
|
||||||
|
|
||||||
Se uno dei due fallisce: DevTools → **Network** → URL esatto e **status code** della richiesta `health_check`.
|
|
||||||
|
|
||||||
### Cause frequenti con Traefik + Docker Swarm
|
|
||||||
|
|
||||||
1. **Routing incompleto** — Traefik deve inoltrare `/` **e** `/api/*`, non solo la homepage.
|
|
||||||
2. **Redirect HTTPS** — dietro Traefik serve `X-Forwarded-Proto: https`, oppure disabilitare il redirect in `.htaccess`. Nelle immagini recenti il Dockerfile imposta `SetEnvIf X-Forwarded-Proto "https" HTTPS=on`.
|
|
||||||
3. **Sottopath** — EverShelf usa URL relativi (`api/index.php`); se l'app è su `/sottocartella/`, l'URL pubblico deve essere coerente.
|
|
||||||
4. **Volume `data/`** — al primo avvio può essere quasi vuoto; assicurati permessi scrivibili:
|
|
||||||
```bash
|
|
||||||
docker exec -it CONTAINER chown -R www-data:www-data /var/www/html/data
|
|
||||||
docker exec -it CONTAINER chmod -R 775 /var/www/html/data
|
|
||||||
```
|
|
||||||
5. **`API_TOKEN` in `.env`** — se impostato, compare un prompt token (non «server non raggiungibile»).
|
|
||||||
|
|
||||||
### Per il passo successivo
|
|
||||||
|
|
||||||
Puoi condividere:
|
|
||||||
- URL pubblico esatto (con path)
|
|
||||||
- Output dei due `curl` sopra
|
|
||||||
- Screenshot Network tab su `health_check`
|
|
||||||
- Labels Traefik del servizio (router + middlewares)
|
|
||||||
|
|
||||||
Resta aperta finché non confermi che `ping`/`health_check` rispondono — poi chiudiamo insieme.
|
|
||||||
MD;
|
|
||||||
|
|
||||||
commentIssue($token, $repo, 200, $body200, $dryRun);
|
|
||||||
|
|
||||||
// ── Resolved auto-report bugs ───────────────────────────────────────────────
|
|
||||||
$bugs = [
|
$bugs = [
|
||||||
198 => "Risolto in develop: `PRAGMA busy_timeout` portato a 10s e `dbWithRetry()` su `updateInventory` per ritentare su SQLITE_BUSY quando cron smart-shopping e PWA scrivono in parallelo.",
|
198 => 'Fixed in develop: `PRAGMA busy_timeout` raised to 10s and `dbWithRetry()` on `updateInventory` retries SQLITE_BUSY when cron and PWA write in parallel.',
|
||||||
199 => "Duplicato di #198 — stesso evento (`inventory_update` → database locked). Fix: retry + busy_timeout aumentato.",
|
199 => 'Duplicate of #198 — same event (`inventory_update` → database locked). Fix: retry + longer busy_timeout.',
|
||||||
196 => "Risolto in v1.7.38+: `saveProduct` intercetta `UNIQUE constraint failed: products.barcode`, fa merge sul prodotto esistente o risponde 409 JSON (`barcode_already_used`) invece di HTTP 500.",
|
196 => 'Fixed in v1.7.38+: `saveProduct` handles duplicate barcodes (merge or 409 JSON) instead of HTTP 500.',
|
||||||
197 => "Conseguenza lato PWA del crash PHP #196 — risolto con gestione barcode duplicato in `saveProduct`.",
|
197 => 'PWA side-effect of PHP crash #196 — fixed with duplicate barcode handling in `saveProduct`.',
|
||||||
195 => "Risolto: `EverLog::request()` ora riceve sempre stringhe — `\$method = (string)(\$_SERVER['REQUEST_METHOD'] ?? 'GET')` (fix CLI/cron che passavano null).",
|
195 => 'Fixed: `EverLog::request()` always receives strings — `(string)($_SERVER[\'REQUEST_METHOD\'] ?? \'GET\')`.',
|
||||||
193 => "Stesso root cause di #195 (fatal TypeError su `EverLog::request` con method null da CLI). Fix già in develop.",
|
193 => 'Same root cause as #195 (TypeError when method was null from CLI).',
|
||||||
194 => "Risolto: `_applySpesaScanUI` usava `currentPage` (inesistente) → corretto in `_currentPageId`.",
|
194 => 'Fixed: `_applySpesaScanUI` referenced `currentPage` → corrected to `_currentPageId`.',
|
||||||
192 => "Risolto: in `renderShoppingItems` la variabile `enriched` veniva referenziata prima della dichiarazione (TDZ). Ora `enrichedRaw` → `_dedupeShoppingByGeneric` → `enriched`.",
|
192 => 'Fixed: TDZ on `enriched` in `renderShoppingItems`.',
|
||||||
191 => "Risolto: in `_runStartupCheck` `setProgress` è dichiarata prima delle chiamate e `barEl` inizializzato prima dell'uso (niente più TDZ).",
|
191 => 'Fixed: TDZ on `setProgress` / `barEl` in `_runStartupCheck`.',
|
||||||
134 => "Segnalazione auto-report su volume Docker non scrivibile. Mitigazioni: `_ensureDataDir()`, `_ensureDbWritable()`, Dockerfile `chown www-data`. Su Swarm: `chown -R www-data:www-data data` al primo boot.",
|
134 => 'Auto-report for non-writable Docker volume. Mitigations: `_ensureDataDir()`, `_ensureDbWritable()`, Dockerfile chown.',
|
||||||
184 => "Correlato a #134: SQLite readonly quando `data/` o `evershelf.db` non sono scrivibili. Fix operativo + chmod WAL/SHM sidecar in `_ensureDbWritable()`.",
|
184 => 'Related to #134: SQLite readonly when `data/` is not writable.',
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($bugs as $num => $msg) {
|
foreach ($bugs as $num => $msg) {
|
||||||
commentIssue($token, $repo, $num, $msg . "\n\n_Chiuso dopo triage — fix in develop._", $dryRun);
|
commentIssue($token, $repo, $num, $msg . "\n\n_Closed after triage — fix shipped in develop._", $dryRun);
|
||||||
closeIssue($token, $repo, $num, $dryRun);
|
closeIssue($token, $repo, $num, $dryRun);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Feature/enhancement issues stay OPEN — do not bulk-close backlog items here.
|
|
||||||
|
|
||||||
echo "Done.\n";
|
echo "Done.\n";
|
||||||
|
|||||||
Reference in New Issue
Block a user