1	// apex-kanban.js — Shared kanban logic for APEX Portfolio standards
2	// Used by: Standard1.html, Standard5.html
3
4	function initKanban(config) {
5	    const { cols, storageKey, sidebarKey, pageUrl, defaultCards } = config;
6	    let activeFilter = 'all';
7
8	    let _saveToastTimer = null;
9	    function showSaveToast() {
10	        const t = document.getElementById('apex-save-toast');
11	        if (!t) return;
12	        t.classList.add('show');
13	        clearTimeout(_saveToastTimer);
14	        _saveToastTimer = setTimeout(() => t.classList.remove('show'), 1400);
15	    }
16
17	    function saveToStorage(btn) {
18	        if (btn && btn.classList.contains('save-btn')) {
19	            btn.innerText = '✓ Saved';
20	            btn.classList.add('saved');
21	            setTimeout(() => { btn.innerText = 'Done'; btn.classList.remove('saved'); }, 2000);
22	        } else {
23	            showSaveToast();
24	        }
25	        const state = {};
26	        cols.forEach(col => {
27	            const cards = [];
28	            document.querySelectorAll(`#cards-${col} .sprint-card`).forEach(card => {
29	                cards.push({
30	                    id: card.dataset.id,
31	                    title: card.querySelector('.title-text').innerText,
32	                    brief: card.querySelector('.brief-text').innerText,
33	                    link: card.querySelector('.link-input').value,
34	                    reflection: card.querySelector('.reflection-textarea').value,
35	                    isCustom: card.dataset.custom === 'true',
36	                    done: card.dataset.done === 'true'
37	                });
38	            });
39	            state[col] = cards;
40	        });
41	        localStorage.setItem(storageKey, JSON.stringify(state));
42	    }
43
44	    function createCardHTML(col, cardData) {
45	        const isCustom = cardData.isCustom || String(cardData.id).startsWith('custom-') || String(cardData.id).startsWith('dup-');
46	        const isDone = cardData.done === true;
47	        const card = document.createElement('div');
48	        card.className = 'sprint-card';
49	        card.dataset.id = cardData.id;
50	        card.dataset.custom = isCustom;
51	        card.dataset.done = isDone ? 'true' : 'false';
52	        card.innerHTML = `
53

54	                <button class="bookmark-btn" onclick="bookmarkCard(this)">📌 Bookmark</button>
55	                <button class="duplicate-btn" onclick="duplicateCard(this, '${col}')">Duplicate</button>
56	                ${isCustom ? `<button class="delete-btn" onclick="if(confirm('Delete this artifact?')){this.closest('.sprint-card').remove(); saveToStorage(); updateProgress('${col}');}">✕ Delete</button>` : ''}
57

58

59	                ▶
60
${cardData.title}

61

62

63
64	                    Experience Brief
65
${cardData.brief || 'Describe the experience...'}

66
67
68	                    💭 Associate Reflection
69	                    <textarea class="reflection-textarea" placeholder="How does this demonstrate growth?" onblur="saveToStorage()">${cardData.reflection || ''}</textarea>
70
71

72

73	                        <input type="text" class="link-input" placeholder="Paste link here..." value="${cardData.link || ''}" onblur="saveToStorage()">
74	                        <button class="save-btn ${isDone ? 'saved-done' : ''}" onclick="markDone(this, '${col}')">${isDone ? '✓ Done' : 'Done'}</button>
75

76

77
`;
78	        return card;
79	    }
80
81	    function duplicateCard(btn, col) {
82	        const orig = btn.closest('.sprint-card');
83	        const newCard = createCardHTML(col, {
84	            id: 'dup-' + Date.now(),
85	            title: orig.querySelector('.title-text').innerText,
86	            brief: orig.querySelector('.brief-text').innerText,
87	            link: '', reflection: '', isCustom: true
88	        });
89	        orig.after(newCard);
90	        saveToStorage();
91	    }
92
93	    function addNewCard(col) {
94	        const container = document.getElementById(`cards-${col}`);
95	        const newCard = createCardHTML(col, {
96	            id: 'custom-' + Date.now(),
97	            title: '✨ Click to name your custom artifact',
98	            brief: 'Describe the experience here...',
99	            link: '', reflection: '', isCustom: true
100	        });
101	        container.appendChild(newCard);
102	        saveToStorage();
103	        activeFilter = 'all';
104	        document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
105	        document.querySelector('[data-filter="all"]').classList.add('active');
106	        document.getElementById('filterSearch').value = '';
107	        applyFilter();
108	    }
109
110	    function bookmarkCard(btn) {
111	        const card = btn.closest('.sprint-card');
112	        const title = card.querySelector('.title-text').innerText;
113	        const brief = card.querySelector('.brief-text').innerText;
114	        addToQueue(title, pageUrl, brief);
115	        btn.innerText = '✓ Bookmarked';
116	        btn.style.background = '#10B981';
117	        btn.style.color = 'white';
118	        setTimeout(() => {
119	            btn.innerText = '📌 Bookmark';
120	            btn.style.background = '#FEF3C7';
121	            btn.style.color = '#92400E';
122	        }, 2000);
123	    }
124
125	    function addToQueue(title, url, desc) {
126	        const queue = JSON.parse(localStorage.getItem('apex_work_queue') || '[]');
127	        queue.push({ id: Date.now().toString(), title, url, desc, addedAt: new Date().toISOString() });
128	        localStorage.setItem('apex_work_queue', JSON.stringify(queue));
129	    }
130
131	    function markDone(btn, col) {
132	        const card = btn.closest('.sprint-card');
133	        const isNowDone = card.dataset.done !== 'true';
134	        card.dataset.done = isNowDone ? 'true' : 'false';
135	        btn.innerText = isNowDone ? '✓ Done' : 'Done';
136	        btn.classList.toggle('saved-done', isNowDone);
137	        saveToStorage();
138	        updateProgress(col);
139	        applyFilter();
140	    }
141
142	    function updateProgress(col) {
143	        const cards = document.querySelectorAll(`#cards-${col} .sprint-card`);
144	        const doneCount = [...cards].filter(c => c.dataset.done === 'true').length;
145	        const pips = document.querySelectorAll(`#progress-${col} .progress-pip`);
146	        pips.forEach((pip, i) => pip.classList.toggle('lit', i < doneCount));
147	    }
148
149	    function toggleCard(headerEl) {
150	        const body = headerEl.closest('.sprint-card').querySelector('.card-body');
151	        const isOpen = headerEl.classList.contains('open');
152	        headerEl.classList.toggle('open', !isOpen);
153	        body.classList.toggle('open', !isOpen);
154	    }
155
156	    function setFilter(filter, btn) {
157	        activeFilter = filter;
158	        document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
159	        btn.classList.add('active');
160	        applyFilter();
161	    }
162
163	    function applyFilter() {
164	        const query = document.getElementById('filterSearch').value.toLowerCase().trim();
165	        cols.forEach(col => {
166	            let anyVisible = false;
167	            document.querySelectorAll(`#cards-${col} .sprint-card`).forEach(card => {
168	                const isDone = card.dataset.done === 'true';
169	                const link = card.querySelector('.link-input').value.trim();
170	                const reflection = card.querySelector('.reflection-textarea').value.trim();
171	                const title = card.querySelector('.title-text').innerText.toLowerCase();
172	                let statusMatch;
173	                if (activeFilter === 'all') statusMatch = true;
174	                else if (activeFilter === 'done') statusMatch = isDone;
175	                else if (activeFilter === 'in-progress') statusMatch = !isDone && (link !== '' || reflection !== '');
176	                else statusMatch = !isDone && link === '' && reflection === '';
177	                const show = statusMatch && (!query || title.includes(query));
178	                card.style.display = show ? '' : 'none';
179	                if (show) anyVisible = true;
180	            });
181	            const noMsg = document.querySelector(`[data-col="${col}"] .no-match-msg`);
182	            if (noMsg) noMsg.style.display = anyVisible ? 'none' : 'block';
183	        });
184	    }
185
186	    function toggleSidebar() {
187	        const sidebar = document.getElementById('sidebar');
188	        const isOpen = sidebar.classList.contains('open');
189	        sidebar.classList.toggle('open', !isOpen);
190	        document.body.classList.toggle('body-sidebar-open', !isOpen);
191	        localStorage.setItem(sidebarKey, !isOpen);
192	    }
193
194	    function loadQueue() {
195	        const queue = JSON.parse(localStorage.getItem('apex_work_queue') || '[]');
196	        displayQueue(queue);
197	    }
198
199	    function displayQueue(queue) {
200	        const list = document.getElementById('queueList');
201	        if (!list) return;
202	        if (queue.length === 0) {
203	            list.innerHTML = '
No items yet. Bookmark an artifact card to add it here.
';
204	            return;
205	        }
206	        list.innerHTML = queue.map(item => `
207

208	                ${item.title}
209
${item.desc || ''}

210	                <button class="remove-btn" onclick="removeFromQueue('${item.id}')">×</button>
211
`).join('');
212	    }
213
214	    function removeFromQueue(id) {
215	        const queue = JSON.parse(localStorage.getItem('apex_work_queue') || '[]');
216	        localStorage.setItem('apex_work_queue', JSON.stringify(queue.filter(item => item.id !== id)));
217	        displayQueue(JSON.parse(localStorage.getItem('apex_work_queue') || '[]'));
218	    }
219
220	    function init() {
221	        const saved = localStorage.getItem(storageKey);
222	        const state = saved ? JSON.parse(saved) : defaultCards;
223	        cols.forEach(col => {
224	            const container = document.getElementById(`cards-${col}`);
225	            if (!container) return;
226	            container.innerHTML = '';
227	            (state[col] || []).forEach(cardData => {
228	                if (cardData.brief === undefined) {
229	                    const def = (defaultCards[col] || []).find(c => c.id === cardData.id);
230	                    cardData.brief = def ? def.brief : 'Experience details...';
231	                }
232	                container.appendChild(createCardHTML(col, cardData));
233	            });
234	            updateProgress(col);
235	        });
236	        cols.forEach(col => {
237	            const msg = document.createElement('div');
238	            msg.className = 'no-match-msg';
239	            msg.textContent = 'No matching artifacts.';
240	            document.querySelector(`[data-col="${col}"] .add-btn`).before(msg);
241	        });
242	        loadQueue();
243	        if (localStorage.getItem(sidebarKey) === 'true') {
244	            document.getElementById('sidebar').classList.add('open');
245	            document.body.classList.add('body-sidebar-open');
246	        }
247	        applyFilter();
248
249	        // Save toast
250	        if (!document.getElementById('apex-save-toast')) {
251	            const s = document.createElement('style');
252	            s.textContent = '#apex-save-toast{position:fixed;bottom:20px;right:20px;z-index:9999;background:#059669;color:white;padding:7px 16px;border-radius:999px;font-size:.78rem;font-weight:700;font-family:inherit;pointer-events:none;opacity:0;transition:opacity .2s}#apex-save-toast.show{opacity:1}';
253	            document.head.appendChild(s);
254	            const t = document.createElement('div');
255	            t.id = 'apex-save-toast';
256	            t.textContent = 'Saved ✓';
257	            document.body.appendChild(t);
258	        }
259	    }
260
261	    // Expose as globals for HTML onclick attributes
262	    window.saveToStorage = saveToStorage;
263	    window.addNewCard = addNewCard;
264	    window.bookmarkCard = bookmarkCard;
265	    window.duplicateCard = duplicateCard;
266	    window.markDone = markDone;
267	    window.updateProgress = updateProgress;
268	    window.toggleCard = toggleCard;
269	    window.toggleSidebar = toggleSidebar;
270	    window.removeFromQueue = removeFromQueue;
271	    window.setFilter = setFilter;
272	    window.applyFilter = applyFilter;
273
274	    document.addEventListener('DOMContentLoaded', () => {
275	        const sb = document.getElementById('sidebarToggle');
276	        const cl = document.getElementById('closeSidebar');
277	        if (sb) sb.addEventListener('click', toggleSidebar);
278	        if (cl) cl.addEventListener('click', toggleSidebar);
279	        const fs = document.getElementById('filterSearch');
280	        if (fs) fs.addEventListener('input', applyFilter);
281	    });
282
283	    document.addEventListener('input', e => {
284	        if (e.target.classList.contains('reflection-textarea') || e.target.hasAttribute('contenteditable')) {
285	            saveToStorage();
286	        }
287	    });
288
289	    window.onload = init;
290	}
291

