How to add an audio to a DOM button?

I am trying to enhance my Js knowledge by building a simple music player in HTML, JS and Tailwind CSS.

I am trying to add the DOM buttons to all of the audios. I expect the buttons to play the audio if you click on it. Although, it does not play an audio and it does not show any errors. I don’t understand what is wrong with my code.

My Js code:


const allSongs = [
    {
    id: 0,
    title: "Scratching The Surface",
    artist: "Quincy Larson",
    duration: "4:25",
    src:  //here is a link
  },
  {
    id: 1,
    title: "Can't Stay Down",
    artist: "Quincy Larson",
    duration: "4:15",
    src:  //here is a link
  },
  {
    id: 2,
    title: "Still Learning",
    artist: "Quincy Larson",
    duration: "3:51",
    src:  //here is a link
  },
  {
    id: 3,
    title: "Cruising for a Musing",
    artist: "Quincy Larson",
    duration: "3:34",
    src: //here is a link
  },
  {
    id: 4,
    title: "Never Not Favored",
    artist: "Quincy Larson",
    duration: "3:35",
    src: //here is a link
  },
  {
    id: 5,
    title: "From the Ground Up",
    artist: "Quincy Larson",
    duration: "3:12",
    src: //here is a link
  },
  {
    id: 6,
    title: "Walking on Air",
    artist: "Quincy Larson",
    duration: "3:25",
    src: //here is a link
  },
  {
    id: 7,
    title: "Can't Stop Me. Can't Even Slow Me Down.",
    artist: "Quincy Larson",
    duration: "3:52",
    src: //here is a link
  },
  {
    id: 8,
    title: "The Surest Way Out is Through",
    artist: "Quincy Larson",
    duration: "3:10",
    src: //here is a link
  },
  {
    id: 9,
    title: "Chasing That Feeling",
    artist: "Quincy Larson",
    duration: "2:43",
    src: //here is a link
  },
];

const songs = allSongs.sort((a, b) => a.title.localeCompare(b.title));
const currentSongTime = 0;
const currentSong = null;
const song = songs.find((song, id) => song.id === id);
const audio = new Audio();


const renderSongs = () => {

  songs.forEach(data => {
    playlist.innerHTML +=`
    <li class="flex flex-row items-center gap-2 border-b border-b-yellow-600 border-b-2 border-dashed px-3 py-4">
    <button id="playlistBtn" class="flex flex-row items-center gap-3">
    <div class="flex-1 flex-wrap">${data.title}</div>
    <div class="text-[13px] flex-none mr-2">${data.artist}</div>
    <div>${data.duration}</div>
    </button>
    <button id="removeBtn">
        <svg width="30" height="25" fill="white" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z"/></svg>
    </button>
    </li>
    `;
  });
 
  playlist.appendChild(li);
  playlist.appendChild(playlistBtn);
  playlist.appendChild(removeBtn);

  // by clicking on a song, it will play a clicked song
  playlistBtn.addEventListener("click", () => {
    audio.src = song.src;
    audio.play();
  })

}
renderSongs();

Backend to Frontend malfunction

My function “findDate()”always gives me false when called from backend. It is supposed to search for a date not in the date format but as plain text and will return true or false if it finds it or not.

I am using Google Spreadsheets paired with apps scripts. The format used for the dates is “dd/MM/yyyy”.
The html part uses bootstrap5 (for the datepicker).

Html header:

<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
    integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">

  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
    integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous">
  </script>

Html body:

<div id="datepick" class="solid mx-auto" style="margin-bottom: 10px">
  <div class="d-flex justify-content-center">
    <input id="datep" class="form-control" type="date" value="" style="width: 45%">
  </div>
  <div class="d-flex justify-content-center">
    <button class="btn btn-outline-primary" id="btn_date" style="margin: 5px;">choose date</button>
  </div>
</div>

Backend functions:

function findDate(date) {
  const data = SpreadsheetApp.openById('1FMCp-T3fuW9NHUFfvgoeBE9l5dXxWWG5UOqldF0OTDE')
      .getActiveSheet().getDataRange().getValues();
  var parts = date.split('-');
  var year = parseInt(parts[0], 10);
  var month = parseInt(parts[1], 10);
  if (month < 10) {month = "0"+month} 
  var day = parseInt(parts[2], 10);
  console.log("input=>" + date +"; day "+day+", month "+month+ ", year "+year)
  var refDate = day +"/"+ month +"/" + year
  for (var i = 1; i < data.length; i++) {
    var date_gs = data[i][0];
    console.log("Date being compared : " + date_gs);
    if (date_gs == refDate) {
      console.log('found')
      return true
    }
  }
  return false
}

function testFD() {
  var e = findDate("2025-05-23")
  console.log(e)
}
function doGet() {
  return HtmlService.createHtmlOutputFromFile('html')
}

The function works out perfectly fine when i’m testing it mannually (putting the input my frontend uses to call the backend, found on the console log). But it does not work when the backend gets called from the frontend.

So with the same input tried from within my web app and from apps script itself is different, I will always get false when using back to front and I can get true using an inside manual callout of the getDate.

Javascript:

