Răsfoiți Sursa

Fix empty Zombie-Apokalypse list and translate its items to German

Migration 002 looked item groups and items up by their English sample-data
names, so on a database with its own German groups the hidden activity got
no items. Migration 003 rebuilds the list: items go into their German group
or, if it is missing, a new "Zombie-Abwehr" group; existing items are
reused; the English items 002 created are removed. It is safe to rerun.

Adds a few more items (Twinkies, Knoblauch, Gartenzwerg zur Ablenkung, ...)
and renames the activity to Zombie-Apokalypse in the group Überleben.

The progress nag no longer lowercases item names, which mangled German
nouns ("the stirnlampe").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sebastian 2 săptămâni în urmă
părinte
comite
3badc35adf
4 a modificat fișierele cu 119 adăugiri și 39 ștergeri
  1. 4 3
      README.md
  2. 85 0
      migrations/003_zombie_items_german.sql
  3. 1 7
      packing.php
  4. 29 29
      schema.sql

+ 4 - 3
README.md

@@ -26,8 +26,8 @@ PackIt is a smart packing list generator for outdoor activities. Select the acti
 - **Per-mapping quantities** — Each activity-to-item mapping carries a quantity. Items needed in multiples (e.g. 2× Water bottle) are rendered with a leading multiplier, and merging across activities keeps the largest required quantity.
 - **Per-mapping quantities** — Each activity-to-item mapping carries a quantity. Items needed in multiples (e.g. 2× Water bottle) are rendered with a leading multiplier, and merging across activities keeps the largest required quantity.
 - **Grouped items** — Items are organised into logical categories (Clothing, Shelter, Navigation, Medical, Water & Food, Safety Gear) for easy scanning.
 - **Grouped items** — Items are organised into logical categories (Clothing, Shelter, Navigation, Medical, Water & Food, Safety Gear) for easy scanning.
 - **Interactive checklist** — Click any item to mark it as packed. Checked items are visually struck through and a progress bar updates in real time. Ticks are stored in the browser under the list's hash and restored on the next visit; a **Reset** button appears once something is ticked. Nothing is sent to the server, so each device keeps its own progress, and a browser with site data blocked simply falls back to not remembering.
 - **Interactive checklist** — Click any item to mark it as packed. Checked items are visually struck through and a progress bar updates in real time. Ticks are stored in the browser under the list's hash and restored on the next visit; a **Reset** button appears once something is ticked. Nothing is sent to the server, so each device keeps its own progress, and a browser with site data blocked simply falls back to not remembering.
-- **Nagging progress messages** — A line under the progress bar comments on how the packing is going, from *"The tent won't pack itself."* at zero to *"Just the headlamp + spare batteries left."* for the final item. The line stays the same until you reach the next stage, so it doesn't change on every tick.
-- **Hidden activities** — Activities flagged as hidden in the admin panel are left off the selection page. They appear when someone clicks **Pack it!** ten times in quick succession (at most 0.5 s between clicks) while nothing is selected. The revealed activity is pre-selected, and its lists and short links work like any other. The sample data hides one: *Zombie apocalypse*.
+- **Nagging progress messages** — A line under the progress bar comments on how the packing is going, from *"The tent won't pack itself."* at zero to *"Just the Stirnlampe left."* for the final item. The line stays the same until you reach the next stage, so it doesn't change on every tick.
+- **Hidden activities** — Activities flagged as hidden in the admin panel are left off the selection page. They appear when someone clicks **Pack it!** ten times in quick succession (at most 0.5 s between clicks) while nothing is selected. The revealed activity is pre-selected, and its lists and short links work like any other. The sample data hides one: *Zombie-Apokalypse*.
 - **Shareable short URLs** — Each unique activity selection is stored under a deterministic 5-character hash, producing a permanent link short enough to share comfortably over chat or read out loud.
 - **Shareable short URLs** — Each unique activity selection is stored under a deterministic 5-character hash, producing a permanent link short enough to share comfortably over chat or read out loud.
 - **Installable PWA** — A web app manifest, theme color, and maskable icons let PackIt be installed to the home screen on mobile or as a standalone app on desktop.
 - **Installable PWA** — A web app manifest, theme color, and maskable icons let PackIt be installed to the home screen on mobile or as a standalone app on desktop.
 - **Works offline** — A service worker (`sw.js`) caches the app shell and every list you have opened, which is what makes an installed PackIt useful at the trailhead. Pages are network-first, so an online visit always reflects the latest admin changes and falls back to the cached copy only when the network is gone. A list never opened on the device shows `offline.html` instead. The admin panel is deliberately never cached.
 - **Works offline** — A service worker (`sw.js`) caches the app shell and every list you have opened, which is what makes an installed PackIt useful at the trailhead. Pages are network-first, so an online visit always reflects the latest admin changes and falls back to the cached copy only when the network is gone. A list never opened on the device shows `offline.html` instead. The admin panel is deliberately never cached.
