From a3cb35f07831daadc2e679f77277389fd32f5dbe Mon Sep 17 00:00:00 2001 From: Cotiso Hanganu Date: Sat, 18 Apr 2026 19:23:26 +0300 Subject: [PATCH] Initial commit: qvw-preserve-interface skill --- SKILL.md | 172 +++++++++++++++++++++++++++++++++++ templates/.gitignore | 46 ++++++++++ templates/Backup_QVW.ps1 | 28 ++++++ templates/Reload_wrapped.bat | 43 +++++++++ templates/inject_include.ps1 | 41 +++++++++ 5 files changed, 330 insertions(+) create mode 100644 SKILL.md create mode 100644 templates/.gitignore create mode 100644 templates/Backup_QVW.ps1 create mode 100644 templates/Reload_wrapped.bat create mode 100644 templates/inject_include.ps1 diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..c9c8297 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,172 @@ +--- +name: qvw-preserve-interface +description: > + Protect QVW interface edits (sheets, tableboxes, charts, listboxes) from being + destroyed by reload-save cycles. Sets up local git tracking of the -prj/ folder, + enables QlikView's native auto-backup, installs a rolling 20-slot QVW backup, + and wraps the reload BAT with pre/post-reload git commits so every interface + edit is recoverable even if the QVW binary gets clobbered. Use this skill when + the user says "my interface disappeared", "reload overwrote my edits", + "protect my QVW objects", "version-control the QlikView app", + "set up git for the QVW project", or mentions losing tableboxes/charts + after a reload. Also apply proactively when scaffolding any new QVW project + that will be reloaded repeatedly. Uses the COM GetProperties/SetProperties + pattern (NOT SetScript — which doesn't exist on modern QV Document). +--- + +# QVW Interface Preservation (git-backed) + +You are setting up a git-based safety net for QlikView Desktop projects so +that **every interface edit survives reload cycles**. Without this, QV's +default behaviour can silently drop charts/tableboxes whose source fields +change during a reload. + +## Why this is needed (the failure mode) + +1. User opens `app.qvw` in QlikView, adds a tablebox or chart, saves. +2. Reload BAT fires (`qv.exe /r app.qvw`) — QV re-executes the script, + which may rename/drop fields, then saves. +3. Objects referencing dropped/renamed fields are **silently removed** from + the saved `.qvw` binary. +4. QV's native ring-buffer backup (`BackupKeepCopies=4` by default) gets + evicted within a day of reload cycles → user's edit is **unrecoverable**. + +The fix: put every `-prj/` XML under git **before** each reload. + +## Step 1: Gather inputs + +Ask the user for: +1. **Project root** — e.g. `C:\Users\...\MyProject\`. Must contain the QVW and the + reload BAT. +2. **QVW filename** — e.g. `MyProject_v3.qvw`. (If multiple QVWs, ask which one.) +3. **Reload BAT filename** — e.g. `Reload_MyProject.bat`. (Often in `8.BAT\`.) + +Read the project layout to confirm those exist before proceeding. + +## Step 2: Enable QlikView's native auto-backup + +Check `%APPDATA%\QlikTech\QlikView\Settings.ini` for these keys: +```ini +[Settings 7] +BackupBeforeReload=1 +BackupEnable=1 +BackupKeepCopies=4 ; QV default; user can raise +AlwaysGenerateProject=1 ; required so -prj/ is populated on save +``` +If any are missing, add them. Explain to the user that +`AlwaysGenerateProject=1` is essential — without it the `-prj/` folder +doesn't get the XML files that git tracks. + +## Step 3: Initialize git in the project root + +```bash +cd +git init +git config user.email "" +git config user.name "" +``` + +## Step 4: Write the `.gitignore` + +Critical: **Windows git is case-insensitive by default**, so `*.qvw` would +match the folder `4.Apps.QVW/` and ignore everything inside it (including +the precious `-prj/` subfolders). Use the `.gitignore` template +`templates/.gitignore` from this skill, which has the case-insensitive fix: + +- Ignores `**/*.qvw`, `**/*.qvf`, `**/*.qvd`, `**/*.qvw.bak*` (actual binary files) +- Ignores `/4.Apps.QVW/backups/**` and `/4.Apps.QVW/_recovery/**` +- **Re-includes** `/4.Apps.QVW/**/*-prj/` and its contents (the `!` rules come + AFTER the ignore rules) +- Ignores logs, `.DS_Store`, `Thumbs.db`, `*.tmp` +- Ignores `4.Apps.QVW/Version * of *` (QV's native backup files, too big) + +Copy the template to `/.gitignore` and adapt paths if the +project doesn't follow the QQinfo `4.Apps.QVW/` convention — ask the user. + +## Step 5: Install `Backup_QVW.ps1` (rolling buffer) + +Copy `templates/Backup_QVW.ps1` into `/8.BAT/` (or wherever +BATs live). Default retention is **20 slots** (parameter `-Keep`), so a day +of reloads can't evict a good edit. This runs BEFORE each reload as a +fallback safety net in case git isn't enough (e.g. if the user forgot +to enable `AlwaysGenerateProject`). + +## Step 6: Wrap the reload BAT with git commits + +The reload BAT must do, in order: +1. **Pre-reload `git add -A && git commit`** — captures whatever manual + edits the user made since the last reload. +2. Backup QVW to rolling buffer (`Backup_QVW.ps1 -Keep 20`). +3. `qv.exe /r ` — the actual reload. +4. **Post-reload `git add -A && git commit`** — captures script-side + changes so you can diff "user edits" vs "reload output". + +Use the template `templates/Reload_wrapped.bat`. Substitute: +- `%PROJECT_ROOT%` — the project root +- `%APP_PATH%` — the QVW to reload +- `%LOG_FILE%` — where to append reload log (usually `10.QQlog\`) + +Warn user: all `git commit` commands use `--allow-empty` so they never +fail even when nothing changed. + +## Step 7: Changing the script include path (if needed) + +If the QVW's embedded script points at an old `9.QVSvN` folder and needs +to target a new one (e.g. during a version bump), use COM script +injection via `GetProperties / SetProperties`. The legacy `SetScript()` +method does NOT exist on modern QV Document COM. Template at +`templates/inject_include.ps1`. Usage: + +```powershell +powershell -File inject_include.ps1 ` + -QvwPath "C:\path\to\app.qvw" ` + -NewInclude "C:\path\to\9.QVSv9\0100.MustInclude.qvs" +``` + +The script calls `$doc.GetProperties()` → sets `.Script` → `$doc.SetProperties($props)` → `.Save()`. + +## Step 8: Make the initial commit + +```bash +cd +git add -A +git commit -m "Initial commit: QVS/config/-prj tracked; binaries excluded" +git tag initial +``` + +Then verify with `git log --oneline` and confirm the `-prj/*.xml` files +made it into the commit (not just `.qvs`/`.csv`). Run +`git check-ignore -v 4.Apps.QVW/-prj/TB01.xml` to confirm NOT ignored. + +## Step 9: Tell the user what to do when disaster strikes + +If a reload destroys an object: +1. `git log --oneline --all -- 4.Apps.QVW/-prj/` — find the last + commit that had it. +2. `git show :4.Apps.QVW/-prj/TB05.xml` — recover the XML. +3. Paste the XML back into the `-prj/` folder and "Import from project" + in QlikView, OR restore the whole QVW from the rolling buffer: + `cp 4.Apps.QVW/backups/_.qvw 4.Apps.QVW/.qvw`. + +## Step 10: Post-install verification + +Run the reload BAT once end-to-end. Confirm: +- Two new git commits appear (`pre-reload` + `post-reload`). +- A new `.qvw` lands in `4.Apps.QVW/backups/`. +- The `-prj/` folder is populated with fresh XML timestamps. +- `git check-ignore -v` on any `-prj/*.xml` returns empty (NOT ignored). + +If any of those fail, diagnose before declaring success. + +## Common gotchas + +- **Filename with spaces/typos** (e.g. `QQpo_v2 inerface preps.qvw`) — QV COM + `OpenDoc` returns null silently. Rename to a clean path before scripting. +- **`.qvw.bak.qvf`** is QV's native one-off backup (same content, renamed). + Safe to keep — gitignore already excludes. +- **`Version N of .qvw`** — also QV native backups. Gitignored. +- **Tableboxes in "show all fields of table" mode** self-heal after field + renames. Tableboxes with explicit field lists do NOT — user has to re-edit. +- **Branch protection** — the local-only setup has no remote by design. + If the user wants off-site backup, run `git remote add origin ...` as a + follow-up step (Gitea / GitHub / Forgejo). diff --git a/templates/.gitignore b/templates/.gitignore new file mode 100644 index 0000000..006a7df --- /dev/null +++ b/templates/.gitignore @@ -0,0 +1,46 @@ +# ===== QVW project — preserve interface via git (-prj/ tracked, binaries ignored) ===== +# NOTE: on Windows, git is case-insensitive by default — a bare *.qvw would match +# the folder "4.Apps.QVW" and ignore everything inside it. So ignores are scoped +# to leaf filenames via explicit directory globs. + +# QVW/QVF/QVD files — only as files, never folders +**/*.qvw +**/*.qvf +**/*.qvw.bak* +**/*.qvd + +# Logs & scratch +*.log +**/*.qvs.log +*.tmp +.DS_Store +Thumbs.db + +# Explicit data folders to ignore (no content tracked) +/0.Sources.CSV/** +/1.Init.QVD/** +/3.Intermed.QVD/** +/4.Apps.QVW/backups/** +/4.Apps.QVW/_recovery/** +/10.LOG/** +/10.QQlog/** + +# "Version N of ..." — Qlik native backups next to QVW +4.Apps.QVW/Version */ + +# Re-include key folders that the case-insensitive QVW pattern accidentally blanketed. +# Order: un-ignore folder, then un-ignore contents. +!/4.Apps.QVW/ +!/4.Apps.QVW/* +!/4.Apps.QVW/**/*-prj/ +!/4.Apps.QVW/**/*-prj/** +!/1.Init.QVD/ +!/3.Intermed.QVD/ + +# But re-apply binary ignore inside the re-included 4.Apps.QVW +/4.Apps.QVW/*.qvw +/4.Apps.QVW/*.qvf +/4.Apps.QVW/*.qvw.bak* +/4.Apps.QVW/*.qvd +/4.Apps.QVW/backups/ +/4.Apps.QVW/_recovery/ diff --git a/templates/Backup_QVW.ps1 b/templates/Backup_QVW.ps1 new file mode 100644 index 0000000..a71bf40 --- /dev/null +++ b/templates/Backup_QVW.ps1 @@ -0,0 +1,28 @@ +# Backup_QVW.ps1 — rolling ring of pre-reload QVW snapshots. +# Usage: powershell -File Backup_QVW.ps1 -QvwPath "..\4.Apps.QVW\MyApp.qvw" [-Keep 20] +# Default retention 20 slots (a day of reloads won't evict good edits). +# NOTE: this is a FALLBACK. Primary safety net is git commits in Reload_*.bat. +param( + [Parameter(Mandatory=$true)][string]$QvwPath, + [int]$Keep = 20 +) + +$ErrorActionPreference = "Stop" +if (-not (Test-Path $QvwPath)) { Write-Host "No QVW to back up: $QvwPath"; exit 0 } + +$dir = Split-Path $QvwPath -Parent +$name = [System.IO.Path]::GetFileNameWithoutExtension($QvwPath) +$ext = [System.IO.Path]::GetExtension($QvwPath) +$backupDir = Join-Path $dir "backups" +if (-not (Test-Path $backupDir)) { New-Item -ItemType Directory -Path $backupDir -Force | Out-Null } + +$stamp = Get-Date -Format "yyyyMMdd_HHmmss" +$dest = Join-Path $backupDir ($name + "_" + $stamp + $ext) +Copy-Item $QvwPath $dest -Force +Write-Host "Backed up: $dest" + +# Prune: keep only $Keep most-recent backups matching "$name_*.qvw" +Get-ChildItem $backupDir -Filter ($name + "_*" + $ext) -File | + Sort-Object LastWriteTime -Descending | + Select-Object -Skip $Keep | + ForEach-Object { Remove-Item $_.FullName -Force; Write-Host "Pruned: $($_.Name)" } diff --git a/templates/Reload_wrapped.bat b/templates/Reload_wrapped.bat new file mode 100644 index 0000000..f756ee9 --- /dev/null +++ b/templates/Reload_wrapped.bat @@ -0,0 +1,43 @@ +@echo off +SETLOCAL ENABLEDELAYEDEXPANSION + +REM ============================================================ +REM Reload wrapped with pre/post git commits + rolling QVW backup. +REM Substitute PROJECT_ROOT and APP_PATH for your project. +REM ============================================================ + +SET "PROJECT_ROOT=<>" +SET "APP_PATH=%PROJECT_ROOT%\4.Apps.QVW\<>.qvw" +SET "LOG_DIR=%PROJECT_ROOT%\10.QQlog" +IF NOT EXIST "%LOG_DIR%" MKDIR "%LOG_DIR%" +SET "LOG_FILE=%LOG_DIR%\QQreload_monitor_<>.txt" + +FOR /F %%a IN ('powershell -NoProfile -Command "Get-Date -Format yyyyMMdd_HHmmss"') DO SET "TS=%%a" + +REM --- Pre-reload: snapshot current -prj state to git --- +PUSHD "%PROJECT_ROOT%" +git add -A >> "%LOG_FILE%" 2>&1 +git commit -m "pre-reload %TS% (user edits captured)" --allow-empty >> "%LOG_FILE%" 2>&1 +POPD + +REM --- Rolling QVW backup (fallback safety net) --- +powershell -ExecutionPolicy Bypass -File "%~dp0Backup_QVW.ps1" -QvwPath "%APP_PATH%" -Keep 20 >> "%LOG_FILE%" 2>&1 + +SET "QV_EXE=" +FOR /F "tokens=2*" %%A IN ('REG QUERY "HKLM\SOFTWARE\QlikTech\QlikView" /v InstallDir 2^>nul') DO SET "QV_EXE=%%B\qv.exe" +IF NOT DEFINED QV_EXE IF EXIST "C:\Program Files\QlikView\qv.exe" SET "QV_EXE=C:\Program Files\QlikView\qv.exe" +IF NOT DEFINED QV_EXE (ECHO qv.exe not found >> "%LOG_FILE%" & EXIT /B 2) + +ECHO ============================================================ >> "%LOG_FILE%" +ECHO [%date% %time%] reload start >> "%LOG_FILE%" +"%QV_EXE%" /r "%APP_PATH%" +SET "RC=%ERRORLEVEL%" +ECHO [%date% %time%] reload end rc=%RC% >> "%LOG_FILE%" + +REM --- Post-reload: capture script-side -prj changes to git --- +PUSHD "%PROJECT_ROOT%" +git add -A >> "%LOG_FILE%" 2>&1 +git commit -m "post-reload %TS% (rc=%RC%)" --allow-empty >> "%LOG_FILE%" 2>&1 +POPD + +EXIT /B %RC% diff --git a/templates/inject_include.ps1 b/templates/inject_include.ps1 new file mode 100644 index 0000000..3b639dc --- /dev/null +++ b/templates/inject_include.ps1 @@ -0,0 +1,41 @@ +# inject_include.ps1 — rewrite a QVW's embedded Script to include a different +# MustInclude .qvs file (typical use: version bump from 9.QVSv8 -> 9.QVSv9). +# +# IMPORTANT: The legacy $doc.SetScript($s) method does NOT exist on modern +# QlikView Document COM (>= QV11). Use GetProperties() -> .Script -> SetProperties(). +# +# Usage: +# powershell -ExecutionPolicy Bypass -File inject_include.ps1 ` +# -QvwPath "C:\path\to\app.qvw" ` +# -NewInclude "C:\path\to\9.QVSv9\0100.MustInclude.qvs" + +param( + [Parameter(Mandatory=$true)][string]$QvwPath, + [Parameter(Mandatory=$true)][string]$NewInclude +) + +$ErrorActionPreference = "Stop" +if (-not (Test-Path $QvwPath)) { throw "QVW not found: $QvwPath" } +if (-not (Test-Path $NewInclude)) { throw "Include file not found: $NewInclude" } + +$qv = New-Object -ComObject QlikTech.QlikView +$doc = $qv.OpenDoc($QvwPath, "", "") +if (-not $doc) { throw "OpenDoc returned null for $QvwPath (check filename for spaces/typos)" } +Start-Sleep -Seconds 3 + +$newScript = @" +SET ErrorMode = 0; +SET DateFormat='YYYY-MM-DD'; +`$(Include=$NewInclude); +"@ + +$props = $doc.GetProperties() +Write-Host "Current script head: $($props.Script.Substring(0, [Math]::Min(120, $props.Script.Length)))" + +$props.Script = $newScript +$doc.SetProperties($props) +$doc.Save() +Write-Host "Saved $QvwPath with include -> $NewInclude" + +Start-Sleep -Seconds 2 +$qv.Quit()