<script>
  let dateInput;
  var btn_datepicker = document.getElementById('btn_date')
  var datepick = document.getElementById('datep')
  function getD(dateInput) {  
    console.log("findDate  backend call with arg : " + dateInput)
    var e = google.script.run.findDate(dateInput);
    console.log(e)  
  }
  btn_datepicker.addEventListener('click', function(e) {
    dateInput = datepick.value
    getD(dateInput)
  })
</script>

link to a copy of my gs:

https://docs.google.com/spreadsheets/d/1FMCp-T3fuW9NHUFfvgoeBE9l5dXxWWG5UOqldF0OTDE/edit?usp=sharing

How to prevent BlockNote editor from losing focus when clicking on an adjacent SVG, without breaking SVG text selection or events?

I’m using BlockNote for the left-side rich text editor in my React app. On the right, I render a dynamic mind map using SVG (e.g., from markmap-lib).

When I click on the (on the right side), the BlockNote editor on the left loses focus, and the blinking cursor disappears.

I want to preserve the editor focus, so that I can keep typing right after interacting with the SVG.

But if I prevent mousedown on the SVG (e.g., e.preventDefault() or e.stopPropagation()), it breaks SVG interactions, such as: selecting inside the SVG, responding to clicks/double-clicks on nodes

I want to:

Keep BlockNote editor focused even after clicking on the SVG

Allow normal SVG interaction: click, double-click, and text selection

demo:
https://stackblitz.com/edit/vitejs-vite-h237idks?file=src%2FApp.tsx

Undetectable JavaScript Error Object Override: Bypassing Standard Detection Mechanisms

I’m attempting to create an undetectable method of overriding the Error object in JavaScript without triggering standard detection mechanisms. My goal is to modify the Error prototype or constructor in a way that cannot be easily identified by typical inspection techniques.

I’ve explored multiple strategies for overriding the Error object:

  • Using Proxy
  • Arrow Function
  • Long Function(Error=function(){})
  • Short Function(Error={Error(){}}.Error)

I’ve developed a comprehensive test function to validate the stealth of the override:

function testError(){
    if(Object.hasOwn(Error,"caller") || Object.hasOwn(Error,"arguments") || Object.hasOwn(Error,"prepareStackTrace"))
        return false;
    try {
        new Error()
    } catch (error) {
        return false;
    }
    try {
        Error()
    } catch (error) {
        return false;
    }
    try {
        Object.create(Error).toString()
    } catch (error) {
        if(error.stack.includes("Object.toString"))
           return false
    }
    return true
}

What techniques can successfully bypass these detection mechanisms and pass the provided testError() function?

Current challenges:

  • Most standard override techniques are easily detectable
  • Need a method that doesn’t trigger standard inspection checks

leaf task not updated after parent task update