// apex-kanban.js — Shared kanban logic for APEX Portfolio standards
// Used by: Standard1.html, Standard5.html

function initKanban(config) {
    const { cols, storageKey, sidebarKey, pageUrl, defaultCards } = config;
    let activeFilter = 'all';

    let _saveToastTimer = null;
    function showSaveToast() {
        const t = document.getElementById('apex-save-toast');
        if (!t) return;
        t.classList.add('show');
        clearTimeout(_saveToastTimer);
        _saveToastTimer = setTimeout(() => t.classList.remove('show'), 1400);
    }

    function saveToStorage(btn) {
        if (btn && btn.classList.contains('save-btn')) {
            btn.innerText = '✓ Saved';
            btn.classList.add('saved');
            setTimeout(() => { btn.innerText = 'Done'; btn.classList.remove('saved'); }, 2000);
        } else {
            showSaveToast();
        }
        const state = {};
        cols.forEach(col => {
            const cards = [];
            document.querySelectorAll(`#cards-${col} .sprint-card`).forEach(card => {
                cards.push({
                    id: card.dataset.id,
                    title: card.querySelector('.title-text').innerText,
                    brief: card.querySelector('.brief-text').innerText,
                    link: card.querySelector('.link-input').value,
                    reflection: card.querySelector('.reflection-textarea').value,
                    isCustom: card.dataset.custom === 'true',
                    done: card.dataset.done === 'true'
                });
            });
            state[col] = cards;
        });
        localStorage.setItem(storageKey, JSON.stringify(state));
    }

    function createCardHTML(col, cardData) {
        const isCustom = cardData.isCustom || String(cardData.id).startsWith('custom-') || String(cardData.id).startsWith('dup-');
        const isDone = cardData.done === true;
        const card = document.createElement('div');
        card.className = 'sprint-card';
        card.dataset.id = cardData.id;
        card.dataset.custom = isCustom;
        card.dataset.done = isDone ? 'true' : 'false';
        card.innerHTML = `

                <button class="bookmark-btn" onclick="bookmarkCard(this)">📌 Bookmark</button>
                <button class="duplicate-btn" onclick="duplicateCard(this, '${col}')">Duplicate</button>
                ${isCustom ? `<button class="delete-btn" onclick="if(confirm('Delete this artifact?')){this.closest('.sprint-card').remove(); saveToStorage(); updateProgress('${col}');}">✕ Delete</button>` : ''}

                ▶

${cardData.title}

                    Experience Brief

${cardData.brief || 'Describe the experience...'}

                    💭 Associate Reflection
                    <textarea class="reflection-textarea" placeholder="How does this demonstrate growth?" onblur="saveToStorage()">${cardData.reflection || ''}</textarea>

                        <input type="text" class="link-input" placeholder="Paste link here..." value="${cardData.link || ''}" onblur="saveToStorage()">
                        <button class="save-btn ${isDone ? 'saved-done' : ''}" onclick="markDone(this, '${col}')">${isDone ? '✓ Done' : 'Done'}</button>

`;
        return card;
    }

    function duplicateCard(btn, col) {
        const orig = btn.closest('.sprint-card');
        const newCard = createCardHTML(col, {
            id: 'dup-' + Date.now(),
            title: orig.querySelector('.title-text').innerText,
            brief: orig.querySelector('.brief-text').innerText,
            link: '', reflection: '', isCustom: true
        });
        orig.after(newCard);
        saveToStorage();
    }

    function addNewCard(col) {
        const container = document.getElementById(`cards-${col}`);
        const newCard = createCardHTML(col, {
            id: 'custom-' + Date.now(),
            title: '✨ Click to name your custom artifact',
            brief: 'Describe the experience here...',
            link: '', reflection: '', isCustom: true
        });
        container.appendChild(newCard);
        saveToStorage();
        activeFilter = 'all';
        document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
        document.querySelector('[data-filter="all"]').classList.add('active');
        document.getElementById('filterSearch').value = '';
        applyFilter();
    }

    function bookmarkCard(btn) {
        const card = btn.closest('.sprint-card');
        const title = card.querySelector('.title-text').innerText;
        const brief = card.querySelector('.brief-text').innerText;
        addToQueue(title, pageUrl, brief);
        btn.innerText = '✓ Bookmarked';
        btn.style.background = '#10B981';
        btn.style.color = 'white';
        setTimeout(() => {
            btn.innerText = '📌 Bookmark';
            btn.style.background = '#FEF3C7';
            btn.style.color = '#92400E';
        }, 2000);
    }

    function addToQueue(title, url, desc) {
        const queue = JSON.parse(localStorage.getItem('apex_work_queue') || '[]');
        queue.push({ id: Date.now().toString(), title, url, desc, addedAt: new Date().toISOString() });
        localStorage.setItem('apex_work_queue', JSON.stringify(queue));
    }

    function markDone(btn, col) {
        const card = btn.closest('.sprint-card');
        const isNowDone = card.dataset.done !== 'true';
        card.dataset.done = isNowDone ? 'true' : 'false';
        btn.innerText = isNowDone ? '✓ Done' : 'Done';
        btn.classList.toggle('saved-done', isNowDone);
        saveToStorage();
        updateProgress(col);
        applyFilter();
    }

    function updateProgress(col) {
        const cards = document.querySelectorAll(`#cards-${col} .sprint-card`);
        const doneCount = [...cards].filter(c => c.dataset.done === 'true').length;
        const pips = document.querySelectorAll(`#progress-${col} .progress-pip`);
        pips.forEach((pip, i) => pip.classList.toggle('lit', i < doneCount));
    }

    function toggleCard(headerEl) {
        const body = headerEl.closest('.sprint-card').querySelector('.card-body');
        const isOpen = headerEl.classList.contains('open');
        headerEl.classList.toggle('open', !isOpen);
        body.classList.toggle('open', !isOpen);
    }

    function setFilter(filter, btn) {
        activeFilter = filter;
        document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
        btn.classList.add('active');
        applyFilter();
    }

    function applyFilter() {
        const query = document.getElementById('filterSearch').value.toLowerCase().trim();
        cols.forEach(col => {
            let anyVisible = false;
            document.querySelectorAll(`#cards-${col} .sprint-card`).forEach(card => {
                const isDone = card.dataset.done === 'true';
                const link = card.querySelector('.link-input').value.trim();
                const reflection = card.querySelector('.reflection-textarea').value.trim();
                const title = card.querySelector('.title-text').innerText.toLowerCase();
                let statusMatch;
                if (activeFilter === 'all') statusMatch = true;
                else if (activeFilter === 'done') statusMatch = isDone;
                else if (activeFilter === 'in-progress') statusMatch = !isDone && (link !== '' || reflection !== '');
                else statusMatch = !isDone && link === '' && reflection === '';
                const show = statusMatch && (!query || title.includes(query));
                card.style.display = show ? '' : 'none';
                if (show) anyVisible = true;
            });
            const noMsg = document.querySelector(`[data-col="${col}"] .no-match-msg`);
            if (noMsg) noMsg.style.display = anyVisible ? 'none' : 'block';
        });
    }

    function toggleSidebar() {
        const sidebar = document.getElementById('sidebar');
        const isOpen = sidebar.classList.contains('open');
        sidebar.classList.toggle('open', !isOpen);
        document.body.classList.toggle('body-sidebar-open', !isOpen);
        localStorage.setItem(sidebarKey, !isOpen);
    }

    function loadQueue() {
        const queue = JSON.parse(localStorage.getItem('apex_work_queue') || '[]');
        displayQueue(queue);
    }

    function displayQueue(queue) {
        const list = document.getElementById('queueList');
        if (!list) return;
        if (queue.length === 0) {
            list.innerHTML = '
No items yet. Bookmark an artifact card to add it here.
';
            return;
        }
        list.innerHTML = queue.map(item => `

                ${item.title}

${item.desc || ''}

                <button class="remove-btn" onclick="removeFromQueue('${item.id}')">×</button>

`).join('');
    }

    function removeFromQueue(id) {
        const queue = JSON.parse(localStorage.getItem('apex_work_queue') || '[]');
        localStorage.setItem('apex_work_queue', JSON.stringify(queue.filter(item => item.id !== id)));
        displayQueue(JSON.parse(localStorage.getItem('apex_work_queue') || '[]'));
    }

    function init() {
        const saved = localStorage.getItem(storageKey);
        const state = saved ? JSON.parse(saved) : defaultCards;
        cols.forEach(col => {
            const container = document.getElementById(`cards-${col}`);
            if (!container) return;
            container.innerHTML = '';
            (state[col] || []).forEach(cardData => {
                if (cardData.brief === undefined) {
                    const def = (defaultCards[col] || []).find(c => c.id === cardData.id);
                    cardData.brief = def ? def.brief : 'Experience details...';
                }
                container.appendChild(createCardHTML(col, cardData));
            });
            updateProgress(col);
        });
        cols.forEach(col => {
            const msg = document.createElement('div');
            msg.className = 'no-match-msg';
            msg.textContent = 'No matching artifacts.';
            document.querySelector(`[data-col="${col}"] .add-btn`).before(msg);
        });
        loadQueue();
        if (localStorage.getItem(sidebarKey) === 'true') {
            document.getElementById('sidebar').classList.add('open');
            document.body.classList.add('body-sidebar-open');
        }
        applyFilter();

        // Save toast
        if (!document.getElementById('apex-save-toast')) {
            const s = document.createElement('style');
            s.textContent = '#apex-save-toast{position:fixed;bottom:20px;right:20px;z-index:9999;background:#059669;color:white;padding:7px 16px;border-radius:999px;font-size:.78rem;font-weight:700;font-family:inherit;pointer-events:none;opacity:0;transition:opacity .2s}#apex-save-toast.show{opacity:1}';
            document.head.appendChild(s);
            const t = document.createElement('div');
            t.id = 'apex-save-toast';
            t.textContent = 'Saved ✓';
            document.body.appendChild(t);
        }
    }

    // Expose as globals for HTML onclick attributes
    window.saveToStorage = saveToStorage;
    window.addNewCard = addNewCard;
    window.bookmarkCard = bookmarkCard;
    window.duplicateCard = duplicateCard;
    window.markDone = markDone;
    window.updateProgress = updateProgress;
    window.toggleCard = toggleCard;
    window.toggleSidebar = toggleSidebar;
    window.removeFromQueue = removeFromQueue;
    window.setFilter = setFilter;
    window.applyFilter = applyFilter;

    document.addEventListener('DOMContentLoaded', () => {
        const sb = document.getElementById('sidebarToggle');
        const cl = document.getElementById('closeSidebar');
        if (sb) sb.addEventListener('click', toggleSidebar);
        if (cl) cl.addEventListener('click', toggleSidebar);
        const fs = document.getElementById('filterSearch');
        if (fs) fs.addEventListener('input', applyFilter);
    });

    document.addEventListener('input', e => {
        if (e.target.classList.contains('reflection-textarea') || e.target.hasAttribute('contenteditable')) {
            saveToStorage();
        }
    });

    window.onload = init;
}


