Просмотр исходного кода

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 недель назад
Родитель
Сommit
3badc35adf
4 измененных файлов с 119 добавлено и 39 удалено
  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.
 - **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.
-- **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.
 - **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.
@@ -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/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.
 
-`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
 

+ 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';
 }
 
-// "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) {
   const el = document.getElementById('nag');
   if (total === 0) { el.textContent = ''; return; }
@@ -537,7 +531,7 @@ function updateNag(checked) {
   if (nag.text.includes('{item}')) {
     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)];
-    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);
 }

+ 29 - 29
schema.sql

@@ -87,11 +87,11 @@ INSERT INTO activity_groups (groupName, sortOrder) VALUES
 ('Rescue & Safety',  2),   -- groupID 2
 ('Water Activities', 3),   -- groupID 3
 ('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 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
 (1, 'Camping',           1, 0),
 (1, 'Hiking',            2, 0),
@@ -102,7 +102,7 @@ INSERT INTO activities (groupID, activityName, sortOrder, hidden) VALUES
 (3, 'Swimming',          2, 0),
 (4, 'Skiing',            1, 0),
 (4, 'Snowshoeing',       2, 0),
-(5, 'Zombie apocalypse', 1, 1);
+(5, 'Zombie-Apokalypse', 1, 1);
 
 INSERT INTO item_groups (groupName, sortOrder) VALUES
 ('Clothing',     1),   -- groupID 1
@@ -110,7 +110,8 @@ INSERT INTO item_groups (groupName, sortOrder) VALUES
 ('Navigation',   3),   -- groupID 3
 ('Medical',      4),   -- groupID 4
 ('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:
 --  1  Base layer top        (Clothing)
@@ -148,15 +149,7 @@ INSERT INTO item_groups (groupName, sortOrder) VALUES
 -- 33  Shovel                (Safety Gear)
 -- 34  Life jacket           (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
 (1, 'Base layer top',             1),
@@ -194,15 +187,24 @@ INSERT INTO items (groupID, itemName, sortOrder) VALUES
 (6, 'Shovel',                     6),
 (6, 'Life jacket',                7),
 (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)
@@ -278,14 +280,12 @@ INSERT INTO activity_item_map (activityID, itemID) VALUES
 (9,28),(9,29),
 (9,31);
 
--- activityID 10: Zombie apocalypse (hidden)
+-- activityID 10: Zombie-Apokalypse (hidden)
 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