I am trying to build a function to distribute amounts across a WBS. So if the amount is assigned to an element all the parents and children should be updated accordingly.
If I try to update an element (ie. 1.2) and then try to update 1.2.1.3 the update works fine for 1.2 but nothing is updated on the UI after changing 1.2.1.3. I can see that the function updateTaskAmount(task) is correctly called for all the relevant items but nothing changes visually. Where do I go wrong?
Please note that while debugging I am also getting rid of jquery so you will find a mix of the two in the code

        const tasks = [{"id":1,"id_gantt":1,"baseline_id":1,"text":"progetto template","start_date":"2025-01-01T08:00:00.000Z","duration":210,"parent":0,"sortorder":2,"progetto":1,"type":"project","amount":"0.00","version":1},{"id":2,"id_gantt":2,"baseline_id":1,"text":"Preparation","start_date":"2025-01-01T08:00:00.000Z","duration":8,"parent":1,"sortorder":3,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":3,"id_gantt":10,"baseline_id":1,"text":"setup project","start_date":"2025-01-01T08:00:00.000Z","duration":6,"parent":2,"sortorder":4,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":4,"id_gantt":9,"baseline_id":1,"text":"conduct kickoff","start_date":"2025-01-08T08:00:00.000Z","duration":1,"parent":2,"sortorder":5,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":5,"id_gantt":8,"baseline_id":1,"text":"quality gate","start_date":"2025-01-09T17:00:00.000Z","duration":0,"parent":2,"sortorder":6,"progetto":1,"type":"milestone","amount":"0.00","version":1},{"id":6,"id_gantt":3,"baseline_id":1,"text":"Blueprint","start_date":"2025-01-10T08:00:00.000Z","duration":41,"parent":1,"sortorder":7,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":7,"id_gantt":13,"baseline_id":1,"text":"Workshops","start_date":"2025-01-10T08:00:00.000Z","duration":41,"parent":3,"sortorder":8,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":8,"id_gantt":12,"baseline_id":1,"text":"Stream 1","start_date":"2025-01-10T08:00:00.000Z","duration":13,"parent":13,"sortorder":9,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":9,"id_gantt":11,"baseline_id":1,"text":"Stream 2","start_date":"2025-01-24T08:00:00.000Z","duration":13,"parent":13,"sortorder":10,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":10,"id_gantt":25,"baseline_id":1,"text":"Stream 3","start_date":"2025-02-07T08:00:00.000Z","duration":13,"parent":13,"sortorder":11,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":11,"id_gantt":4,"baseline_id":1,"text":"Realization","start_date":"2025-02-21T08:00:00.000Z","duration":70,"parent":1,"sortorder":12,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":12,"id_gantt":16,"baseline_id":1,"text":"Configuration","start_date":"2025-02-21T08:00:00.000Z","duration":55,"parent":4,"sortorder":13,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":13,"id_gantt":23,"baseline_id":1,"text":"Configure","start_date":"2025-02-21T08:00:00.000Z","duration":41,"parent":16,"sortorder":14,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":14,"id_gantt":22,"baseline_id":1,"text":"Unit test","start_date":"2025-04-04T07:00:00.000Z","duration":13,"parent":16,"sortorder":15,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":15,"id_gantt":15,"baseline_id":1,"text":"Development","start_date":"2025-02-21T08:00:00.000Z","duration":69,"parent":4,"sortorder":16,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":16,"id_gantt":14,"baseline_id":1,"text":"Develop & Unit Test","start_date":"2025-02-21T08:00:00.000Z","duration":69,"parent":15,"sortorder":17,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":17,"id_gantt":24,"baseline_id":1,"text":"Quality gate","start_date":"2025-05-02T16:00:00.000Z","duration":0,"parent":4,"sortorder":18,"progetto":1,"type":"milestone","amount":"0.00","version":1},{"id":18,"id_gantt":5,"baseline_id":1,"text":"Test & Training","start_date":"2025-05-05T07:00:00.000Z","duration":42,"parent":1,"sortorder":19,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":19,"id_gantt":19,"baseline_id":1,"text":"Training","start_date":"2025-05-05T07:00:00.000Z","duration":25,"parent":5,"sortorder":20,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":20,"id_gantt":18,"baseline_id":1,"text":"Test","start_date":"2025-05-19T07:00:00.000Z","duration":25,"parent":5,"sortorder":21,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":21,"id_gantt":17,"baseline_id":1,"text":"Quality gate","start_date":"2025-06-16T07:00:00.000Z","duration":0,"parent":5,"sortorder":22,"progetto":1,"type":"milestone","amount":"0.00","version":1},{"id":22,"id_gantt":6,"baseline_id":1,"text":"Go live","start_date":"2025-06-17T07:00:00.000Z","duration":43,"parent":1,"sortorder":23,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":23,"id_gantt":21,"baseline_id":1,"text":"Go no go decision","start_date":"2025-06-17T07:00:00.000Z","duration":1,"parent":6,"sortorder":24,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":24,"id_gantt":20,"baseline_id":1,"text":"Hypercare","start_date":"2025-06-18T07:00:00.000Z","duration":41,"parent":6,"sortorder":25,"progetto":1,"type":"task","amount":"0.00","version":1},{"id":25,"id_gantt":26,"baseline_id":1,"text":"Quality gate","start_date":"2025-07-30T16:00:00.000Z","duration":0,"parent":6,"sortorder":26,"progetto":1,"type":"milestone","amount":"0.00","version":1}];

        // Add user_amount property initialized to null
        tasks.forEach(task => task.user_amount = null);

        // Build a map for quick parent lookup
        const taskMap = new Map();
        tasks.forEach(task => taskMap.set(task.id_gantt, { ...task, children: [] }));

        function buildWBS(tasks) {
        const roots = [];
        tasks.forEach(task => {
            const t = taskMap.get(task.id_gantt);
            if (task.parent !== 0) {
            const parent = taskMap.get(task.parent);
            if (parent) parent.children.push(t);
            } else {
            roots.push(t);
            }
        });
        return roots;
        }
        // Initialize the dropdown
        function initializeDropdown(data) {
            // Helper function to assign wbsNumber recursively
            function assignWBSNumbers(items, parentId = 0, prefix = '') {
                let index = 1;
                items
                    .filter(item => item.parent === parentId)
                    .sort((a, b) => a.sortorder - b.sortorder)
                    .forEach(item => {
                        const wbsNumber = prefix ? prefix + '.' + index : '' + index;
                        item.wbsNumber = wbsNumber;
                        // Recursively assign to children
                        assignWBSNumbers(items, item.id_gantt, wbsNumber);
                        index++;
                    });
            }
            assignWBSNumbers(data);
            const select = document.getElementById("element-select");
            let firstOption = document.createElement("option");
            firstOption.value = "";
            firstOption.textContent = "Choose a WBS element to update";
            select.appendChild(firstOption);
            data.forEach(item => {
                const option = document.createElement("option");
                option.value = item.id_gantt;
                option.textContent = item.wbsNumber + ' ' + item.text;
                select.appendChild(option);
            });
        }   
        function getWeight(task) {
            if (task.parent === 0) return 100;
            const parent = taskMap.get(task.parent);
            if (!parent) return 0;
            const parentDuration = parent.duration === 0 ? 1 : parent.duration;
            const taskDuration = task.duration === 0 ? 1 : task.duration;
            return ((taskDuration / parentDuration) * 100).toFixed(2);
        }

        function renderWBS(tasks, container, wbsPrefix = '', level = 1, parentId = null) {
            const numberFormat = new Intl.NumberFormat('it-IT', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
            tasks.sort((a, b) => a.sortorder - b.sortorder);
            tasks.forEach((task, index) => {
                const wbsCode = wbsPrefix ? `${wbsPrefix}.${index + 1}` : `${index + 1}`;
                const hasChildren = task.children.length > 0;
                const weight = getWeight(task);

                const row = document.createElement('tr');
                row.setAttribute('data-id', task.id_gantt);
                row.setAttribute('data-level', level);
                if (parentId !== null) {
                    row.setAttribute('data-parent', parentId);
                    row.classList.add('hidden-row');
                }

                const toggleIcon = hasChildren ? `<span class="toggle-btn" data-toggle-id="${task.id}">▶</span>` : '';
                const startDate = new Date(task.start_date).toLocaleDateString();

                // Format weight: dot as thousands, comma as decimal
                const formattedWeight = numberFormat.format(weight) + '%';
                // Format amount: dot as thousands, comma as decimal, prepend € with space
                const formattedAmount = '€ ' + numberFormat.format(parseFloat(task.amount));
                // Display duration: if 0, show 1 instead
                const displayDuration = task.duration === 0 ? 1 : task.duration;

                row.innerHTML = `
                    <td>${wbsCode}</td>
                    <td>
                        <span class="indent" style="--level: ${level};">
                        ${toggleIcon}${task.text}
                        </span>
                    </td>
                    <td>${startDate}</td>
                    <td>${displayDuration}</td>
                    <td>${formattedWeight}</td>
                    <td><span class="computed-amount" data-id="${task.id_gantt}">${formattedAmount}</span></td>
                `;

                container.appendChild(row);

                if (hasChildren) {
                    renderWBS(task.children, container, wbsCode, level + 1, task.id);
                }
            });
        }

        function toggleRowChildren(toggleId, expand) {
        const rows = document.querySelectorAll(`tr[data-parent='${toggleId}']`);
        rows.forEach(row => {
            if (expand) {
            row.classList.remove('hidden-row');
            } else {
            row.classList.add('hidden-row');
            }

            // Collapse children recursively
            if (!expand) {
            const childId = row.getAttribute('data-id');
            toggleRowChildren(childId, false);
            const icon = row.querySelector(`[data-toggle-id='${childId}']`);
            if (icon) icon.textContent = '▶';
            }
        });
        }

        function addToggleHandlers() {
        document.querySelectorAll('[data-toggle-id]').forEach(icon => {
            icon.addEventListener('click', () => {
            const id = icon.getAttribute('data-toggle-id');
            const isExpanded = icon.textContent === '▼';
            toggleRowChildren(id, !isExpanded);
            icon.textContent = isExpanded ? '▶' : '▼';
            });
        });
        }

        // Expand/collapse WBS to a certain level
        let currentLevel = 1;
        function setWBSLevel(level) {
            const rows = document.querySelectorAll('#wbsTableBody tr');
            let maxLevel = 1;
            rows.forEach(row => {
                const rowLevel = parseInt(row.getAttribute('data-level'), 10);
                if (!isNaN(rowLevel) && rowLevel > maxLevel) maxLevel = rowLevel;
            });
            // Clamp level to range [1, maxLevel]
            if (level < 1) level = 1;
            if (level > maxLevel) level = maxLevel;
            currentLevel = level;
            rows.forEach(row => {
                const rowLevel = parseInt(row.getAttribute('data-level'), 10);
                if (!isNaN(rowLevel)) {
                    if (rowLevel <= level) {
                        row.classList.remove('hidden-row');
                        // set toggle icon to expanded for rows with children at this level
                        const icon = row.querySelector('[data-toggle-id]');
                        if (icon) icon.textContent = (rowLevel < level) ? '▼' : '▶';
                    } else {
                        row.classList.add('hidden-row');
                    }
                }
            });
            // For all rows at the current level, set their toggle icon to collapsed (▶)
            rows.forEach(row => {
                const rowLevel = parseInt(row.getAttribute('data-level'), 10);
                if (!isNaN(rowLevel) && rowLevel === level) {
                    const icon = row.querySelector('[data-toggle-id]');
                    if (icon) icon.textContent = '▶';
                }
            });
        }

        // Update amounts based on user input and propagate changes
        function updateAmounts() {
            const numberFormat = new Intl.NumberFormat('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
            // Reset all amounts to 0 before recalculating
            taskMap.forEach(task => {
                task.amount = 0;
            });

            // Helper function to update parent amounts recursively
            function updateParentAmounts(task) {
                if (task.parent === 0) return;
                const parent = taskMap.get(task.parent);
                if (!parent) return;
                let sumAmounts = 0;
                parent.children.forEach(child => {
                    sumAmounts += parseFloat(child.amount);
                });
                parent.amount = sumAmounts.toFixed(2);
                updateParentAmounts(parent);
            }

            // Recursive function to update amounts for a task and its children
            function updateTaskAmount(task) {
                //console.log(`Updating task: ${task.text} (ID: ${task.id_gantt})`);
                if (task.children.length > 0) {
                    if (task.user_amount !== null && task.user_amount !== '') {
                        // Clear user_amount for all children before distributing
                        task.children.forEach(child => { child.user_amount = null; });
                        // Distribute user_amount among children by weight
                        let totalWeight = 0;
                        task.children.forEach(child => {
                            totalWeight += parseFloat(getWeight(child));
                        });
                        task.children.forEach(child => {
                            const weight = parseFloat(getWeight(child));
                            const childUserAmount = (weight / totalWeight) * parseFloat(task.user_amount);
                            child.user_amount = childUserAmount.toFixed(2);
                            updateTaskAmount(child);
                        });
                        // After children updated, sum their amounts for this task
                        let sumAmounts = 0;
                        task.children.forEach(child => {
                            sumAmounts += parseFloat(child.amount);
                        });
                        task.amount = sumAmounts.toFixed(2);
                    } else {
                        // If no user_amount for this task, sum children amounts
                        let sumAmounts = 0;
                        task.children.forEach(child => {
                            updateTaskAmount(child);
                            sumAmounts += parseFloat(child.amount);
                        });
                        task.amount = sumAmounts.toFixed(2);
                    }
                } else {
                    // Leaf task
                    if (task.user_amount !== null && task.user_amount !== '') {
                        task.amount = parseFloat(task.user_amount).toFixed(2);
                    } else {
                        task.amount = '0.00';
                    }
                }
                // Ensure amount is stored as string with two decimals
                task.amount = parseFloat(task.amount).toFixed(2);
                updateParentAmounts(task);
            }

            // Always update all root tasks (full hierarchy)
            const roots = [];
            taskMap.forEach(task => {
                if (task.parent === 0) roots.push(task);
            });
            roots.forEach(root => updateTaskAmount(root));

            // After all calculations, update DOM for the full hierarchy
            taskMap.forEach(task => {
                const amountSpan = document.querySelector(`.computed-amount[data-id='${task.id_gantt}']`);
                if (amountSpan) {
                    // Format amount: dot as thousands, comma as decimal, prepend € with space
                    amountSpan.textContent = '€ ' + numberFormat.format(parseFloat(task.amount));
                }
                const inputField = document.querySelector(`.user-amount-input[data-id='${task.id_gantt}']`);
                if (inputField) {
                    // Update input field only if different from current user_amount to avoid infinite loop
                    const val = inputField.value === '' ? null : parseFloat(inputField.value);
                    if (val !== task.user_amount) {
                        inputField.value = task.user_amount !== null ? task.user_amount : '';
                    }
                }
            });
            // Ensure original tasks array is updated from taskMap
            tasks.forEach(task => {
                const updated = taskMap.get(task.id_gantt);
                if (updated) {
                    task.amount = updated.amount;
                }
            });
        }

        // Init
        const tree = buildWBS(tasks);
        initializeDropdown(tasks);
        const tbody = document.getElementById('wbsTableBody');
        renderWBS(tree, tbody);
        addToggleHandlers();

        // WBS expand/collapse level buttons
        document.getElementById('expandLevel').addEventListener('click', function() {
            const rows = document.querySelectorAll('#wbsTableBody tr');
            let maxLevel = 1;
            rows.forEach(row => {
                const rowLevel = parseInt(row.getAttribute('data-level'), 10);
                if (!isNaN(rowLevel) && rowLevel > maxLevel) maxLevel = rowLevel;
            });
            if (currentLevel < maxLevel) {
                setWBSLevel(currentLevel + 1);
            }
        });
        document.getElementById('collapseLevel').addEventListener('click', function() {
            if (currentLevel > 1) {
                setWBSLevel(currentLevel - 1);
            }
        });
        //$('#amount-input').number(true,2,',','.');
        $('#element-select').change(function (e) {
            e.preventDefault();
            let idToSearch = $(this).val();
            if (idToSearch !== '') {
                const task = taskMap.get(Number(idToSearch));
                let amountValue = '';
                if (task) {
                    // Prefer user_amount if set, otherwise fallback to calculated amount
                    //amountValue = task.user_amount !== null && task.user_amount !== undefined ? task.user_amount : task.amount;
                    amountValue = parseInt(task.amount) == 0 ? '' : task.amount;
                }
                $('#amount-input').val(amountValue);
            } else {
                $('#amount-input').val('');
            }
        });
        $('#updateAmount').click(function(e){
            e.preventDefault();
            let selectedId = $('#element-select').val();
            let inputAmount = $('#amount-input').val();
            if(selectedId != ''){
                const task = taskMap.get(Number(selectedId));
                if (task) {
                    task.user_amount = inputAmount;
                    updateAmounts();
                    // Retrieve the selected task again after updateAmounts()
                    const updatedTask = taskMap.get(Number(selectedId));
                    if (updatedTask) {
                        const userAmount = updatedTask.user_amount !== null && updatedTask.user_amount !== '' ? parseFloat(updatedTask.user_amount) : null;
                        const calculatedAmount = parseFloat(updatedTask.amount);
                        if (userAmount !== null) {
                            const diff = Math.abs(userAmount - calculatedAmount);
                            if (task.children.length > 0 && diff > 0) {
                                const itNumberFormat = new Intl.NumberFormat('it-IT', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
                                const formattedUserAmount = itNumberFormat.format(userAmount);
                                const formattedCalculatedAmount = itNumberFormat.format(calculatedAmount);
                                const formattedDiff = itNumberFormat.format(diff);
                                if (typeof $(document).Toasts === "function") {
                                    $(document).Toasts('create', { 
                                        autohide: true, 
                                        delay: 4000, 
                                        title: 'Rounding problem', 
                                        class: 'bg-warning', 
                                        body: `Task: <b>${updatedTask.text}</b><br>
                                            Importo originale: <b>€ ${formattedUserAmount}</b><br>
                                            Importo risultante: <b>€ ${formattedCalculatedAmount}</b><br>
                                            Differenza: <b>€ ${formattedDiff}</b>` 
                                    });

                                } else {
                                    alert(
                                        "Piccola differenza di arrotondamenton" +
                                        "Task: " + updatedTask.text + "n" +
                                        "Importo originale: € " + formattedUserAmount + "n" +
                                        "Importo risultante: € " + formattedCalculatedAmount + "n" +
                                        "Differenza: € " + formattedDiff
                                    );
                                }
                            }
                        }
                    }
                }
            }else{
                if (typeof $(document).Toasts === "function") {
                    $(document).Toasts('create', { autohide: true, delay: 750, title: 'Baseline', class: 'bg-warning', body: 'Please select a task to update' });
                } else {
                    alert("Please select a task to update");
                }
            }
        });
        $('#back').click(function(e){
            e.preventDefault();
            location.replace('gantt.html');
        })
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<div class="content-wrapper">
                <div class="content">
                    <div class="container-fluid" id="target">
                        <div class="row text-center" style="margin-bottom: 10px;">
                            <!--<div class="col-md-2 d-none d-sm-block"></div>-->
                            <div class="col-12">
                                <h1>WBS Amount Allocation</h1>
                                <div class="row">
                                    <div class="col-4">
                                        <select id="element-select" class="custom-select form-control-border "></select>
                                    </div>
                                    <div class="col-4">
                                        <div class="input-group">
                                            <input type="text" class="form-control" id="amount-input">
                                            <span class="input-group-append">
                                              <button type="button" class="btn btn-secondary btn-flat" id="updateAmount">Update Amount</button>
                                            </span>
                                          </div>
                                    </div>
                                    <div class="col-2">
                                        <button type="button" class="btn btn-secondary btn-flat" id="expandLevel"><i class="fas fa-search-plus"></i> Zoom In</button>
                                        <button type="button" class="btn btn-secondary btn-flat" id="collapseLevel"><i class="fas fa-search-minus"></i> Zoom Out</button>
                                    </div>
                                    <div class="col-2">
                                        <button type="button" class="btn btn-secondary btn-flat" id="back"><i class="fas fa-long-arrow-alt-left"></i> Back</button>
                                        <button type="button" class="btn btn-secondary btn-flat" id="saveEvBaseline"><i class="fas fa-save"></i> Save</button>
                                        <button type="button" class="btn btn-secondary btn-flat" id="resetEvBaseline"><i class="fas fa-redo-alt"></i> Reset</button>
                                    </div>
                                </div>
                            </div>
                        </div>
                        <div class="row">
                            <div class="col-12">
                                <div id="output">
                                    <table class="table table-bordered table-striped table-hover align-middle">
                                        <thead class="table-dark">
                                          <tr>
                                            <th>WBS Code</th>
                                            <th>Text</th>
                                            <th>Start Date</th>
                                            <th>Duration</th>
                                            <th>Weight (%)</th>
                                            <th>Amount</th>
                                          </tr>
                                        </thead>
                                        <tbody id="wbsTableBody"></tbody>
                                    </table>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>

FirebaseError: Firebase Storage: User does not have permission to access ‘vehicles/LwDaUZiAn8WIfye5Oj6S/backgroundPic’. (storage/unauthorized)

FirebaseError: Firebase Storage: User does not have permission to access ‘vehicles/LwDaUZiAn8WIfye5Oj6S/backgroundPic’. (storage/unauthorized)

I am completly stuck with firebase rules. It’s not working at all. I already checked if userid, carid is correct and it was correct. Firestore path is also correct.

const fetchCar = async (user: User, carId: string) => {
    setLoading(true);
    setError(null);
    try {
      const carRef = doc(db, "vehicles", carId);
      const carSnap = await getDoc(carRef);
      if (carSnap.exists()) {
        let carData: any = { id: carSnap.id, ...carSnap.data() };
        // Try to fetch backgroundPic from storage
        try {
          const url = await getDownloadURL(
            storageRef(storage, `vehicles/${carId}/backgroundPic`)
          );
          carData.image = url;
        } catch (e) {
          carData.image = "/logo.png";
        }
        setCar(carData);
        document.title = `IDMOTO | ${carData.manufacturer} ${carData.model}`;
      } else {
        setError("Car not found.");
      }
    } catch (err) {
      console.error("Error fetching car data:", err);
      setError("Failed to fetch car data.");
    }
    setLoading(false);
  };

  const handleUpload = async (file: File) => {
    if (!user) {
      alert("You must be logged in!");
      return;
    }
    if (!carId) {
      alert("No car ID!");
      return;
    }
    await uploadPhoto(file, user, carId);
    await fetchCar(user, carId);
    // do something with the url
  };
export const uploadPhoto = async (file: File, user: User, carid: string): Promise<string> => {
    if (!file) throw new Error("No file provided.");
    if (!user) throw new Error("User is not authenticated. Please log in.");
    if (!carid) throw new Error("Car ID is not provided.");

    const storageRef = ref(storage, `vehicles/${carid}/backgroundPic`);
    const uploadTask = uploadBytesResumable(storageRef, file);

    return new Promise<string>((resolve, reject) => {
        uploadTask.on(
            "state_changed",
            (snapshot: UploadTaskSnapshot) => {
                const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
                console.log(`Upload is ${progress.toFixed(0)}% done`);
            },
            (error) => {
                console.error("Upload failed", error);
                reject(error);
            },
            async () => {
                try {
                    const downloadURL = await getDownloadURL(uploadTask.snapshot.ref);
                    resolve(downloadURL);
                } catch (error) {
                    reject(error);
                }
            }
        );
    });
};

Firestore rules:

rules_version = '2';

// Allow read/write access to a document keyed by the user's UID
service cloud.firestore {
  match /databases/{database}/documents {
    // User private data
    match /users/{userId} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }
    match /users/{userId}/vehicles/{vehicleId} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }

    // Public vehicles collection
    match /vehicles/{vehicleId} {
        allow read: if true;
    allow get: if request.auth != null && request.auth.uid == resource.data.userID;
    allow write: if request.auth != null
    && (
      // Creating or updating: userID in the data being written
      request.auth.uid == request.resource.data.userID
      // Updating or deleting: userID in the existing document
      || (resource.data.userID != null && request.auth.uid == resource.data.userID)
    );
}}}

Firestorage rules:

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /vehicles/{carId}/{documents=**} {
      allow read: if true;
      allow write: if request.auth != null &&
        get(/databases/(default)/documents/vehicles/$(carId)).data.userID == request.auth.uid;
    }
  }
}

When firestorage rules only include “allow write: if true” or “allow write: if request.auth != null” it is working. Only after typing get line, it stops working.

Tried a lot of things. Nothing works. Is there any way to make it work like that?

Should I use Next.js for external links, or just use ?

I’m working on a Next.js project and wondering about the best practice for linking to external websites.

Next.js provides the component for internal routing. But when it comes to external links (e.g., to https://example.com), I’m not sure whether I should still put them in , or just use a plain tag.

For example:

import Link from 'next/link';

// Option 1 - Using <Link>
<Link href="https://example.com" target="_blank" rel="noopener noreferrer">
  Visit Example
</Link>

// Option 2 - Just <a>
<a href="https://example.com" target="_blank" rel="noopener noreferrer">
  Visit Example
</a>

Questions:

  • Is there any advantage or downside to using for external URLs?
  • Does Next.js handle external links differently if wrapped in ?

Would appreciate any guidance or official docs if available.

jqxGrid: Nested grid not showing for some rows in Firefox (works fine in Chrome/Edge)

I’m using jqxGrid with nested grids (row details) enabled via rowdetails: true and initrowdetails to show detailed subgrids for each parent row. It works perfectly in Chrome and Edge, but in Firefox, some of the nested grids do not appear at all.

$("#orderComparisonGrid").jqxGrid({
            width: '100%',
            pageable: true,
            autoheight: true,
            rowdetails: true,
            initrowdetails: initRowDetailsFunction,
            // ...
});
        
function initRowDetailsFunction(index, parentElement, gridElement, record) {
            var detailsContainer = $($(parentElement).children()[0]);
            var nestedDiv = $("<div style='margin: 10px;'></div>");
            detailsContainer.append(nestedDiv);
        
            if (record.ComponentCodeList && record.ComponentCodeList.length > 0) {
                nestedDiv.jqxGrid({
                    width: '95%',
                    autoheight: true,
                    source: new $.jqx.dataAdapter({ localdata: record.ComponentCodeList, datatype: "array" }),
                    columns: [
                        { text: 'Component Name', datafield: 'Name' },
                        { text: 'Amount', datafield: 'Amount' }
                    ]
                });
       }
}

In Firefox, for some of the rows (first 9 rows) does not render
No console errors.
Wrapping the nested grid creation inside a setTimeout didn’t help
Any workaround or fix would be greatly appreciated.

Luggage Storage in Paddington Station London

Enjoy Stress-free travel with our secure luggage storage near Paddington Station. Whether you’re a tourist, business traveler, or local, free yourself from heavy bags and explore with ease. Our safe and convenient storage solution lets you travel light and worry-free. With affordable rates starting at just £3.99 per day, you can store your luggage for a few hours or even the whole day without breaking the bank.

Our location near Paddington Station makes it easy to drop off your bags and continue exploring the city without any extra weight. Your belongings are safe with us – we offer 24/7 security and easy access to ensure peace of mind. No more dragging heavy suitcases through crowded streets or worrying about the safety of your items. Whether you’re visiting for business or leisure, our luggage storage service gives you the freedom to make the most of your time in London.

India map rendering issue on d3 + svelte

I’m an absolute beginner when it comes to d3 + svelte and I’m trying to render a map of Indian districts in d3 in svelte framework. The json I’m accessing is from here. I’m trying to render the districts in geoMercator projection. I’m guessing the problem lies with the projection function

$: projection = geoMercator().fitSize([width, height], geojson);
    $: pathGenerator = geoPath(projection);
  
    let districts = [];
    $: if (geojson) districts = geojson.features.map(feature => {
      return {
        ...feature,
        path: pathGenerator(feature)
      };
    });

the rendering is done here

{#each districts as district}
        <!-- svelte-ignore a11y-no-static-element-interactions -->
        <path
          d={district.path}
        />
{/each}

But all I see on the browser is black box, which on inspection is the svg container under which I’m rendering the {#each} block. I only want to render the map of India before I move on to joining data and interactivity.

How to Handle Dynamic Geolayer and Geozone Names in Chart.js for Excel Report Download

I’m working on a system where users generate reports based on a selected time range, and the report includes charts created with Chart.js. The charts display data for “Geolayer” and “Geozone” entities, but their names are dynamic and change based on the selected time range (e.g., “MTB DEPARTURE LEVEL”, “CP COMBINED PLAN DOMESTIC”). This makes it challenging to implement a consistent Excel download feature for the chart data.

Example Image:

Here’s an example Excel sheet showing dynamic Geolayer names (e.g., MTB DEPARTURE LEVEL, CP COMBINED PLAN DOMESTIC) and their trolley counts. The names vary based on user-selected time ranges, complicating the Excel export process.

Excel Report Image

Questions:

  • How can I handle dynamic Geolayer and Geozone names in Chart.js to create a consistent and user-friendly Excel download feature?

JsSIP DTMF Issue with Spy/Whisper/Barge Feature

I’m attempting to implement FreePBX’s spy/whisper/barge functionality in a web application using JsSIP, but having issues with the DTMF functionality.

FreePBX Workflow

As per the FreePBX documentation:

FreePBX Feature code prefix allows spy/whisper/barge on the specified extension.

Usage:

  • Dial local extension with 556 prefix to spy
  • While spying on active channel use the following DTMF input to toggle modes:
    • DTMF 4 – spy mode
    • DTMF 5 – whisper mode
    • DTMF 6 – barge mode

Current Implementation

I’m currently using JsSIP to connect to our FreePBX server and trying to implement the whisper functionality:

init: async () => {
  if (ua && ua.isConnected()) return;

  JsSIP.debug.disable("JsSIP:*");  

  const session = await getSession();
  if (!session) throw new Error("No active session found. Please log in.");

  const sipExtension = session.user.sip_config.sip_extension;
  const sipSecret = session.user.sip_config.sip_secret;

  if (!sipExtension || !sipSecret)
    throw new Error("SIP credentials not found in session.");

  const socket = new JsSIP.WebSocketInterface("wss://domain/ws");
  const configuration = {
    sockets: [socket],
    uri: `sip:${sipExtension}@[domain]`,
    password: sipSecret,
    display_name: "Client",
  };

  ua = new JsSIP.UA(configuration);

  // Various event handlers...
  ua.on("registered", () => {
    status = "Connected to PBX";
    // Successfully registered
  });

  ua.on("newRTCSession", (data) => {
    // Session handling...
  });

  ua.start();
},

whisperCall: async (sipConfig) => {
  console.log("Whispering to:", sipConfig);

  if (!ua)
    throw new Error("SIP user agent is not initialized. Call init first.");

  if (currentSession)
    throw new Error(
      "Another call is in progress. End the current call first."
    );

  const targetUri = `sip:${sipConfig.sip_extension}@${SIP_DOMAIN}`;

  // Store the session from the call
  currentSession = ua.call(targetUri);

  // Add event listener for when the call is connected
  currentSession.on("confirmed", () => {
    // Only send DTMF after the call is established
    currentSession.sendDTMF(5, { transportType: "RFC2833" });
    console.log("DTMF tone sent");
  });

  if (!currentSession) throw new Error("Failed to initiate whisper.");

  return currentSession;
}

Problem

  1. When I establish the call using JsSIP, I’m not sure if I need to prefix the extension with “556” as would be done with a regular phone, or if I need to handle that in the SIP URI structure.

  2. When I attempt to send DTMF tone “5” to enter whisper mode after the call is established, it doesn’t appear to be recognized by the FreePBX server.

  3. When my agent is in a call with a client as an admin I want to whisper to my agent

Questions

  1. What is the correct way to implement the FreePBX spy/whisper/barge feature using JsSIP?

  2. Should I be dialing with the prefix in the SIP URI (e.g., sip:556${extension}@${domain}) or should I dial the extension normally and then use DTMF?

  3. Are there specific JsSIP settings or configurations needed for DTMF to work correctly with FreePBX?

Environment

  • JsSIP version: 3.10.1

Any guidance on the correct implementation would be greatly appreciated.