feat: add initial implementation of eSodisce / GURS batch helper extension
Publish Firefox extension / publish (push) Successful in 4m21s

This commit is contained in:
komelt committed 2026-10-05 14:42:50 +02:00
1 parent 902cf5d813
commit f1a16ddec0
10 files changed
+769 -1

No files matched your search

+454
View File
@@ -0,0 +1,454 @@
name: Publish Firefox extension
on:
push:
branches:
- main
# The workflow's own update-manifest-only commit must not start another run.
paths-ignore:
- updates.json
permissions:
contents: write
concurrency:
group: publish-main
cancel-in-progress: false
jobs:
publish:
runs-on: ubuntu-latest
env:
API_BASE: https://git.vahta.net/api/v1
REPOSITORY: komelt/zemljiska-knjiga-vticnik
RUN_NUMBER: ${{ github.run_number }}
TARGET_COMMIT: ${{ github.sha }}
steps:
- name: Check out source
uses: actions/checkout@v4
with:
token: ${{ secrets.GITEA_TOKEN }}
persist-credentials: true
- name: Validate and stage extension
shell: bash
run: |
set -Eeuo pipefail
command -v jq >/dev/null
command -v node >/dev/null
command -v curl >/dev/null
command -v npx >/dev/null
command -v sha256sum >/dev/null
jq -e . manifest.json >/dev/null
SOURCE_VERSION="$(jq -er '.version | strings' manifest.json)"
if [[ ! "$SOURCE_VERSION" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]]; then
echo "manifest.json version must contain one to three numeric components" >&2
exit 1
fi
if [[ ! "$RUN_NUMBER" =~ ^[1-9][0-9]*$ ]]; then
echo "The Actions run number is missing or is not a positive integer" >&2
exit 1
fi
VERSION="${SOURCE_VERSION}.${RUN_NUMBER}"
if [[ ! "$VERSION" =~ ^[0-9]+(\.[0-9]+){1,3}$ ]]; then
echo "Computed Firefox version is invalid: $VERSION" >&2
exit 1
fi
STAGING_DIR="$RUNNER_TEMP/zemljiska-knjiga-vticnik-$VERSION"
SIGNED_XPI="$RUNNER_TEMP/zemljiska-knjiga-vticnik-$VERSION.xpi"
rm -rf "$STAGING_DIR"
mkdir -p "$STAGING_DIR"
RUNTIME_FILES=(manifest.json background.js content-script.js popup.html popup.css popup.js)
cp -- "${RUNTIME_FILES[@]}" "$STAGING_DIR/"
jq --arg version "$VERSION" '.version = $version' \
"$STAGING_DIR/manifest.json" > "$STAGING_DIR/manifest.json.tmp"
mv -- "$STAGING_DIR/manifest.json.tmp" "$STAGING_DIR/manifest.json"
jq -e . "$STAGING_DIR/manifest.json" >/dev/null
for javascript_file in background.js content-script.js popup.js; do
node --check "$STAGING_DIR/$javascript_file"
done
printf 'VERSION=%s\nSTAGING_DIR=%s\nSIGNED_XPI=%s\n' \
"$VERSION" "$STAGING_DIR" "$SIGNED_XPI" >> "$GITHUB_ENV"
- name: Reuse an existing Gitea release asset when available
shell: bash
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
set -Eeuo pipefail
ASSET_NAME="zemljiska-knjiga-vticnik-$VERSION.xpi"
RELEASE_RESPONSE="$RUNNER_TEMP/gitea-release-$VERSION.json"
RELEASE_URL="$API_BASE/repos/$REPOSITORY/releases/tags/v$VERSION"
if ! RELEASE_STATUS="$(curl --silent --show-error \
--header "Authorization: token $GITEA_TOKEN" \
--write-out '%{http_code}' --output "$RELEASE_RESPONSE" \
"$RELEASE_URL")"; then
echo 'Could not query the Gitea release.' >&2
exit 1
fi
case "$RELEASE_STATUS" in
404)
printf 'RELEASE_ID=\nRELEASE_ASSET_EXISTS=false\n' >> "$GITHUB_ENV"
exit 0
;;
2??)
;;
*)
echo "Gitea release lookup returned HTTP $RELEASE_STATUS" >&2
cat "$RELEASE_RESPONSE" >&2
exit 1
;;
esac
RELEASE_ID="$(jq -er '.id | numbers' "$RELEASE_RESPONSE")"
matching_assets="$(jq -c --arg name "$ASSET_NAME" \
'[.assets[]? | select(.name == $name)]' "$RELEASE_RESPONSE")"
asset_count="$(jq -er 'length' <<< "$matching_assets")"
if [[ "$asset_count" -gt 1 ]]; then
echo "Gitea release contains multiple assets named $ASSET_NAME" >&2
exit 1
fi
if [[ "$asset_count" -eq 0 ]]; then
printf 'RELEASE_ID=%s\nRELEASE_ASSET_EXISTS=false\n' "$RELEASE_ID" >> "$GITHUB_ENV"
exit 0
fi
asset_size="$(jq -r '.[0].size // empty' <<< "$matching_assets")"
asset_url="$(jq -r '.[0].browser_download_url // empty' <<< "$matching_assets")"
if [[ ! "$asset_url" =~ ^https://[^[:space:]]+$ ]]; then
echo 'The existing release asset has a missing or invalid HTTPS browser download URL.' >&2
exit 1
fi
rm -f -- "$SIGNED_XPI"
curl --fail --silent --show-error --location \
--header "Authorization: token $GITEA_TOKEN" \
--output "$SIGNED_XPI" \
"$asset_url"
[[ -s "$SIGNED_XPI" ]] || {
echo 'The existing release asset downloaded as an empty file.' >&2
exit 1
}
if [[ -n "$asset_size" && "$(wc -c < "$SIGNED_XPI")" != "$asset_size" ]]; then
echo 'The downloaded release asset size does not match Gitea metadata.' >&2
exit 1
fi
XPI_SHA256="$(sha256sum "$SIGNED_XPI" | awk '{print $1}')"
[[ "$XPI_SHA256" =~ ^[0-9a-f]{64}$ ]] || {
echo 'Could not calculate a valid SHA-256 digest for the existing release asset.' >&2
exit 1
}
expected_hash="$(jq -r --arg version "$VERSION" \
'[.addons[]?.updates[]? | select(.version == $version) | .update_hash] | unique | if length == 1 then .[0] else "" end' \
updates.json 2>/dev/null || true)"
if [[ -n "$expected_hash" && "$expected_hash" != "sha256:$XPI_SHA256" ]]; then
echo 'The existing release asset does not match the update manifest hash.' >&2
exit 1
fi
printf 'RELEASE_ID=%s\nRELEASE_ASSET_EXISTS=true\nXPI_SHA256=%s\n' \
"$RELEASE_ID" "$XPI_SHA256" >> "$GITHUB_ENV"
- name: Sign unlisted build through AMO
if: env.RELEASE_ASSET_EXISTS != 'true'
shell: bash
env:
AMO_JWT_ISSUER: ${{ secrets.AMO_JWT_ISSUER }}
AMO_JWT_SECRET: ${{ secrets.AMO_JWT_SECRET }}
run: |
set -Eeuo pipefail
ARTIFACTS_DIR="$RUNNER_TEMP/web-ext-artifacts-$VERSION"
rm -rf "$ARTIFACTS_DIR" "$SIGNED_XPI"
mkdir -p "$ARTIFACTS_DIR"
npx --yes web-ext@10.7.0 sign \
--channel=unlisted \
--source-dir "$STAGING_DIR" \
--artifacts-dir "$ARTIFACTS_DIR" \
--api-key "$AMO_JWT_ISSUER" \
--api-secret "$AMO_JWT_SECRET"
mapfile -t signed_files < <(find "$ARTIFACTS_DIR" -maxdepth 1 -type f -name '*.xpi' -print)
if [[ "${#signed_files[@]}" -ne 1 ]]; then
echo "Expected exactly one signed XPI, found ${#signed_files[@]}" >&2
exit 1
fi
cp -- "${signed_files[0]}" "$SIGNED_XPI"
XPI_SHA256="$(sha256sum "$SIGNED_XPI" | awk '{print $1}')"
[[ "$XPI_SHA256" =~ ^[0-9a-f]{64}$ ]] || {
echo 'Could not calculate a valid SHA-256 digest for the signed XPI.' >&2
exit 1
}
printf 'XPI_SHA256=%s\n' "$XPI_SHA256" >> "$GITHUB_ENV"
- name: Create public Gitea release and upload XPI
shell: bash
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
set -Eeuo pipefail
ASSET_NAME="zemljiska-knjiga-vticnik-$VERSION.xpi"
RELEASE_RESPONSE="$RUNNER_TEMP/gitea-release-$VERSION.json"
download_asset() {
local asset_url="$1"
local asset_size="$2"
if [[ ! "$asset_url" =~ ^https://[^[:space:]]+$ ]]; then
echo 'The existing release asset has a missing or invalid HTTPS browser download URL.' >&2
exit 1
fi
rm -f -- "$SIGNED_XPI"
curl --fail --silent --show-error --location \
--header "Authorization: token $GITEA_TOKEN" \
--output "$SIGNED_XPI" \
"$asset_url"
[[ -s "$SIGNED_XPI" ]] || {
echo 'The existing release asset downloaded as an empty file.' >&2
exit 1
}
if [[ -n "$asset_size" && "$(wc -c < "$SIGNED_XPI")" != "$asset_size" ]]; then
echo 'The downloaded release asset size does not match Gitea metadata.' >&2
exit 1
fi
XPI_SHA256="$(sha256sum "$SIGNED_XPI" | awk '{print $1}')"
[[ "$XPI_SHA256" =~ ^[0-9a-f]{64}$ ]] || {
echo 'Could not calculate a valid SHA-256 digest for the existing release asset.' >&2
exit 1
}
local expected_hash
expected_hash="$(jq -r --arg version "$VERSION" \
'[.addons[]?.updates[]? | select(.version == $version) | .update_hash] | unique | if length == 1 then .[0] else "" end' \
updates.json 2>/dev/null || true)"
if [[ -n "$expected_hash" && "$expected_hash" != "sha256:$XPI_SHA256" ]]; then
echo 'The existing release asset does not match the update manifest hash.' >&2
exit 1
fi
RELEASE_ASSET_EXISTS=true
}
inspect_assets() {
local asset_count asset_url asset_size
asset_count="$(jq -er --arg name "$ASSET_NAME" \
'[.assets[]? | select(.name == $name)] | length' "$RELEASE_RESPONSE")"
if [[ "$asset_count" -gt 1 ]]; then
echo "Gitea release contains multiple assets named $ASSET_NAME" >&2
exit 1
fi
if [[ "$asset_count" -eq 1 ]]; then
asset_size="$(jq -r --arg name "$ASSET_NAME" \
'.assets[] | select(.name == $name) | .size // empty' "$RELEASE_RESPONSE")"
asset_url="$(jq -r --arg name "$ASSET_NAME" \
'.assets[] | select(.name == $name) | .browser_download_url // empty' "$RELEASE_RESPONSE")"
download_asset "$asset_url" "$asset_size"
fi
}
fetch_release_by_tag() {
local status
if ! status="$(curl --silent --show-error \
--header "Authorization: token $GITEA_TOKEN" \
--write-out '%{http_code}' --output "$RELEASE_RESPONSE" \
"$API_BASE/repos/$REPOSITORY/releases/tags/v$VERSION")"; then
echo 'Could not query the Gitea release after a concurrent change.' >&2
exit 1
fi
[[ "$status" == 2?? ]] || {
echo "Gitea release lookup returned HTTP $status" >&2
cat "$RELEASE_RESPONSE" >&2
exit 1
}
RELEASE_ID="$(jq -er '.id | numbers' "$RELEASE_RESPONSE")"
}
if [[ -z "${RELEASE_ID:-}" ]]; then
RELEASE_PAYLOAD="$(jq -n \
--arg tag "v$VERSION" \
--arg name "zemljiska-knjiga-vticnik $VERSION" \
--arg target "$TARGET_COMMIT" \
'{tag_name: $tag, name: $name, target_commitish: $target, draft: false, prerelease: false}')"
if ! CREATE_STATUS="$(curl --silent --show-error \
--request POST \
--header "Authorization: token $GITEA_TOKEN" \
--header 'Content-Type: application/json' \
--data "$RELEASE_PAYLOAD" \
--write-out '%{http_code}' --output "$RELEASE_RESPONSE" \
"$API_BASE/repos/$REPOSITORY/releases")"; then
echo 'Could not create the Gitea release.' >&2
exit 1
fi
case "$CREATE_STATUS" in
2??)
RELEASE_ID="$(jq -er '.id | numbers' "$RELEASE_RESPONSE")"
;;
409|422)
fetch_release_by_tag
;;
*)
echo "Gitea release creation returned HTTP $CREATE_STATUS" >&2
cat "$RELEASE_RESPONSE" >&2
exit 1
;;
esac
fi
if [[ "${RELEASE_ASSET_EXISTS:-false}" != 'true' ]]; then
curl --fail --silent --show-error \
--header "Authorization: token $GITEA_TOKEN" \
--output "$RELEASE_RESPONSE" \
"$API_BASE/repos/$REPOSITORY/releases/$RELEASE_ID"
inspect_assets
fi
if [[ "${RELEASE_ASSET_EXISTS:-false}" != 'true' ]]; then
UPLOAD_RESPONSE="$RUNNER_TEMP/gitea-upload-$VERSION.json"
if ! UPLOAD_STATUS="$(curl --silent --show-error \
--request POST \
--header "Authorization: token $GITEA_TOKEN" \
--header 'Content-Type: application/x-xpinstall' \
--data-binary "@$SIGNED_XPI" \
--write-out '%{http_code}' --output "$UPLOAD_RESPONSE" \
"$API_BASE/repos/$REPOSITORY/releases/$RELEASE_ID/assets?name=$ASSET_NAME")"; then
echo 'Could not upload the signed XPI to the Gitea release.' >&2
exit 1
fi
case "$UPLOAD_STATUS" in
2??)
;;
409|422)
curl --fail --silent --show-error \
--header "Authorization: token $GITEA_TOKEN" \
--output "$RELEASE_RESPONSE" \
"$API_BASE/repos/$REPOSITORY/releases/$RELEASE_ID"
inspect_assets
[[ "${RELEASE_ASSET_EXISTS:-false}" == 'true' ]] || {
echo 'Gitea rejected the upload but the expected asset is still absent.' >&2
exit 1
}
;;
*)
echo "Gitea asset upload returned HTTP $UPLOAD_STATUS" >&2
cat "$UPLOAD_RESPONSE" >&2
exit 1
;;
esac
fi
[[ "$XPI_SHA256" =~ ^[0-9a-f]{64}$ ]] || {
echo 'Could not calculate a valid SHA-256 digest for the signed XPI.' >&2
exit 1
}
UPDATE_LINK="https://git.vahta.net/komelt/zemljiska-knjiga-vticnik/releases/download/v$VERSION/$ASSET_NAME"
UPDATE_MANIFEST="$RUNNER_TEMP/zemljiska-knjiga-vticnik-$VERSION-updates.json"
jq -n \
--arg id 'eprostor-gurs-batch@example.invalid' \
--arg version "$VERSION" \
--arg update_link "$UPDATE_LINK" \
--arg update_hash "sha256:$XPI_SHA256" \
'{addons: {($id): {updates: [{version: $version, update_link: $update_link, update_hash: $update_hash}]}}}' \
> "$UPDATE_MANIFEST"
jq -e . "$UPDATE_MANIFEST" >/dev/null
printf 'XPI_SHA256=%s\nUPDATE_MANIFEST=%s\n' "$XPI_SHA256" "$UPDATE_MANIFEST" >> "$GITHUB_ENV"
- name: Commit update manifest
shell: bash
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
set -Eeuo pipefail
export LC_ALL=C
normalize_component() {
local component="$1"
component="${component#${component%%[!0]*}}"
printf '%s\n' "${component:-0}"
}
compare_firefox_versions() {
local left="$1"
local right="$2"
local left_part right_part left_normalized right_normalized
local max_parts i
local -a left_parts right_parts
IFS='.' read -r -a left_parts <<< "$left"
IFS='.' read -r -a right_parts <<< "$right"
max_parts="${#left_parts[@]}"
[[ "${#right_parts[@]}" -gt "$max_parts" ]] && max_parts="${#right_parts[@]}"
for ((i = 0; i < max_parts; i += 1)); do
left_part="${left_parts[i]:-0}"
right_part="${right_parts[i]:-0}"
left_normalized="$(normalize_component "$left_part")"
right_normalized="$(normalize_component "$right_part")"
if [[ "${#left_normalized}" -lt "${#right_normalized}" ]]; then
printf '%s\n' -1
return
fi
if [[ "${#left_normalized}" -gt "${#right_normalized}" ]]; then
printf '%s\n' 1
return
fi
if [[ "$left_normalized" < "$right_normalized" ]]; then
printf '%s\n' -1
return
fi
if [[ "$left_normalized" > "$right_normalized" ]]; then
printf '%s\n' 1
return
fi
done
printf '%s\n' 0
}
compare_current_manifest_versions() {
local current_version comparison
[[ -f updates.json ]] || return 0
jq -e . updates.json >/dev/null
while IFS= read -r current_version; do
[[ -z "$current_version" ]] && continue
if [[ ! "$current_version" =~ ^[0-9]+(\.[0-9]+)*$ ]]; then
echo "Current updates.json contains an invalid Firefox version: $current_version" >&2
exit 1
fi
comparison="$(compare_firefox_versions "$VERSION" "$current_version")"
if [[ "$comparison" != '1' ]]; then
echo "Current update version $current_version is newer than or equal to $VERSION; skipping manifest commit."
return 1
fi
done < <(jq -er '.addons[]?.updates[]?.version // empty' updates.json)
return 0
}
for attempt in 1 2 3; do
git fetch --prune origin main
git reset --hard origin/main
if ! compare_current_manifest_versions; then
exit 0
fi
cp -- "$UPDATE_MANIFEST" updates.json
jq -e . updates.json >/dev/null
git config user.name 'gitea-actions[bot]'
git config user.email 'gitea-actions[bot]@users.noreply.git.vahta.net'
git add -- updates.json
if git diff --cached --quiet; then
echo 'updates.json is already current; nothing to commit.'
exit 0
fi
git commit --only updates.json -m "chore: publish Firefox update $VERSION"
if git push origin HEAD:main; then
exit 0
fi
echo "The main branch advanced during manifest publication; retrying ($attempt/3)." >&2
git reset --hard origin/main
sleep "$((attempt * 2))"
done
echo 'Could not publish updates.json after three bounded retries.' >&2
exit 1
+15 -1
View File
@@ -9,8 +9,9 @@ Neuradna Firefox WebExtension za pomoč pri ponavljajočem se izpolnjevanju obra
- Izrecno dejanje uporabnika za zagon je potrebno v trenutnem zavihku.
- Razširitev je omejena na `https://esodisce.si/*`.
- Razširitev ne uporablja Firefox Downloads API-ja in ne shranjuje PDF-datotek; samo izpolni obrazec in pritisne gumb `btn_pdf`.
- Vhodno datoteko hrani samo v lokalni shrambi razširitve; ne zbira in ne pošilja telemetrije ali podatkov razvijalcu.
## Namestitev v Firefox
## Razvojna/testna namestitev v Firefox
1. Odprite `about:debugging`.
2. Izberite **This Firefox** / **Ta Firefox**.
@@ -21,6 +22,19 @@ Neuradna Firefox WebExtension za pomoč pri ponavljajočem se izpolnjevanju obra
Za spremembe datotek razširitev odstranite in jo znova naložite v `about:debugging`. Sidebar ostane odprt med uporabo strani in izbiro datoteke; zadnja uspešno prebrana datoteka se shrani v lokalno shrambo razširitve.
## Produkcijska namestitev in samodejne posodobitve
Produkcijske izdaje objavlja Gitea Actions v javnem repozitoriju [komelt/zemljiska-knjiga-vticnik](https://git.vahta.net/komelt/zemljiska-knjiga-vticnik). Za produkcijsko uporabo prenesite in namestite podpisani XPI iz javnega Gitea Release-a. Ne nameščajte ga prek `about:debugging`, ker je taka namestitev začasna in nima stabilnega mehanizma za samodejne posodobitve.
Za nastavitev repozitorija mora biti repozitorij javen; javno morajo biti berljivi tudi Release asseti in surove datoteke, zlasti `updates.json` na stabilnem naslovu. V Gitea omogočite Actions in nastavite vgrajeni Actions token tega opravila na pravice `contents: write` (za potisk veje `main` in Release API). **Ne ustvarjajte skrivnosti `GITEA_TOKEN`**: `secrets.GITEA_TOKEN` je vgrajeni token opravila, ne uporabniško določena skrivnost. Ustvarite samo naslednji skrivnosti:
- `AMO_JWT_ISSUER` – Mozilla AMO JWT issuer;
- `AMO_JWT_SECRET` – Mozilla AMO JWT secret.
Vsak potisk v `main` ustvari podpisano AMO unlisted izdajo, objavi XPI v javnem Release-u in posodobi `updates.json`; samodejni potisk samo te datoteke ne sproži nove izdaje. Razširitev uporablja `browser_specific_settings.gecko.update_url` do surove datoteke na veji `main`, zato Firefox preverja nove izdaje po namestitvi podpisanega XPI. Osnovna različica je v izvornem `manifest.json`, akcija pa ji samodejno doda številko zagona, na primer `0.2.0` postane `0.2.0.17`.
Za izvajanje je potreben registriran Gitea runner z oznako `ubuntu-latest`. Na runnerju morajo biti na voljo `git`, `bash`, Node.js/npm/npx, `jq`, `curl` in `sha256sum`. Strežnik `git.vahta.net` mora imeti HTTPS-certifikat, ki mu zaupata Firefox in runnerji; sicer namestitev ter preverjanje in samodejne posodobitve ne bodo delovali.
## Vhodna datoteka
Vsaka vrstica vsebuje dve polji: **številčno šifro KO** in **parcelno številko**. Podprta so ločila `;`, tabulator in vejica. Prvo polje mora biti pozitivno celo število; vodilne ničle se ohranijo kot del niza. Drugo polje ostane niz, zato so podprte oblike, kot je `901/29`.
+5
View File
@@ -0,0 +1,5 @@
browser.action.onClicked.addListener(() => {
browser.sidebarAction.toggle().catch((error) => {
console.error('[eProstor GURS] Sidebar ni bilo mogoče preklopiti.', error);
});
});
+125
View File
@@ -0,0 +1,125 @@
(() => {
if (window.__eprostorGursHelperLoaded) return;
window.__eprostorGursHelperLoaded = true;
const SELECTORS = {
koNumber: 'idZnakNep.katastrskaObcina.idsrcsifrant',
parcel: 'idZnakNep.parcelnaStevilka'
};
const LOG_PREFIX = '[eProstor GURS]';
let stopRequested = false;
let running = false;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const report = (text) => browser.runtime.sendMessage({ type: 'BATCH_STATUS', text }).catch(() => {});
const log = (...args) => console.log(LOG_PREFIX, ...args);
const logError = (...args) => console.error(LOG_PREFIX, ...args);
function findField(selector) {
// getElementById avoids CSS escaping issues because the literal IDs contain dots.
return document.getElementById(selector);
}
function setNativeValue(element, value) {
element.focus();
const setter = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(element), 'value')?.set;
if (setter) setter.call(element, value); else element.value = value;
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
}
async function processRecord(record, index, total, confirmMode) {
if (stopRequested) return false;
log(`Začetek zapisa ${index + 1}/${total}`, {
koNumber: record.koNumber,
parcel: record.parcel
});
const koNumber = findField(SELECTORS.koNumber);
const parcel = findField(SELECTORS.parcel);
log('Polji najdeni', {
koField: SELECTORS.koNumber,
koFieldFound: Boolean(koNumber),
parcelField: SELECTORS.parcel,
parcelFieldFound: Boolean(parcel)
});
if (!koNumber || !parcel) throw new Error('Polj z ID-jema ni mogoče najti; preverite selectorje v content-script.js.');
if (confirmMode === 'manual' && !window.confirm(`Obdelam ${index + 1}/${total}: KO ${record.koNumber} / parcela ${record.parcel}?`)) {
log(`Zapis ${index + 1}/${total} preskočen na ročno zahtevo`);
return true;
}
log('Neposreden vnos KO', { recordNumber: index + 1, koNumber: record.koNumber });
setNativeValue(koNumber, record.koNumber);
log('Neposreden vnos parcele', { recordNumber: index + 1, parcel: record.parcel });
setNativeValue(parcel, record.parcel);
const button = document.getElementById('btn_pdf');
log('btn_pdf preverjen', { recordNumber: index + 1, found: Boolean(button) });
if (!button) throw new Error('Gumba z ID-jem »btn_pdf« ni mogoče najti.');
button.click();
log('btn_pdf kliknjen', { recordNumber: index + 1 });
report(`Zapis ${index + 1}/${total}: vrednosti vnesene in potrjene z gumbom btn_pdf.`);
log(`Zapis ${index + 1}/${total} uspešno obdelan`);
return true;
}
function describeError(error) {
const isObject = error !== null && (typeof error === 'object' || typeof error === 'function');
return {
message: isObject && error.message !== undefined ? String(error.message) : String(error),
name: isObject && error.name !== undefined ? String(error.name) : typeof error,
cause: isObject ? error.cause : undefined,
stack: isObject ? error.stack : undefined
};
}
function getDomDiagnostics() {
return {
koFieldPresent: Boolean(findField(SELECTORS.koNumber)),
parcelFieldPresent: Boolean(findField(SELECTORS.parcel)),
btnPdfPresent: Boolean(document.getElementById('btn_pdf'))
};
}
browser.runtime.onMessage.addListener(async (message) => {
if (message.type === 'STOP_BATCH') {
stopRequested = true;
log('Zahtev za ustavitev prejet', { state: 'stop-requested' });
return { message: 'Ustavljanje…' };
}
if (message.type !== 'START_BATCH') return undefined;
if (running) return { message: 'Obdelava že teče.' };
running = true;
stopRequested = false;
const { records, confirmMode = 'manual', delayMs = 1500 } = message;
log('Batch začet', { total: records.length, confirmMode, delayMs });
try {
for (let i = 0; i < records.length && !stopRequested; i += 1) {
try {
await processRecord(records[i], i, records.length, confirmMode);
} catch (error) {
const errorDetails = describeError(error);
logError(`Napaka pri zapisu ${i + 1}/${records.length}`, {
recordNumber: i + 1,
total: records.length,
record: records[i],
error: errorDetails,
dom: getDomDiagnostics()
});
report(`Zapis ${i + 1}: ${errorDetails.message}`);
if (confirmMode === 'manual' && !window.confirm(`Napaka pri zapisu ${i + 1}: ${errorDetails.message}\nNadaljujem?`)) break;
}
await sleep(delayMs);
}
const messageText = stopRequested ? 'Obdelava ustavljena.' : 'Obdelava končana.';
log(messageText, {
state: stopRequested ? 'stopped' : 'completed',
total: records.length
});
report(messageText);
return { message: messageText };
} finally {
running = false;
}
});
})();
+6
View File
@@ -0,0 +1,6 @@
Šifra KO;Parcelna številka
2491;901/29
2491;901/30
0107;1820
0107;1861
1535;1192/1
+36
View File
@@ -0,0 +1,36 @@
{
"manifest_version": 3,
"name": "eSodisce / GURS batch helper",
"version": "0.2.0",
"description": "Assists with repeated eSodisce/GURS parcel form entries. It never handles certificates, keys, PINs or PDF downloads.",
"permissions": ["activeTab", "storage"],
"host_permissions": ["https://esodisce.si/*"],
"background": {
"scripts": ["background.js"]
},
"action": {
"default_title": "eSodisce GURS batch helper"
},
"sidebar_action": {
"default_title": "eSodisce / GURS batch helper",
"default_panel": "popup.html",
"open_at_install": false
},
"content_scripts": [
{
"matches": ["https://esodisce.si/*"],
"js": ["content-script.js"],
"run_at": "document_idle"
}
],
"browser_specific_settings": {
"gecko": {
"id": "eprostor-gurs-batch@example.invalid",
"strict_min_version": "109.0",
"update_url": "https://git.vahta.net/komelt/zemljiska-knjiga-vticnik/raw/branch/main/updates.json",
"data_collection_permissions": {
"required": ["none"]
}
}
}
}
+14
View File
@@ -0,0 +1,14 @@
:root { color-scheme: light dark; font: 14px system-ui, sans-serif; }
body { width: 100%; box-sizing: border-box; margin: 0; background: Canvas; color: CanvasText; }
main { padding: 14px; }
h1 { font-size: 18px; margin: 0 0 4px; }
.muted { color: GrayText; font-size: 12px; }
label, legend { display: block; margin-top: 10px; font-weight: 600; }
input[type=file], input[type=number] { box-sizing: border-box; width: 100%; margin-top: 4px; }
fieldset { margin: 12px 0; padding: 8px; }
fieldset label { margin-top: 5px; font-weight: 400; }
button { width: 100%; margin-top: 14px; padding: 8px; border: 0; border-radius: 5px; background: #1769aa; color: white; cursor: pointer; }
button:disabled { opacity: .5; cursor: not-allowed; }
button.secondary { background: #666; }
#status { min-height: 2.5em; margin-top: 10px; white-space: pre-wrap; }
details { margin-top: 10px; font-size: 12px; }
+32
View File
@@ -0,0 +1,32 @@
<!doctype html>
<html lang="sl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>eSodisce / GURS batch helper</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<main>
<h1>eSodisce / GURS</h1>
<p class="muted">Pomočnik za zaporedno izpolnjevanje obrazca po šifri KO.</p>
<label for="inputFile">TXT/CSV seznam</label>
<input id="inputFile" type="file" accept=".txt,.csv,text/plain,text/csv">
<div id="fileInfo" class="muted">Datoteka še ni izbrana.</div>
<fieldset>
<legend>Potrjevanje</legend>
<label><input type="radio" name="confirmMode" value="manual" checked> ročno pred vsakim zapisom</label>
<label><input type="radio" name="confirmMode" value="auto"> samodejno (brez vprašanj)</label>
</fieldset>
<label for="delayMs">Čakanje med zapisi (ms)</label>
<input id="delayMs" type="number" min="500" step="100" value="1500">
<button id="startButton" type="button" disabled>Začni v trenutnem zavihku</button>
<button id="stopButton" type="button" class="secondary">Ustavi</button>
<div id="status" role="status" aria-live="polite">Pripravljeno.</div>
</main>
<script src="popup.js"></script>
</body>
</html>
+75
View File
@@ -0,0 +1,75 @@
(() => {
const inputFile = document.querySelector('#inputFile');
const fileInfo = document.querySelector('#fileInfo');
const startButton = document.querySelector('#startButton');
const stopButton = document.querySelector('#stopButton');
const delayInput = document.querySelector('#delayMs');
const status = document.querySelector('#status');
let records = [];
function updateLoadedFile(fileName) {
fileInfo.textContent = `${fileName}: ${records.length} veljavnih zapisov`;
startButton.disabled = records.length === 0;
}
function setStatus(text) { status.textContent = text; }
function parseLine(line) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) return null;
// Prefer tab, then semicolon, then comma; exactly two fields are required.
const separator = trimmed.includes('\t') ? '\t' : (trimmed.includes(';') ? ';' : ',');
const fields = trimmed.split(separator).map((field) => field.trim().replace(/^"|"$/g, ''));
if (fields.length !== 2 || !fields[0] || !fields[1]) return null;
const header = `${fields[0]} ${fields[1]}`.toLocaleLowerCase('sl');
if (header.includes('katastr') && header.includes('parcel')) return null;
if (!/^\d+$/.test(fields[0]) || /^0+$/.test(fields[0])) return null;
return { koNumber: fields[0], parcel: fields[1] };
}
inputFile.addEventListener('change', async () => {
const file = inputFile.files?.[0];
if (!file) return;
const lines = (await file.text()).split(/\r?\n/);
records = lines.map(parseLine).filter(Boolean);
await browser.storage.local.set({ batchInput: { fileName: file.name, records } });
updateLoadedFile(file.name);
setStatus(records.length ? 'Pripravljeno za trenutni zavihek.' : 'Ni veljavnih zapisov.');
});
browser.storage.local.get('batchInput').then(({ batchInput }) => {
if (!batchInput || !Array.isArray(batchInput.records) || !batchInput.records.length) return;
records = batchInput.records;
updateLoadedFile(batchInput.fileName || 'Shranjena vhodna datoteka');
setStatus('Naložena je zadnja izbrana vhodna datoteka.');
});
startButton.addEventListener('click', async () => {
if (!records.length) return;
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
if (!tab?.id) { setStatus('Ciljnega zavihka ni mogoče najti.'); return; }
const confirmMode = document.querySelector('input[name="confirmMode"]:checked').value;
const delayMs = Math.max(500, Number.parseInt(delayInput.value, 10) || 1500);
try {
const response = await browser.tabs.sendMessage(tab.id, { type: 'START_BATCH', records, confirmMode, delayMs });
setStatus(response?.message || 'Obdelava zagnana.');
} catch (error) {
setStatus(`Napaka: odprite ciljno stran in poskusite znova. ${error.message}`);
}
});
stopButton.addEventListener('click', async () => {
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
if (!tab?.id) { setStatus('Ciljnega zavihka ni mogoče najti.'); return; }
try {
const response = await browser.tabs.sendMessage(tab.id, { type: 'STOP_BATCH' });
setStatus(response?.message || 'Zahteva za ustavitev poslana.');
} catch (error) {
setStatus(`Napaka: odprite ciljno stran in poskusite znova. ${error.message}`);
}
});
browser.runtime.onMessage.addListener((message) => {
if (message.type === 'BATCH_STATUS') setStatus(message.text);
});
})();
+7
View File
@@ -0,0 +1,7 @@
{
"addons": {
"eprostor-gurs-batch@example.invalid": {
"updates": []
}
}
}