@@ -77,11 +77,12 @@ Run any migrations your database predates, in order:
 ```
 ```
 mysql -u <user> -p <your-database> < migrations/001_hardening.sql
 mysql -u <user> -p <your-database> < migrations/001_hardening.sql
 mysql -u <user> -p <your-database> < migrations/002_hidden_activities.sql
 mysql -u <user> -p <your-database> < migrations/002_hidden_activities.sql
+mysql -u <user> -p <your-database> < migrations/003_zombie_items_german.sql
 ```
 ```
 
 
 It adds the `login_attempts` table, narrows `selection_hashes.hash` to `VARCHAR(16)`, and indexes `activity_ids`. It also deletes any rows still holding a 64-character hash from the original link scheme — those links have been unreachable since links were shortened to 5 characters, and the table is only a cache of selection → hash, so anything deleted is recreated by re-picking the activities.
 It adds the `login_attempts` table, narrows `selection_hashes.hash` to `VARCHAR(16)`, and indexes `activity_ids`. It also deletes any rows still holding a 64-character hash from the original link scheme — those links have been unreachable since links were shortened to 5 characters, and the table is only a cache of selection → hash, so anything deleted is recreated by re-picking the activities.
 
 
-`002` adds the `activities.hidden` column and the hidden *Zombie apocalypse* activity (in a new *Survival* group) with its items. It finds item groups and existing items by their sample-data names. An item whose group has been renamed is skipped, and you can add it later in the admin panel.
+`002` adds the `activities.hidden` column and the hidden zombie activity. Its item lookups only match the English sample-data names, so on a database with its own item groups the activity ends up empty. `003` fixes that and renames everything to German (*Zombie-Apokalypse* in *Überleben*). Each item goes into its German group (*Kleidung*, *Essen*, *Küche*, *Elektronik*, *Sicherheit*, *Kleinzeug*) or, if that group doesn't exist, into a new *Zombie-Abwehr* group. Items you already have, such as *Stirnlampe* or *EH-Set*, are reused, and the English items `002` created are removed. It is safe to run more than once.
 
 
 ### Admin password
 ### Admin password
 
 

+ 85 - 0
migrations/003_zombie_items_german.sql