Done. I even updated config.js on my own thank you very much.


Check memory persistence after extension restart


Now I have the full picture. Let me think about how to create `v2/js/apex-kanban-v2.js`:

The key insight: I want to keep the entire UI behavior identical, just add API sync on top.

Strategy:
1. The `saveToStorage` function currently writes to localStorage only. I'll extend it to also call `ApexAPI.savePlaybookProgress()` (using standard as the "playbook")
2. The `init` function currently reads from localStorage only. I'll extend it to check the API first and use whichever is fresher (based on a timestamp)

3. For the backend, I'll store the kanban state as JSON in `playbook_progress` with the standard ID as the playbook identifier and `portfolio` as the project
4. On initialization, I'll load from localStorage immediately to avoid any UI delay, then sync with the API in the background to catch any updates from other sessions
5. When saving, changes go to localStorage right away while API updates are debounced to 3 seconds to prevent excessive requests
6. I'll add an `_updatedAt` timestamp to the state so I can determine which version is fresher when syncing

Now I'm writing the apex-kanban-v2.js file... I'll base it on apex-kanban.js but add:
1. `standard` parameter in config
2. Async `syncFromAPI()` on init
3. Debounced `syncToAPI()` on save
4. Timestamp tracking on state

I'm setting up a debounced API sync that waits 3 seconds after each save before pushing state changes to the backend, and I'm modifying the storage function to track update timestamps and trigger that sync. I'm also adding an async function to fetch the latest state from the API on initialization and re-render if the remote version is fresher than what's stored locally.