@@ -0,0 +1,85 @@
+-- Migration 003 – German item list for the hidden Zombie apocalypse activity.
+-- Run once, after 002, against an existing PackIt database:
+--     mysql -u <user> -p <your-database> < migrations/003_zombie_items_german.sql
+-- New installs get all of this from schema.sql and can skip it.
+--
+-- 002 looked item groups and items up by the English sample-data names, so on
+-- a database with its own (e.g. German) groups the activity ended up empty.
+-- This rebuilds the list without depending on any particular group existing:
+-- an item whose group is missing lands in the new "Zombie-Abwehr" group.
+-- Safe to run more than once.
+
+-- 1. Activity and its group in German.
+SET @zombie = (SELECT activityID FROM activities
+               WHERE hidden = 1 AND activityName IN ('Zombie apocalypse', 'Zombie-Apokalypse')
+               ORDER BY activityID LIMIT 1);
+
+UPDATE activities SET activityName = 'Zombie-Apokalypse' WHERE activityID = @zombie;
+UPDATE activity_groups ag
+JOIN activities a ON a.groupID = ag.groupID AND a.activityID = @zombie
+SET ag.groupName = 'Überleben'
+WHERE ag.groupName = 'Survival';
+
+-- 2. Start the list over. The English items 002 created are only removed when
+--    no other activity uses them.
+DELETE FROM activity_item_map WHERE activityID = @zombie;
+DELETE FROM items
+WHERE itemName IN ('Running shoes (cardio is rule #1)', 'Bite-proof arm guards',
+                   'Map with safe zones marked', 'Canned beans', 'Manual can opener',
+                   'Cricket bat', 'Crowbar', 'Duct tape', 'Walkie-talkies')
+  AND itemID NOT IN (SELECT itemID FROM activity_item_map);
+
+-- 3. Item group for the gear no normal trip needs. The guard sits in HAVING:
+--    an aggregate still yields a row when WHERE filters every row out.
+INSERT INTO item_groups (groupName, sortOrder)
+SELECT 'Zombie-Abwehr', COALESCE(MAX(sortOrder), 0) + 1 FROM item_groups
+HAVING SUM(groupName = 'Zombie-Abwehr') = 0;
+SET @zgroup = (SELECT groupID FROM item_groups WHERE groupName = 'Zombie-Abwehr'
+               ORDER BY groupID LIMIT 1);
+
+-- 4. The list. altName is the English sample-data name of an item that may
+--    already exist; an item found under either name is reused, not duplicated.
+CREATE TEMPORARY TABLE zombie_list (
+    itemName  VARCHAR(150) NOT NULL,
+    altName   VARCHAR(150) NULL,
+    groupName VARCHAR(100) NOT NULL,
+    quantity  INT          NOT NULL
+);
+INSERT INTO zombie_list (itemName, altName, groupName, quantity) VALUES
+('Laufschuhe (Regel Nr. 1: Cardio)',        NULL,                         'Kleidung',      1),
+('Bissfeste Armschützer (Paar)',            NULL,                         'Kleidung',      1),
+('Schlafsack',                              'Sleeping bag',               'Kleidung',      1),
+('Dosenbohnen',                             NULL,                         'Essen',         12),
+('Twinkies (letzte Packung der Welt)',      NULL,                         'Essen',         1),
+('Trinken (1 l)',                           'Water bottle',               'Essen',         2),
+('Dosenöffner (manuell)',                   NULL,                         'Küche',         1),
+('Feuerzeug',                               NULL,                         'Küche',         1),
+('Stirnlampe',                              'Headlamp + spare batteries', 'Elektronik',    1),
+('Funkgerät',                               NULL,                         'Elektronik',    2),
+('Kurbelradio',                             NULL,                         'Elektronik',    1),
+('EH-Set',                                  'First aid kit',              'Sicherheit',    1),
+('Multitool',                               NULL,                         'Sicherheit',    1),
+('Karte mit sicheren Zonen',                NULL,                         'Kleinzeug',     1),
+('Zombie-Wörterbuch (Grrr = Hallo)',        NULL,                         'Kleinzeug',     1),
+('Glaubwürdige Ausrede für die Bisswunde',  NULL,                         'Kleinzeug',     1),
+('Cricketschläger',                         NULL,                         'Zombie-Abwehr', 1),
+('Brecheisen',                              NULL,                         'Zombie-Abwehr', 1),
+('Panzertape',                              NULL,                         'Zombie-Abwehr', 1),
+('Bolzenschneider',                         NULL,                         'Zombie-Abwehr', 1),
+('Knoblauch (hilft nicht, beruhigt aber)',  NULL,                         'Zombie-Abwehr', 1),
+('Gartenzwerg zur Ablenkung',               NULL,                         'Zombie-Abwehr', 1);
+
+INSERT INTO items (groupID, itemName, sortOrder)
+SELECT COALESCE((SELECT ig.groupID FROM item_groups ig
+                 WHERE ig.groupName = z.groupName ORDER BY ig.groupID LIMIT 1), @zgroup),
+       z.itemName, 0
+FROM zombie_list z
+WHERE NOT EXISTS (SELECT 1 FROM items i WHERE i.itemName IN (z.itemName, z.altName));
+
+INSERT INTO activity_item_map (activityID, itemID, quantity)
+SELECT @zombie, MIN(i.itemID), z.quantity
+FROM zombie_list z
+JOIN items i ON i.itemName IN (z.itemName, z.altName)
+GROUP BY z.itemName, z.quantity;
+
+DROP TEMPORARY TABLE zombie_list;

+ 1 - 7
packing.php

@@ -515,12 +515,6 @@ function nagTier(checked) {
   return 'close';
   return 'close';
 }
 }
 
 
-// "It's the Headlamp, isn't it" reads better as "the headlamp", but leave
-// acronyms like "GPS device" alone.
-function midSentence(name) {
-  return /^[A-Z][a-z]/.test(name) ? name[0].toLowerCase() + name.slice(1) : name;
-}
-
 function updateNag(checked) {
 function updateNag(checked) {
   const el = document.getElementById('nag');
   const el = document.getElementById('nag');
   if (total === 0) { el.textContent = ''; return; }
   if (total === 0) { el.textContent = ''; return; }
@@ -537,7 +531,7 @@ function updateNag(checked) {
   if (nag.text.includes('{item}')) {
   if (nag.text.includes('{item}')) {
     const open = Array.from(document.querySelectorAll('.item-row:not(.checked)'), r => r.dataset.name);
     const open = Array.from(document.querySelectorAll('.item-row:not(.checked)'), r => r.dataset.name);
     if (!open.includes(nag.item)) nag.item = open[Math.floor(Math.random() * open.length)];
     if (!open.includes(nag.item)) nag.item = open[Math.floor(Math.random() * open.length)];
-    item = midSentence(nag.item);
+    item = nag.item;   // as written: lowercasing would mangle German nouns
   }
   }
   el.textContent = nag.text.replace('{left}', total - checked).replace('{item}', item);
   el.textContent = nag.text.replace('{left}', total - checked).replace('{item}', item);
 }
 }

+ 29 - 29
schema.sql

@@ -87,11 +87,11 @@ INSERT INTO activity_groups (groupName, sortOrder) VALUES
 ('Rescue & Safety',  2),   -- groupID 2
 ('Rescue & Safety',  2),   -- groupID 2
 ('Water Activities', 3),   -- groupID 3
 ('Water Activities', 3),   -- groupID 3
 ('Winter Sports',    4),   -- groupID 4
 ('Winter Sports',    4),   -- groupID 4
-('Survival',         5);   -- groupID 5 (only hidden activities)
+('Überleben',        5);   -- groupID 5 (only hidden activities)
 
 
 -- activityID 1=Camping 2=Hiking 3=Bivouac 4=First Aid 5=Mountain Rescue
 -- activityID 1=Camping 2=Hiking 3=Bivouac 4=First Aid 5=Mountain Rescue
 -- activityID 6=Kayaking 7=Swimming 8=Skiing 9=Snowshoeing
 -- activityID 6=Kayaking 7=Swimming 8=Skiing 9=Snowshoeing
--- activityID 10=Zombie apocalypse (hidden easter egg)
+-- activityID 10=Zombie-Apokalypse (hidden easter egg)
 INSERT INTO activities (groupID, activityName, sortOrder, hidden) VALUES
 INSERT INTO activities (groupID, activityName, sortOrder, hidden) VALUES
 (1, 'Camping',           1, 0),
 (1, 'Camping',           1, 0),
 (1, 'Hiking',            2, 0),
 (1, 'Hiking',            2, 0),
@@ -102,7 +102,7 @@ INSERT INTO activities (groupID, activityName, sortOrder, hidden) VALUES
 (3, 'Swimming',          2, 0),
 (3, 'Swimming',          2, 0),
 (4, 'Skiing',            1, 0),
 (4, 'Skiing',            1, 0),
 (4, 'Snowshoeing',       2, 0),
 (4, 'Snowshoeing',       2, 0),
-(5, 'Zombie apocalypse', 1, 1);
+(5, 'Zombie-Apokalypse', 1, 1);
 
 
 INSERT INTO item_groups (groupName, sortOrder) VALUES
 INSERT INTO item_groups (groupName, sortOrder) VALUES
 ('Clothing',     1),   -- groupID 1
 ('Clothing',     1),   -- groupID 1
@@ -110,7 +110,8 @@ INSERT INTO item_groups (groupName, sortOrder) VALUES
 ('Navigation',   3),   -- groupID 3
 ('Navigation',   3),   -- groupID 3
 ('Medical',      4),   -- groupID 4
 ('Medical',      4),   -- groupID 4
 ('Water & Food', 5),   -- groupID 5
 ('Water & Food', 5),   -- groupID 5
-('Safety Gear',  6);   -- groupID 6
+('Safety Gear',  6),   -- groupID 6
+('Zombie-Abwehr', 7);  -- groupID 7 (Zombie-Apokalypse only)
 
 
 -- Items with their resulting auto_increment IDs:
 -- Items with their resulting auto_increment IDs:
 --  1  Base layer top        (Clothing)
 --  1  Base layer top        (Clothing)
@@ -148,15 +149,7 @@ INSERT INTO item_groups (groupName, sortOrder) VALUES
 -- 33  Shovel                (Safety Gear)
 -- 33  Shovel                (Safety Gear)
 -- 34  Life jacket           (Safety Gear)
 -- 34  Life jacket           (Safety Gear)
 -- 35  Paddle float          (Safety Gear)
 -- 35  Paddle float          (Safety Gear)
--- 36  Running shoes         (Clothing)      \
--- 37  Bite-proof arm guards (Clothing)       |
--- 38  Map with safe zones   (Navigation)     |
--- 39  Canned beans          (Water & Food)   |  Zombie apocalypse
--- 40  Manual can opener     (Water & Food)   |  only
--- 41  Cricket bat           (Safety Gear)    |
--- 42  Crowbar               (Safety Gear)    |
--- 43  Duct tape             (Safety Gear)    |
--- 44  Walkie-talkies        (Safety Gear)   /
+-- 36-53  Zombie-Abwehr items, used only by the hidden Zombie-Apokalypse
 
 
 INSERT INTO items (groupID, itemName, sortOrder) VALUES
 INSERT INTO items (groupID, itemName, sortOrder) VALUES
 (1, 'Base layer top',             1),
 (1, 'Base layer top',             1),
@@ -194,15 +187,24 @@ INSERT INTO items (groupID, itemName, sortOrder) VALUES
 (6, 'Shovel',                     6),
 (6, 'Shovel',                     6),
 (6, 'Life jacket',                7),
 (6, 'Life jacket',                7),
 (6, 'Paddle float',               8),
 (6, 'Paddle float',               8),
-(1, 'Running shoes (cardio is rule #1)', 12),
-(1, 'Bite-proof arm guards',     13),
-(3, 'Map with safe zones marked', 4),
-(5, 'Canned beans',               6),
-(5, 'Manual can opener',          7),
-(6, 'Cricket bat',                9),
-(6, 'Crowbar',                   10),
-(6, 'Duct tape',                 11),
-(6, 'Walkie-talkies',            12);
+(7, 'Laufschuhe (Regel Nr. 1: Cardio)',        1),   -- 36
+(7, 'Bissfeste Armschützer (Paar)',            2),   -- 37
+(7, 'Dosenbohnen',                             3),   -- 38
+(7, 'Twinkies (letzte Packung der Welt)',      4),   -- 39
+(7, 'Dosenöffner (manuell)',                   5),   -- 40
+(7, 'Feuerzeug',                               6),   -- 41
+(7, 'Funkgerät',                               7),   -- 42
+(7, 'Kurbelradio',                             8),   -- 43
+(7, 'Multitool',                               9),   -- 44
+(7, 'Karte mit sicheren Zonen',               10),   -- 45
+(7, 'Zombie-Wörterbuch (Grrr = Hallo)',       11),   -- 46
+(7, 'Glaubwürdige Ausrede für die Bisswunde', 12),   -- 47
+(7, 'Cricketschläger',                        13),   -- 48
+(7, 'Brecheisen',                             14),   -- 49
+(7, 'Panzertape',                             15),   -- 50
+(7, 'Bolzenschneider',                        16),   -- 51
+(7, 'Knoblauch (hilft nicht, beruhigt aber)', 17),   -- 52
+(7, 'Gartenzwerg zur Ablenkung',              18);   -- 53
 
 
 -- -----------------------------------------------
 -- -----------------------------------------------
 -- Activity -> Item mappings (all IDs verified)
 -- Activity -> Item mappings (all IDs verified)
@@ -278,14 +280,12 @@ INSERT INTO activity_item_map (activityID, itemID) VALUES
 (9,28),(9,29),
 (9,28),(9,29),
 (9,31);
 (9,31);
 
 
--- activityID 10: Zombie apocalypse (hidden)
+-- activityID 10: Zombie-Apokalypse (hidden)
 INSERT INTO activity_item_map (activityID, itemID, quantity) VALUES
 INSERT INTO activity_item_map (activityID, itemID, quantity) VALUES
-(10,36, 1),(10,37, 2),
-(10,13, 1),
-(10,38, 1),(10,17, 1),
-(10,19, 1),
-(10,23, 2),(10,24, 1),(10,39,12),(10,40, 1),
-(10,28, 1),(10,30, 1),(10,41, 1),(10,42, 1),(10,43, 1),(10,44, 2);
+(10,13, 1),(10,19, 1),(10,23, 2),(10,28, 1),
+(10,36, 1),(10,37, 1),(10,38,12),(10,39, 1),(10,40, 1),(10,41, 1),
+(10,42, 2),(10,43, 1),(10,44, 1),(10,45, 1),(10,46, 1),(10,47, 1),
+(10,48, 1),(10,49, 1),(10,50, 1),(10,51, 1),(10,52, 1),(10,53, 1);
 
 
 -- -----------------------------------------------
 -- -----------------------------------------------
 -- Default admin user
 -- Default admin user