Comparing timestamps to determine which version is more recent... Writing the sync function... Writing the v2 kanban file...

For the HTML file in v2/, I need to add the shared script imports at the top and update the initKanban call to pass the `standard` parameter, while keeping the body structure and styling exactly as it was in v1.

The nav should display the current user's name to indicate they're signed in, though the rest of the UI remains visually identical since the data syncing happens transparently in the background.

Now I need to read the existing Standard1.html file and adapt it for v2, making sure to update the script references, add the necessary config files, and adjust the relative paths since the v2 folder sits one level deeper in the directory structure.


Good. Now I'll write the v2 kanban engine — identical UI, adds backend sync on top — then Standard 1 as the reference implementation.


Write

// apex-kanban-v2.js — Kanban engine with backend sync
// Requires: config.js, auth.js, api.js (loaded before this file)
//
// Changes from v1:
//   - config now requires `standard` (e.g. 'S1')
//   - saveToStorage also queues an API write (debounced 3s)
//   - init() fetches from API after local render; re-renders if API is fresher
//   - state object gets a _updatedAt timestamp for cross-device conflict resolution

function initKanban(config) {
    const { cols, storageKey, sidebarKey, pageUrl, defaultCards, standard } = config;
    let activeFilter = 'all';

    // ── SAVE TOAST ────────────────────────────────────────────────────────────

    let _saveToastTimer = null;
    function showSaveToast() {
        const t = document.getElementById('apex-save-toast');
        if (!t) return;
        t.classList.add('show');
        clearTimeout(_saveToastTimer);
        _saveToastTimer = setTimeout(() => t.classList.remove('show'), 1400);
    }

    // ── API SYNC ──────────────────────────────────────────────────────────────

    let _syncTimer = null;

    function scheduleSyncToAPI(state) {
        clearTimeout(_syncTimer);
        _syncTimer = setTimeout(() => syncToAPI(state), 3000);
    }

    async function syncToAPI(state) {
        try {
            await ApexAPI.savePlaybookProgress(standard, 'portfolio', 'Portfolio', state);
        } catch(e) {
            console.warn('API sync failed (offline?):', e);
        }
    }

    async function syncFromAPI() {
        try {
            const remote = await ApexAPI.getPlaybookProgress(standard, 'portfolio');
            if (!remote || !remote.state_json) return;

            const remoteState = JSON.parse(remote.state_json);
            const localRaw = localStorage.getItem(storageKey);
            const localState = localRaw ? JSON.parse(localRaw) : null;

            const remoteTime = new Date(remoteState._updatedAt || 0);
            const localTime  = new Date((localState && localState._updatedAt) || 0);

            if (remoteTime > localTime) {
                localStorage.setItem(storageKey, JSON.stringify(remoteState));
                renderState(remoteState);
                showSaveToast();
            }
        } catch(e) {
            console.warn('API fetch failed (offline?):', e);
        }
    }

    // ── STORAGE ───────────────────────────────────────────────────────────────

    function saveToStorage(btn) {
        if (btn && btn.classList.contains('save-btn')) {
            btn.innerText = '✓ Saved';
            btn.classList.add('saved');
            setTimeout(() => { btn.innerText = 'Done'; btn.classList.remove('saved'); }, 2000);
        } else {
            showSaveToast();
        }
        const state = { _updatedAt: new Date().toISOString() };
        cols.forEach(col => {
            const cards = [];
            document.querySelectorAll(`#cards-${col} .sprint-card`).forEach(card => {
                cards.push({
                    id:         card.dataset.id,
                    title:      card.querySelector('.title-text').innerText,
                    brief:      card.querySelector('.brief-text').innerText,
                    link:       card.querySelector('.link-input').value,
                    reflection: card.querySelector('.reflection-textarea').value,
                    isCustom:   card.dataset.custom === 'true',
                    done:       card.dataset.done === 'true'
                });
            });
            state[col] = cards;
        });
        localStorage.setItem(storageKey, JSON.stringify(state));
        scheduleSyncToAPI(state);
    }

    // ── CARD RENDERING ────────────────────────────────────────────────────────

    function createCardHTML(col, cardData) {
        const isCustom = cardData.isCustom || String(cardData.id).startsWith('custom-') || String(cardData.id).startsWith('dup-');
        const isDone = cardData.done === true;
        const card = document.createElement('div');
        card.className = 'sprint-card';
        card.dataset.id = cardData.id;
        card.dataset.custom = isCustom;
        card.dataset.done = isDone ? 'true' : 'false';
        card.innerHTML = `

                <button class="bookmark-btn" onclick="bookmarkCard(this)">📌 Bookmark</button>
                <button class="duplicate-btn" onclick="duplicateCard(this, '${col}')">Duplicate</button>
                ${isCustom ? `<button class="delete-btn" onclick="if(confirm('Delete this artifact?')){this.closest('.sprint-card').remove(); saveToStorage(); updateProgress('${col}');}">✕ Delete</button>` : ''}

                ▶

${cardData.title}

                    Experience Brief

${cardData.brief || 'Describe the experience...'}

                    💭 Associate Reflection
                    <textarea class="reflection-textarea" placeholder="How does this demonstrate growth?" onblur="saveToStorage()">${cardData.reflection || ''}</textarea>

                        <input type="text" class="link-input" placeholder="Paste link here..." value="${cardData.link || ''}" onblur="saveToStorage()">
                        <button class="save-btn ${isDone ? 'saved-done' : ''}" onclick="markDone(this, '${col}')">${isDone ? '✓ Done' : 'Done'}</button>

`;
        return card;
    }

    function renderState(state) {
        cols.forEach(col => {
            const container = document.getElementById(`cards-${col}`);
            if (!container) return;
            container.innerHTML = '';
            (state[col] || []).forEach(cardData => {
                if (cardData.brief === undefined) {
                    const def = (defaultCards[col] || []).find(c => c.id === cardData.id);
                    cardData.brief = def ? def.brief : 'Experience details...';
                }
                container.appendChild(createCardHTML(col, cardData));
            });
            updateProgress(col);
        });
        applyFilter();
    }

    // ── CARD ACTIONS ──────────────────────────────────────────────────────────

    function duplicateCard(btn, col) {
        const orig = btn.closest('.sprint-card');
        const newCard = createCardHTML(col, {
            id: 'dup-' + Date.now(),
            title: orig.querySelector('.title-text').innerText,
            brief: orig.querySelector('.brief-text').innerText,
            link: '', reflection: '', isCustom: true
        });
        orig.after(newCard);
        saveToStorage();
    }

    function addNewCard(col) {
        const container = document.getElementById(`cards-${col}`);
        const newCard = createCardHTML(col, {
            id: 'custom-' + Date.now(),
            title: '✨ Click to name your custom artifact',
            brief: 'Describe the experience here...',
            link: '', reflection: '', isCustom: true
        });
        container.appendChild(newCard);
        saveToStorage();
        activeFilter = 'all';
        document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
        document.querySelector('[data-filter="all"]').classList.add('active');
        document.getElementById('filterSearch').value = '';
        applyFilter();
    }

    function bookmarkCard(btn) {
        const card = btn.closest('.sprint-card');
        const title = card.querySelector('.title-text').innerText;
        const brief = card.querySelector('.brief-text').innerText;
        addToQueue(title, pageUrl, brief);
        btn.innerText = '✓ Bookmarked';
        btn.style.background = '#10B981';
        btn.style.color = 'white';
        setTimeout(() => { btn.innerText = '📌 Bookmark'; btn.style.background = '#FEF3C7'; btn.style.color = '#92400E'; }, 2000);
    }

    function addToQueue(title, url, desc) {
        const queue = JSON.parse(localStorage.getItem('apex_work_queue') || '[]');
        queue.push({ id: Date.now().toString(), title, url, desc, addedAt: new Date().toISOString() });
        localStorage.setItem('apex_work_queue', JSON.stringify(queue));
    }

    function markDone(btn, col) {
        const card = btn.closest('.sprint-card');
        const isNowDone = card.dataset.done !== 'true';
        card.dataset.done = isNowDone ? 'true' : 'false';
        btn.innerText = isNowDone ? '✓ Done' : 'Done';
        btn.classList.toggle('saved-done', isNowDone);
        saveToStorage();
        updateProgress(col);
        applyFilter();
    }

    // ── PROGRESS ──────────────────────────────────────────────────────────────

    function updateProgress(col) {
        const cards = document.querySelectorAll(`#cards-${col} .sprint-card`);
        const doneCount = [...cards].filter(c => c.dataset.done === 'true').length;
        const pips = document.querySelectorAll(`#progress-${col} .progress-pip`);
        pips.forEach((pip, i) => pip.classList.toggle('lit', i < doneCount));
    }

    // ── UI HELPERS ────────────────────────────────────────────────────────────

    function toggleCard(headerEl) {
        const body = headerEl.closest('.sprint-card').querySelector('.card-body');
        const isOpen = headerEl.classList.contains('open');
        headerEl.classList.toggle('open', !isOpen);
        body.classList.toggle('open', !isOpen);
    }

    function setFilter(filter, btn) {
        activeFilter = filter;
        document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
        btn.classList.add('active');
        applyFilter();
    }

    function applyFilter() {
        const query = document.getElementById('filterSearch').value.toLowerCase().trim();
        cols.forEach(col => {
            let anyVisible = false;
            document.querySelectorAll(`#cards-${col} .sprint-card`).forEach(card => {
                const isDone = card.dataset.done === 'true';
                const link = card.querySelector('.link-input').value.trim();
                const reflection = card.querySelector('.reflection-textarea').value.trim();
                const title = card.querySelector('.title-text').innerText.toLowerCase();
                let statusMatch;
                if      (activeFilter === 'all')         statusMatch = true;
                else if (activeFilter === 'done')        statusMatch = isDone;
                else if (activeFilter === 'in-progress') statusMatch = !isDone && (link !== '' || reflection !== '');
                else                                     statusMatch = !isDone && link === '' && reflection === '';
                const show = statusMatch && (!query || title.includes(query));
                card.style.display = show ? '' : 'none';
                if (show) anyVisible = true;
            });
            const noMsg = document.querySelector(`[data-col="${col}"] .no-match-msg`);
            if (noMsg) noMsg.style.display = anyVisible ? 'none' : 'block';
        });
    }

    function toggleSidebar() {
        const sidebar = document.getElementById('sidebar');
        const isOpen = sidebar.classList.contains('open');
        sidebar.classList.toggle('open', !isOpen);
        document.body.classList.toggle('body-sidebar-open', !isOpen);
        localStorage.setItem(sidebarKey, !isOpen);
    }

    function loadQueue() {
        const queue = JSON.parse(localStorage.getItem('apex_work_queue') || '[]');
        displayQueue(queue);
    }

    function displayQueue(queue) {
        const list = document.getElementById('queueList');
        if (!list) return;
        if (queue.length === 0) {
            list.innerHTML = '
No items yet. Bookmark an artifact card to add it here.
';
            return;
        }
        list.innerHTML = queue.map(item => `

                ${item.title}

${item.desc || ''}

                <button class="remove-btn" onclick="removeFromQueue('${item.id}')">×</button>

`).join('');
    }

    function removeFromQueue(id) {
        const updated = JSON.parse(localStorage.getItem('apex_work_queue') || '[]').filter(i => i.id !== id);
        localStorage.setItem('apex_work_queue', JSON.stringify(updated));
        displayQueue(updated);
    }

    // ── INIT ──────────────────────────────────────────────────────────────────

    function init() {
        // 1. Render immediately from localStorage (no waiting)
        const saved = localStorage.getItem(storageKey);
        const state = saved ? JSON.parse(saved) : defaultCards;
        renderState(state);

        cols.forEach(col => {
            const msg = document.createElement('div');
            msg.className = 'no-match-msg';
            msg.textContent = 'No matching artifacts.';
            document.querySelector(`[data-col="${col}"] .add-btn`).before(msg);
        });

        loadQueue();
        if (localStorage.getItem(sidebarKey) === 'true') {
            document.getElementById('sidebar').classList.add('open');
            document.body.classList.add('body-sidebar-open');
        }
        applyFilter();

        // Save toast element
        if (!document.getElementById('apex-save-toast')) {
            const s = document.createElement('style');
            s.textContent = '#apex-save-toast{position:fixed;bottom:20px;right:20px;z-index:9999;background:#059669;color:white;padding:7px 16px;border-radius:999px;font-size:.78rem;font-weight:700;font-family:inherit;pointer-events:none;opacity:0;transition:opacity .2s}#apex-save-toast.show{opacity:1}';
            document.head.appendChild(s);
            const t = document.createElement('div');
            t.id = 'apex-save-toast';
            t.textContent = 'Saved ✓';
            document.body.appendChild(t);
        }

        // 2. Register user in backend (fire and forget)
        const user = getCurrentUser();
        ApexAPI.ensureUser(user);

        // 3. Async: fetch from API and re-render if fresher
        syncFromAPI();
    }

    // ── EXPOSE GLOBALS ────────────────────────────────────────────────────────

    window.saveToStorage  = saveToStorage;
    window.addNewCard     = addNewCard;
    window.bookmarkCard   = bookmarkCard;
    window.duplicateCard  = duplicateCard;
    window.markDone       = markDone;
    window.updateProgress = updateProgress;
    window.toggleCard     = toggleCard;
    window.toggleSidebar  = toggleSidebar;
    window.removeFromQueue= removeFromQueue;
    window.setFilter      = setFilter;
    window.applyFilter    = applyFilter;

    document.addEventListener('DOMContentLoaded', () => {
        const sb = document.getElementById('sidebarToggle');
        const cl = document.getElementById('closeSidebar');
        if (sb) sb.addEventListener('click', toggleSidebar);
        if (cl) cl.addEventListener('click', toggleSidebar);
        const fs = document.getElementById('filterSearch');
        if (fs) fs.addEventListener('input', applyFilter);
    });

    document.addEventListener('input', e => {
        if (e.target.classList.contains('reflection-textarea') || e.target.hasAttribute('contenteditable')) {
            saveToStorage();
        }
    });

    window.onload = init;
}


File created successfully at: /Users/russgoerend/Desktop/Portfolio HTMLs/v2/js/apex-kanban-v2.js (file state is current in your context — no need to Read it back)

// apex-kanban-v2.js — Kanban engine with backend sync
// Requires: config.js, auth.js, api.js (loaded before this file)
//
// Changes from v1:
//   - config now requires `standard` (e.g. 'S1')
//   - saveToStorage also queues an API write (debounced 3s)
//   - init() fetches from API after local render; re-renders if API is fresher
//   - state object gets a _updatedAt timestamp for cross-device conflict resolution

function initKanban(config) {
    const { cols, storageKey, sidebarKey, pageUrl, defaultCards, standard } = config;
    let activeFilter = 'all';

    // ── SAVE TOAST ────────────────────────────────────────────────────────────

    let _saveToastTimer = null;
    function showSaveToast() {
        const t = document.getElementById('apex-save-toast');
        if (!t) return;
        t.classList.add('show');
        clearTimeout(_saveToastTimer);
        _saveToastTimer = setTimeout(() => t.classList.remove('show'), 1400);
    }

    // ── API SYNC ──────────────────────────────────────────────────────────────

    let _syncTimer = null;

    function scheduleSyncToAPI(state) {
        clearTimeout(_syncTimer);
        _syncTimer = setTimeout(() => syncToAPI(state), 3000);
    }

    async function syncToAPI(state) {
        try {
            await ApexAPI.savePlaybookProgress(standard, 'portfolio', 'Portfolio', state);
        } catch(e) {
            console.warn('API sync failed (offline?):', e);
        }
    }

    async function syncFromAPI() {
        try {
            const remote = await ApexAPI.getPlaybookProgress(standard, 'portfolio');
            if (!remote || !remote.state_json) return;

            const remoteState = JSON.parse(remote.state_json);
            const localRaw = localStorage.getItem(storageKey);
            const localState = localRaw ? JSON.parse(localRaw) : null;

            const remoteTime = new Date(remoteState._updatedAt || 0);
            const localTime  = new Date((localState && localState._updatedAt) || 0);

            if (remoteTime > localTime) {
                localStorage.setItem(storageKey, JSON.stringify(remoteState));
                renderState(remoteState);
                showSaveToast();
            }
        } catch(e) {
            console.warn('API fetch failed (offline?):', e);
        }
    }

    // ── STORAGE ───────────────────────────────────────────────────────────────

    function saveToStorage(btn) {
        if (btn && btn.classList.contains('save-btn')) {
            btn.innerText = '✓ Saved';
            btn.classList.add('saved');
            setTimeout(() => { btn.innerText = 'Done'; btn.classList.remove('saved'); }, 2000);
        } else {
            showSaveToast();
        }
        const state = { _updatedAt: new Date().toISOString() };
        cols.forEach(col => {
            const cards = [];
            document.querySelectorAll(`#cards-${col} .sprint-card`).forEach(card => {
                cards.push({
                    id:         card.dataset.id,
                    title:      card.querySelector('.title-text').innerText,
                    brief:      card.querySelector('.brief-text').innerText,
                    link:       card.querySelector('.link-input').value,
                    reflection: card.querySelector('.reflection-textarea').value,
                    isCustom:   card.dataset.custom === 'true',
                    done:       card.dataset.done === 'true'
                });
            });
            state[col] = cards;
        });
        localStorage.setItem(storageKey, JSON.stringify(state));
        scheduleSyncToAPI(state);
    }

    // ── CARD RENDERING ────────────────────────────────────────────────────────

    function createCardHTML(col, cardData) {
        const isCustom = cardData.isCustom || String(cardData.id).startsWith('custom-') || String(cardData.id).startsWith('dup-');
        const isDone = cardData.done === true;
        const card = document.createElement('div');
        card.className = 'sprint-card';
        card.dataset.id = cardData.id;
        card.dataset.custom = isCustom;
        card.dataset.done = isDone ? 'true' : 'false';
        card.innerHTML = `

                <button class="bookmark-btn" onclick="bookmarkCard(this)">📌 Bookmark</button>
                <button class="duplicate-btn" onclick="duplicateCard(this, '${col}')">Duplicate</button>
                ${isCustom ? `<button class="delete-btn" onclick="if(confirm('Delete this artifact?')){this.closest('.sprint-card').remove(); saveToStorage(); updateProgress('${col}');}">✕ Delete</button>` : ''}

                ▶

${cardData.title}

                    Experience Brief

${cardData.brief || 'Describe the experience...'}

                    💭 Associate Reflection
                    <textarea class="reflection-textarea" placeholder="How does this demonstrate growth?" onblur="saveToStorage()">${cardData.reflection || ''}</textarea>

                        <input type="text" class="link-input" placeholder="Paste link here..." value="${cardData.link || ''}" onblur="saveToStorage()">
                        <button class="save-btn ${isDone ? 'saved-done' : ''}" onclick="markDone(this, '${col}')">${isDone ? '✓ Done' : 'Done'}</button>

`;
        return card;
    }

    function renderState(state) {
        cols.forEach(col => {
            const container = document.getElementById(`cards-${col}`);
            if (!container) return;
            container.innerHTML = '';
            (state[col] || []).forEach(cardData => {
                if (cardData.brief === undefined) {
                    const def = (defaultCards[col] || []).find(c => c.id === cardData.id);
                    cardData.brief = def ? def.brief : 'Experience details...';
                }
                container.appendChild(createCardHTML(col, cardData));
            });
            updateProgress(col);
        });
        applyFilter();
    }

    // ── CARD ACTIONS ──────────────────────────────────────────────────────────

    function duplicateCard(btn, col) {
        const orig = btn.closest('.sprint-card');
        const newCard = createCardHTML(col, {
            id: 'dup-' + Date.now(),
            title: orig.querySelector('.title-text').innerText,
            brief: orig.querySelector('.brief-text').innerText,
            link: '', reflection: '', isCustom: true
        });
        orig.after(newCard);
        saveToStorage();
    }

    function addNewCard(col) {
        const container = document.getElementById(`cards-${col}`);
        const newCard = createCardHTML(col, {
            id: 'custom-' + Date.now(),
            title: '✨ Click to name your custom artifact',
            brief: 'Describe the experience here...',
            link: '', reflection: '', isCustom: true
        });
        container.appendChild(newCard);
        saveToStorage();
        activeFilter = 'all';
        document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
        document.querySelector('[data-filter="all"]').classList.add('active');
        document.getElementById('filterSearch').value = '';
        applyFilter();
    }

    function bookmarkCard(btn) {
        const card = btn.closest('.sprint-card');
        const title = card.querySelector('.title-text').innerText;
        const brief = card.querySelector('.brief-text').innerText;
        addToQueue(title, pageUrl, brief);
        btn.innerText = '✓ Bookmarked';
        btn.style.background = '#10B981';
        btn.style.color = 'white';
        setTimeout(() => { btn.innerText = '📌 Bookmark'; btn.style.background = '#FEF3C7'; btn.style.color = '#92400E'; }, 2000);
    }

    function addToQueue(title, url, desc) {
        const queue = JSON.parse(localStorage.getItem('apex_work_queue') || '[]');
        queue.push({ id: Date.now().toString(), title, url, desc, addedAt: new Date().toISOString() });
        localStorage.setItem('apex_work_queue', JSON.stringify(queue));
    }

    function markDone(btn, col) {
        const card = btn.closest('.sprint-card');
        const isNowDone = card.dataset.done !== 'true';
        card.dataset.done = isNowDone ? 'true' : 'false';
        btn.innerText = isNowDone ? '✓ Done' : 'Done';
        btn.classList.toggle('saved-done', isNowDone);
        saveToStorage();
        updateProgress(col);
        applyFilter();
    }

    // ── PROGRESS ──────────────────────────────────────────────────────────────

    function updateProgress(col) {
        const cards = document.querySelectorAll(`#cards-${col} .sprint-card`);
        const doneCount = [...cards].filter(c => c.dataset.done === 'true').length;
        const pips = document.querySelectorAll(`#progress-${col} .progress-pip`);
        pips.forEach((pip, i) => pip.classList.toggle('lit', i < doneCount));
    }

    // ── UI HELPERS ────────────────────────────────────────────────────────────

    function toggleCard(headerEl) {
        const body = headerEl.closest('.sprint-card').querySelector('.card-body');
        const isOpen = headerEl.classList.contains('open');
        headerEl.classList.toggle('open', !isOpen);
        body.classList.toggle('open', !isOpen);
    }

    function setFilter(filter, btn) {
        activeFilter = filter;
        document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
        btn.classList.add('active');
        applyFilter();
    }

    function applyFilter() {
        const query = document.getElementById('filterSearch').value.toLowerCase().trim();
        cols.forEach(col => {
            let anyVisible = false;
            document.querySelectorAll(`#cards-${col} .sprint-card`).forEach(card => {
                const isDone = card.dataset.done === 'true';
                const link = card.querySelector('.link-input').value.trim();
                const reflection = card.querySelector('.reflection-textarea').value.trim();
                const title = card.querySelector('.title-text').innerText.toLowerCase();
                let statusMatch;
                if      (activeFilter === 'all')         statusMatch = true;
                else if (activeFilter === 'done')        statusMatch = isDone;
                else if (activeFilter === 'in-progress') statusMatch = !isDone && (link !== '' || reflection !== '');
                else                                     statusMatch = !isDone && link === '' && reflection === '';
                const show = statusMatch && (!query || title.includes(query));
                card.style.display = show ? '' : 'none';
                if (show) anyVisible = true;
            });
            const noMsg = document.querySelector(`[data-col="${col}"] .no-match-msg`);
            if (noMsg) noMsg.style.display = anyVisible ? 'none' : 'block';
        });
    }

    function toggleSidebar() {
        const sidebar = document.getElementById('sidebar');
        const isOpen = sidebar.classList.contains('open');
        sidebar.classList.toggle('open', !isOpen);
        document.body.classList.toggle('body-sidebar-open', !isOpen);
        localStorage.setItem(sidebarKey, !isOpen);
    }

    function loadQueue() {
        const queue = JSON.parse(localStorage.getItem('apex_work_queue') || '[]');
        displayQueue(queue);
    }

    function displayQueue(queue) {
        const list = document.getElementById('queueList');
        if (!list) return;
        if (queue.length === 0) {
            list.innerHTML = '
No items yet. Bookmark an artifact card to add it here.
';
            return;
        }
        list.innerHTML = queue.map(item => `

                ${item.title}

${item.desc || ''}

                <button class="remove-btn" onclick="removeFromQueue('${item.id}')">×</button>

`).join('');
    }

    function removeFromQueue(id) {
        const updated = JSON.parse(localStorage.getItem('apex_work_queue') || '[]').filter(i => i.id !== id);
        localStorage.setItem('apex_work_queue', JSON.stringify(updated));
        displayQueue(updated);
    }

    // ── INIT ──────────────────────────────────────────────────────────────────

    function init() {
        // 1. Render immediately from localStorage (no waiting)
        const saved = localStorage.getItem(storageKey);
        const state = saved ? JSON.parse(saved) : defaultCards;
        renderState(state);

        cols.forEach(col => {
            const msg = document.createElement('div');
            msg.className = 'no-match-msg';
            msg.textContent = 'No matching artifacts.';
            document.querySelector(`[data-col="${col}"] .add-btn`).before(msg);
        });

        loadQueue();
        if (localStorage.getItem(sidebarKey) === 'true') {
            document.getElementById('sidebar').classList.add('open');
            document.body.classList.add('body-sidebar-open');
        }
        applyFilter();

        // Save toast element
        if (!document.getElementById('apex-save-toast')) {
            const s = document.createElement('style');
            s.textContent = '#apex-save-toast{position:fixed;bottom:20px;right:20px;z-index:9999;background:#059669;color:white;padding:7px 16px;border-radius:999px;font-size:.78rem;font-weight:700;font-family:inherit;pointer-events:none;opacity:0;transition:opacity .2s}#apex-save-toast.show{opacity:1}';
            document.head.appendChild(s);
            const t = document.createElement('div');
            t.id = 'apex-save-toast';
            t.textContent = 'Saved ✓';
            document.body.appendChild(t);
        }

        // 2. Register user in backend (fire and forget)
        const user = getCurrentUser();
        ApexAPI.ensureUser(user);

        // 3. Async: fetch from API and re-render if fresher
        syncFromAPI();
    }

    // ── EXPOSE GLOBALS ────────────────────────────────────────────────────────

    window.saveToStorage  = saveToStorage;
    window.addNewCard     = addNewCard;
    window.bookmarkCard   = bookmarkCard;
    window.duplicateCard  = duplicateCard;
    window.markDone       = markDone;
    window.updateProgress = updateProgress;
    window.toggleCard     = toggleCard;
    window.toggleSidebar  = toggleSidebar;
    window.removeFromQueue= removeFromQueue;
    window.setFilter      = setFilter;
    window.applyFilter    = applyFilter;

    document.addEventListener('DOMContentLoaded', () => {
        const sb = document.getElementById('sidebarToggle');
        const cl = document.getElementById('closeSidebar');
        if (sb) sb.addEventListener('click', toggleSidebar);
        if (cl) cl.addEventListener('click', toggleSidebar);
        const fs = document.getElementById('filterSearch');
        if (fs) fs.addEventListener('input', applyFilter);
    });

    document.addEventListener('input', e => {
        if (e.target.classList.contains('reflection-textarea') || e.target.hasAttribute('contenteditable')) {
            saveToStorage();
        }
    });

    window.onload = init;
}


Now read the v1 Standard1 HTML to grab the exact structure before building v2:


340	            transition: all 0.2s;
341	            flex-shrink: 0;
342	        }
343	        .nav-checkin-btn:hover { background: #d4e800; }
344	            .nav-beta-btn {
345	            background: rgba(255,255,255,0.15);
346	            color: rgba(255,255,255,0.95) !important;
347	            border: 1px solid rgba(255,255,255,0.3);
348	            border-radius: 6px;
349	            font-weight: 700 !important;
350	            padding: 7px 13px;
351	            text-decoration: none;
352	            font-size: 0.78rem;
353	            letter-spacing: 0.03em;
354	            text-transform: uppercase;
355	            transition: all 0.2s;
356	            flex-shrink: 0;
357	        }
358	        .nav-beta-btn:hover { background: rgba(255,255,255,0.25) !important; }
359	    </style>
360
361
362
363

364

365
📌 Work Queue

366	        <button id="closeSidebar">×</button>
367

368

369
No items yet. Bookmark an artifact card to add it here.

370

371

372
373
374

375
376
377
378

379	            Dashboard
380

381	                <button class="nav-drop-btn active">Portfolio ▾</button>
382

383	                    S1 · Servant Leadership
384	                    S2 · Beyond GPA
385	                    S3 · Career Exploration
386	                    S4 · Technical Skills
387	                    S5 · Professional Skills
388

389

390

391	                <button class="nav-drop-btn">Playbooks ▾</button>
392

393	                    DCS Onboarding Project
394	                    BSU Onboarding Project
395	                    Video Production
396	                    Social Media
397	                    Marketing Analytics
398	                    Podcast Production
399	                    Graphic Design
400	                    Photography
401

402

403	            📋 Check-In
404	            🧪 Beta
405	            <button id="sidebarToggle" style="color:rgba(255,255,255,0.9);font-size:0.8rem;font-weight:700;padding:8px 14px;border-radius:6px;text-transform:uppercase;letter-spacing:0.03em;transition:all 0.2s;background:none;border:none;cursor:pointer;">📌 Queue</button>
406

407

408
409
410

411
412
Standard 1: Servant Leadership & Client Relationships

413
Documenting professional growth, client communication, and community impact.

414
415
416
417	        📊 S1 Servant Leadership Proficiency Indicators
418

419

420	                Exceeds
421	                Associate has displayed immense involvement throughout the semester, far beyond the expectations that have been laid out by the instructor.
422

423

424	                Secure
425	                Takes roles and responsibilities in project work seriously and is actively engaged. Clearly models AGILE methodologies.
426

427

428	                Developing
429	                Occasionally takes initiative on projects but is not always actively engaged. Doesn't always seek feedback.
430

431

432	                Beginning
433	                Does not participate in project work except for meetings. Cannot communicate project goals.
434

435

436
437
438

439

440
🏗 Portfolio Strands

441
The columns below represent the specific areas of focus for this standard. Collect evidence for each strand to build your overall proficiency rating.

442

443

444

445	                1. Choose
446	                Select a card that matches a recent task or experience.
447

448

449	                2. Link & Reflect
450	                Add your artifact link and write a brief growth reflection.
451

452

453	                3. Add Custom
454	                Use the "+ Add" button to document unique experiences.
455

456

457

458
459

460

461	            <button class="chip active" data-filter="all" onclick="setFilter('all', this)">All</button>
462	            <button class="chip" data-filter="not-started" onclick="setFilter('not-started', this)">Not Started</button>
463	            <button class="chip" data-filter="in-progress" onclick="setFilter('in-progress', this)">In Progress</button>
464	            <button class="chip" data-filter="done" onclick="setFilter('done', this)">Done</button>
465

466	        <input type="text" id="filterSearch" placeholder="Search artifacts...">
467

468
469

470

471
🤲🌟 Servant Leadership

472

473

474

475

476	            <button class="add-btn" onclick="addNewCard('servant')">+ Add Artifact Card</button>
477

478
479

480
🤝💼 Client Relationships

481

482

483

484

485	            <button class="add-btn" onclick="addNewCard('client')">+ Add Artifact Card</button>
486

487
488

489
👥🏗 Team Building

490

491

492

493

494	            <button class="add-btn" onclick="addNewCard('team')">+ Add Artifact Card</button>
495

496
497

498
📣🎯 Brand Ambassador

499

500

501

502

503	            <button class="add-btn" onclick="addNewCard('brand')">+ Add Artifact Card</button>
504

505

506

507
508
509

transition: all 0.2s;
            flex-shrink: 0;
        }
        .nav-checkin-btn:hover { background: #d4e800; }
            .nav-beta-btn {
            background: rgba(255,255,255,0.15);
            color: rgba(255,255,255,0.95) !important;
            border: 1px solid rgba(255,255,255,0.3);
            border-radius: 6px;
            font-weight: 700 !important;
            padding: 7px 13px;
            text-decoration: none;
            font-size: 0.78rem;
            letter-spacing: 0.03em;
            text-transform: uppercase;
            transition: all 0.2s;
            flex-shrink: 0;
        }
        .nav-beta-btn:hover { background: rgba(255,255,255,0.25) !important; }
    </style>

📌 Work Queue

        <button id="closeSidebar">×</button>

No items yet. Bookmark an artifact card to add it here.

            Dashboard

                <button class="nav-drop-btn active">Portfolio ▾</button>

                    S1 · Servant Leadership
                    S2 · Beyond GPA
                    S3 · Career Exploration
                    S4 · Technical Skills
                    S5 · Professional Skills

                <button class="nav-drop-btn">Playbooks ▾</button>

                    DCS Onboarding Project
                    BSU Onboarding Project
                    Video Production
                    Social Media
                    Marketing Analytics
                    Podcast Production
                    Graphic Design
                    Photography

            📋 Check-In
            🧪 Beta
            <button id="sidebarToggle" style="color:rgba(255,255,255,0.9);font-size:0.8rem;font-weight:700;padding:8px 14px;border-radius:6px;text-transform:uppercase;letter-spacing:0.03em;transition:all 0.2s;background:none;border:none;cursor:pointer;">📌 Queue</button>

Standard 1: Servant Leadership & Client Relationships

Documenting professional growth, client communication, and community impact.

        📊 S1 Servant Leadership Proficiency Indicators

                Exceeds
                Associate has displayed immense involvement throughout the semester, far beyond the expectations that have been laid out by the instructor.

                Secure
                Takes roles and responsibilities in project work seriously and is actively engaged. Clearly models AGILE methodologies.

                Developing
                Occasionally takes initiative on projects but is not always actively engaged. Doesn't always seek feedback.

                Beginning
                Does not participate in project work except for meetings. Cannot communicate project goals.

🏗 Portfolio Strands

The columns below represent the specific areas of focus for this standard. Collect evidence for each strand to build your overall proficiency rating.

                1. Choose
                Select a card that matches a recent task or experience.

                2. Link & Reflect
                Add your artifact link and write a brief growth reflection.

                3. Add Custom
                Use the "+ Add" button to document unique experiences.

            <button class="chip active" data-filter="all" onclick="setFilter('all', this)">All</button>
            <button class="chip" data-filter="not-started" onclick="setFilter('not-started', this)">Not Started</button>
            <button class="chip" data-filter="in-progress" onclick="setFilter('in-progress', this)">In Progress</button>
            <button class="chip" data-filter="done" onclick="setFilter('done', this)">Done</button>

        <input type="text" id="filterSearch" placeholder="Search artifacts...">

🤲🌟 Servant Leadership

            <button class="add-btn" onclick="addNewCard('servant')">+ Add Artifact Card</button>

🤝💼 Client Relationships

            <button class="add-btn" onclick="addNewCard('client')">+ Add Artifact Card</button>

👥🏗 Team Building

            <button class="add-btn" onclick="addNewCard('team')">+ Add Artifact Card</button>

📣🎯 Brand Ambassador

            <button class="add-btn" onclick="addNewCard('brand')">+ Add Artifact Card</button>


Now I have everything I need. I can see the full structure of Standard1.html. The key differences for v2 will be:
1. Include the three shared JS files before the kanban script
2. Use `apex-kanban-v2.js` instead of `apex-kanban.js`
3. Add `standard: 'S1'` to the initKanban config
4. Update nav links to work from v2/ subfolder (most link to `../` for v1 pages that don't have v2 versions yet, but Standard pages link to each other within v2/)

Actually, for the reference implementation, since only Standard1 exists in v2/ initially, the other Standard nav links should still point to v1.

For now I'll keep the nav links pointing to `../` which takes you back to v1, and when each v2 page is ready, I'll update those links. The nav dropdown will still show S1 · Servant Leadership as active, pointing to Standard1.html within v2/. Let me grab the CSS and the initKanban call with the defaultCards configuration.