ActiveWin Only Counting VScode Tabs

I have created a code to track active time spent on websites and export it to a csv file called website_time_tracking.csv. When I ran it, it only gave the result of only tracking the amount of time spent on VScode, and when I switched to google, it didn’t track that time:

const activeWin = require('active-win');
const fs = require('fs');
const { createObjectCsvWriter } = require('csv-writer');

// Array to store usage records
const records = [];

// Setup CSV writer
const csvWriter = createObjectCsvWriter({
    path: 'activity_log.csv',
    header: [
        { id: 'start_time', title: 'Start Time' },
        { id: 'end_time', title: 'End Time' },
        { id: 'window_title', title: 'Window Title' },
        { id: 'app_name', title: 'App Name' },
        { id: 'duration_seconds', title: 'Duration (seconds)' }
    ]
});

// Track the previous active window
let previousApp = null;
let previousWindow = null;
let previousStartTime = null;

// Save the interval ID to clear later
const interval = setInterval(trackActiveWindow, 1000);

// Flag to prevent multiple saves
let isSaving = false;

// Function to check active window every second
async function trackActiveWindow() {
    try {
        const activeWindow = await activeWin();

        const now = new Date();

        if (!activeWindow) {
            // No active window detected (maybe locked screen?)
            if (previousApp && previousStartTime) {
                const durationSeconds = (now - previousStartTime) / 1000;

                records.push({
                    start_time: previousStartTime.toISOString(),
                    end_time: now.toISOString(),
                    window_title: previousWindow + ' (screen locked?)',
                    app_name: previousApp,
                    duration_seconds: durationSeconds.toFixed(2)
                });

                console.log(`Logged (locked screen): ${previousApp} - "${previousWindow}" for ${durationSeconds.toFixed(2)} seconds`);

                // Reset previous info
                previousApp = null;
                previousWindow = null;
                previousStartTime = null;
            }
            return;
        }

        const appName = activeWindow.owner.name;
        const windowTitle = activeWindow.title;

        // If we have a different window or app
        if (appName !== previousApp || windowTitle !== previousWindow) {
            // If we had a previous active window, log the duration
            if (previousApp && previousStartTime) {
                const durationSeconds = (now - previousStartTime) / 1000;

                records.push({
                    start_time: previousStartTime.toISOString(),
                    end_time: now.toISOString(),
                    window_title: previousWindow,
                    app_name: previousApp,
                    duration_seconds: durationSeconds.toFixed(2)
                });

                console.log(`Logged: ${previousApp} - "${previousWindow}" for ${durationSeconds.toFixed(2)} seconds`);
            }

            // Update to the new window
            previousApp = appName;
            previousWindow = windowTitle;
            previousStartTime = now;
        }
    } catch (err) {
        console.error('Error tracking window:', err);
    }
}

// Save to CSV when exiting
async function saveOnExit() {
    if (isSaving) return;
    isSaving = true;

    clearInterval(interval);
    await trackActiveWindow(); // Capture the latest window

    console.log('nSaving activity log...');

    try {
        if (records.length > 0) {
            await csvWriter.writeRecords(records);
            console.log('Activity log saved to activity_log.csv');
        } else {
            console.log('No activity recorded.');
        }
    } catch (err) {
        console.error('Error saving activity log:', err);
    } finally {
        process.exit();
    }
}

// Handle process exit
process.on('SIGINT', saveOnExit);
process.on('SIGTERM', saveOnExit);

I’ve tried switching to using the activeWIN, as shown above. But nothing’s worked, and I’m not sure why this is happening. So why is it only counting time spent on VScode?

How to share packages between apps in pnpm monorepo

I’m extremley confused with how monorepos work, I want to build an app with next.js and sanity studio so i have 2 folders

next
studio

both uses some similar packages like

react
react-dom
@types/react
@types/react-dom
@types/node
@typescript

so I dont want each folder to install them independently so i created

pnpm-workspace.yaml

packages:
    - "apps/*"
    - "packages/**/*"

packages/libraries/react/package.json

{
  "name": "@libraries/react",
  "version": "1.0.0",
  "peerDependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "typescript": "^5.3.0"
  },
  "devDependencies": {
    "@types/react": "^19.0.0",
    "@types/react-dom": "^19.0.0"
  }
}

so now inside my next folder

{
  "name": "next",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "@libraries/react": "workspace:*",
  },
  "devDependencies": {
    "@languages/typescript": "workspace:*",
    "@types/node": "^20"
  }
}

but then i get an error react cannot be found but the app working fine i can access it, am I misunderstanding things wrong ?

switching to full-screen yields different results when using the video button and a programmed equivalent

I have a simple web application to display media files (images as well as videos). By default, the application uses part of the screen for controls and the rest for display of the media files.
One of the possible actions the user may trigger is to switch to full-screen mode. This is implemented by invoking the requestFullscreen() method to the media container div. When this is done, the application switches to full-screen while two divs are shown on top with a reduced set of buttons (e.g. “next”, “prev”) and general info related to the shown media file.

When a video file is displays, the video player includes an equivalent button to switch to full-screen mode (and, of course, another button to return to normal). When this button is clicked, full-screen mode is entered BUT the two additional divs are now invisible. I checked setting the z-index very high but these divs remain hidden.

My question is: What is the difference in between the behavior of programmatically triggering full-screen vs. clicking on the video player button?

Alternatively, is there a way to instruct the media player to hide the full-screen button?

Thanks.

Firebase Auth import not recognizing any Modules

I installed Firebase in my Next Js, and I’ve initiallized it correctly. I’ve imported other firebase like (firestore, strage and app) and they work. yet when i try to import from firebase/auth it shows red under it’s imports (like getAuth, GoogleAuthProvider, …) and the error says “Module ‘”firebase/auth”‘ has no exported member ‘getAuth’.” and this is different for each auth imports.

Here is my config file

import { initializeApp, getApp, getApps } from "firebase/app";
import { getFirestore } from "firebase/firestore/lite";
import { getStorage } from "firebase/storage";
import { getAuth, GoogleAuthProvider, EmailAuthProvider } from "firebase/auth";

const firebaseConfig = {
    apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
    authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
    projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
    storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
    messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
    appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
    measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID
};

const app = getApps().length ? getApp() : initializeApp(firebaseConfig);
const db = getFirestore(app);
const storage = getStorage(app);
const auth = getAuth(app)

const googleProvider = new GoogleAuthProvider();
const emailProvider = new EmailAuthProvider();

export { app, db, storage, auth, googleProvider, emailProvider };

it has no problem with recognizing “firebase/auth” and i even tried clearing the cache and reinstalling. downgrading the version and even restarting my IDE. i even tried changing my package manager from pnpm to npm. what am i doing wrong here?

Issue setting up anime.js v4.0.2

I am trying to learn how to use anime.js for a project of mine, but I am having issues making it run. All the tutorials I find are for older version. I tried following the documation provided on the anime.js site, but its still not working. Here are screenshots showing what my pathway and code looks like so far.My DirectoryHTML fileJS fileCSS FileHere is what my current output looks like, just a static boxOutput

Firebase – user customClaims – admin

im trying to find a way how to add to user Admin role via custom claims.
I tried to do it with user creation cloud function, and onCall function, I dont know if claims are assigned, or not, or how to check where is code failing.

Here is my code: 2 cloud functions, I have tried to give admin role after acc creation and then manually (this function is blocked when called from button click by CORS, no idea what to do)

Any help appreciated

Cloud functions:

export const assignAdminRoleOnUserCreation = functions.auth
    .user()
    .onCreate(async (user) => {
      try {
        if (user.email === "[email protected]") {

          await admin.auth().setCustomUserClaims(user.uid, { admin: true });

          console.log(`Admin role assigned to user ${user.email} (${user.uid}).`);
        } else {
          console.log(`No admin role assigned to user ${user.email}.`);
        }
      } catch (error) {
        console.error(`Error assigning admin role to user ${user.email}:`, error);
      }
    });

  export const manuallyAssignAdmin = onCall(async (request) => {
    const targetEmail = "[email protected]"
  
    try {
      const userRecord = await getAuth().getUserByEmail(targetEmail)
  
      await getAuth().setCustomUserClaims(userRecord.uid, { admin: true })
  
      return { message: `Admin role assigned to ${targetEmail}` }
    } catch (error) {
      console.error("Error assigning admin role:", error)
      throw new Error("Failed to assign admin role")
    }
  })

how i call onCall function at front end:

async function assignAdminManually() {
const assignAdmin = httpsCallable(functions, 'manuallyAssignAdmin')

try {
  const result = await assignAdmin()
  console.log(result.data.message)
  alert('Admin role assigned successfully!')
} catch (error) {
  console.error('Error assigning admin role:', error)
  alert('Failed to assign admin role.')
}

}

How I try to check admin role:

  const isAdmin = async () => {
if (cachedIsAdmin !== null) {
  return cachedIsAdmin; 
}

const auth = getAuth();
const user = auth.currentUser;
console.log(auth)
if (user) {
  try {
    const idTokenResult = await user.getIdTokenResult();

    if (idTokenResult.claims.admin) {
      cachedIsAdmin = true;
    } else {
      cachedIsAdmin = false;
    }
  } catch (error) {
    console.error("Error getting ID token result:", error);
    cachedIsAdmin = false;
  }
} else {
  cachedIsAdmin = false;
}

return cachedIsAdmin;

};

Create a pickup Distance Calculation miles pricing in WordPress

I successfully implemented the code from this previous question How to create pickup Distance Calculation miles pricing in WordPress
What I’m trying to do is add a minimum cost of 40 to show if the calculated cost is below that, but really struggling to get an if statement included, any ideas?

document.addEventListener('DOMContentLoaded', function() {
    let map;
    let directionsService;
    let directionsRenderer;

    function initMap() {
        // Initialize the Google Maps objects
        directionsService = new google.maps.DirectionsService();
        directionsRenderer = new google.maps.DirectionsRenderer();
        map = new google.maps.Map(document.getElementById('map'), {
            center: { lat: -34.397, lng: 150.644 },
            zoom: 8
        });
        directionsRenderer.setMap(map);
    }

    function calculateDistance() {
        const pickup = document.getElementById('pickup').value;
        const destination = document.getElementById('destination').value;

        if (pickup && destination) {
            const request = {
                origin: pickup,
                destination: destination,
                travelMode: 'DRIVING'
            };

            directionsService.route(request, function(result, status) {
                if (status == 'OK') {
                    directionsRenderer.setDirections(result);

                    const distance = result.routes[0].legs[0].distance.value / 1000; // distance in km
                    const cost = distance * 2; // Example: $2 per km

                    document.getElementById('distance').textContent = distance.toFixed(2) + ' km';
                    document.getElementById('total-cost').textContent = '$' + cost.toFixed(2);
                } else {
                    alert('Could not calculate distance: ' + status);
                }
            });
        } else {
            alert('Please enter both pickup and destination locations.');
        }
    }

    document.getElementById('calculate-button').addEventListener('click', calculateDistance);

    // Load the map
    initMap();
}); here

I tried various test on IF and Else but does not work

How to make one element shrink while two other elements stay the same

async function main() {
    const numStudents = Number(await new Modal('annannannannanna', 'info', 'How many students do you have?', 'Cancel', 'Submit').response());
    const numGrades = Number(await new Modal('', 'info', 'How many grades does each student have?', 'Cancel', 'Submit').response());
}

class Modal {
    constructor(bodyText, type = 'info', headerText = null, footerText = 'Close', prompt = null, closeable = true) {
        this.type = type;
        if (headerText === null) {
            switch (this.type) {
                case 'success':
                    this.headerText = 'Success!';
                    break;
                case 'info':
                    this.headerText = 'Information';
                    break;
                case 'warning':
                    this.headerText = 'Warning!'
                    break;
                case 'error':
                    this.headerText = 'An error has occurred';
                    break;
                default:
                    this.headerText = 'Notification';
            }
        } else {
            this.headerText = headerText;
        }
        this.bodyText = bodyText;
        this.footerText = footerText;
        this.closeable = closeable;
        this.prompt = prompt;
        this.create();
        this.open();
    }

    create() {
        this.dialog = document.createElement('dialog');

        const header = document.createElement('header');
        header.classList.add(this.type, 'background');
        if (this.closeable) {
            const closeButton = document.createElement('button');
            closeButton.classList.add('close');
            closeButton.innerText = '×';
            header.appendChild(closeButton);
        }
        const headerText = document.createElement('h3');
        headerText.innerText = this.headerText;
        header.appendChild(headerText);
        this.dialog.appendChild(header);

        const form = document.createElement('form');
        form.method = 'dialog';

        const body = document.createElement('main');
        this.bodyText.split('nn').forEach((paragraph) => {
            const p = document.createElement('p');
            p.innerText = paragraph;
            body.appendChild(p);
        });
        if (this.prompt !== null) {
            this.input = document.createElement('input');
            this.input.placeholder = ' ';
            this.input.autofocus = true;
            const p = document.createElement('p');
            p.appendChild(this.input);
            body.appendChild(p);
        }
        form.appendChild(body);

        const footer = document.createElement('footer');
        footer.classList.add(this.type, 'text');
        const hiddenSubmitButton = document.createElement('button');
        hiddenSubmitButton.value = 'submit';
        hiddenSubmitButton.hidden = true;
        footer.appendChild(hiddenSubmitButton);
        const closeButton = document.createElement('button');
        closeButton.classList.add(this.type, 'text', 'animated');
        closeButton.innerText = this.footerText;
        footer.appendChild(closeButton);
        if (this.prompt === null) {
            closeButton.autofocus = true;
        } else {
            const submitButton = document.createElement('button');
            submitButton.classList.add(this.type, 'background', 'animated');
            submitButton.innerText = this.prompt;
            submitButton.value = 'submit';
            footer.appendChild(submitButton);
        }
        form.appendChild(footer);

        this.dialog.addEventListener('close', (event) => {
            this.close(event.target.returnValue);
        });
        this.dialog.appendChild(form);
        document.body.appendChild(this.dialog);
    }

    open() {
        this.dialog.showModal();
    }

    close(returnValue) {
        if (this.prompt !== null) {
            if (returnValue === '') {
                this.responseValue = null;
                if (this.rejectPromise !== undefined) {
                    this.rejectPromise('User canceled prompt');
                }
            } else {
                this.responseValue = this.input.value;
                if (this.rejectPromise !== undefined) {
                    this.resolvePromise(this.responseValue);
                }
            }
        }
    }

    response() {
        this.promise = new Promise((resolve, reject) => {
            if (this.responseValue !== undefined) {
                if (this.responseValue === null) {
                    reject('User canceled prompt')
                } else {
                    resolve(this.responseValue);
                }
            } else {
                this.resolvePromise = resolve;
                this.rejectPromise = reject;
            }
        });
        return this.promise;
    }
}

main();
:root {
    --error: #c00;
    --error-dark: #900;
    --error-light: #f00;
    --info: #36c;
    --info-dark: #039;
    --info-light: #69f;
    --muted: #ddd;
    --muted-dark: #888;
    --muted-light: #eee;
    --success: #0c0;
    --success-dark: #090;
    --success-light: #0f0;
    --warning: #cc0;
    --warning-dark: #990;
    --warning-light: #ff0;
}

body {
    font-family: Arial, Helvetica, sans-serif;
}

button {
    border: 2px solid;
    border-radius: 10px;
    cursor: pointer;
    margin: 1em 0.5em;
    padding: 10px 15px;
    transition: transform 1s;
}

button:active {
    transform: scale(90%);
}

button.animated {
    background-color: transparent;
    overflow: hidden;
    position: relative;
    transition: color 0.3s, border-color 0.3s, transform 0.2s;
    z-index: 1;
}

button.animated:hover {
    border-color: transparent;
}

button.animated::after {
    border: 0;
    border-radius: 50%;
    content: "";
    height: 200%;
    left: -50%;
    opacity: 0;
    position: absolute;
    transform: scale(0.1);
    transform-origin: center;
    transition: opacity 0.3s, transform 0.3s;
    top: -50%;
    width: 200%;
    z-index: -1;
}

button.animated:hover::after {
    opacity: 1;
    transform: scale(1);
}

input {
    border: 0;
    font: inherit;
    letter-spacing: normal;
    margin: 0;
    padding: 0;
}

input:focus, input:placeholder-shown {
    box-shadow: 0 2px 0 var(--muted);
    outline: none;
}

dialog {
    background-color: white;
    border: 0;
    border-radius: 10px;
    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
    opacity: 0;
    outline: none;
    padding: 0;
    transform: scale(0);
    transition: all 0.4s allow-discrete;
    width: 50%;
}

dialog:open {
    opacity: 1;
    transform: scale(1);
}

@starting-style {
    dialog:open {
        opacity: 0;
        transform: scale(0);
    }
}

dialog::backdrop {
    background-color: rgba(0, 0, 0, 0);
    transition: all 0.4s allow-discrete;
}

dialog:open::backdrop {
    background-color: rgba(0, 0, 0, 0.4);
}

@starting-style {
    dialog:open::backdrop {
        background-color: rgba(0, 0, 0, 0);
    }
}

dialog header, dialog main, dialog footer {
    display: flow-root;
    padding: 0 1em;
    text-align: center;
}

dialog header {
    background-color: black;
    color: white;
}

dialog main, dialog footer {
    background-color: white;
    color: black;
}

dialog form {
    display: flex;
    flex-direction: column;
}

dialog header, dialog footer {
    flex: initial;
}

dialog main {
    flex: 1 1 auto;
    overflow-y: auto;
}

.close {
    aspect-ratio: 1 / 1;
    background-color: rgba(0, 0, 0, 0);
    border: 0;
    border-radius: 50%;
    box-sizing: border-box;
    color: var(--muted);
    font-size: 1.2em;
    font-weight: bold;
    height: 1.2em;
    margin: 0;
    padding: 0;
    position: absolute;
    right: 0.5rem;
    top: 0.5rem;
    user-select: none;
}

.close:hover,
.close:focus {
    background-color: rgba(255, 255, 255, 0.2);
    color: white;
}

.success.text, .info.text, .warning.text, .error.text, button.animated.success.background::after, button.animated.info.background::after, button.animated.warning.background::after, button.animated.error.background::after {
    background-color: white;
}

.success.background, .info.background, warning.background, error.background, button.animated.success.text:hover, button.animated.info.text:hover, button.animated.warning.text:hover, button.animated.error.text:hover {
    color: white;
}

button.animated.success.text, button.animated.info.text, button.animated.warning.text, button.animated.error.text {
    border-color: var(--muted);
}

.success.text, button.animated.success.background:hover {
    color: var(--success);
}

.success.background, button.animated.success.text::after {
    background-color: var(--success);
}

button.animated.success.text:hover, button.animated.success.background {
    border-color: var(--success);
} 

.info.text, button.animated.info.background:hover {
    color: var(--info);
}

.info.background, button.animated.info.text::after {
    background-color: var(--info);
}

button.animated.info.text:hover, button.animated.info.background {
    border-color: var(--info);
}

.warning.text, button.animated.warning.background:hover {
    color: var(--warning);
}

.warning.background, button.animated.warning.text::after {
    background-color: var(--warning);
}

button.animated.warning.text:hover, button.animated.warning.background {
    border-color: var(--warning);
}

.error.text, button.animated.error.background:hover {
    color: var(--error);
}

.error.background, button.animated.error.text::after {
    background-color: var(--error);
}

button.animated.error.text:hover, button.animated.error.background {
    border-color: var(--error);
}

I have a modal, as you can easily see in the link. When you resize the window until a scroll bar appears, you will see that the scroll bar appears for the entire modal. If you look closely, you can observe that the modal consists of a header (obviously), a main (the body text), and a footer (the button(s) at the bottom). I want the header and footer to act like a header and footer by only having the main portion have a scroll bar. How do I do this?

Video element not playing content of shared screen

I am trying to build my custom video conferencing using mediasoup. I am totally new to WebRTC and mediasoup. I already built my own SFU script that handles things like producers, consumers, whiteboard events … etc.

I started building a simple mediasoup-client script to connect to a room and share events among peers in the same room. My problem is with sharing screens in general, the event is sent successfully with creating producer and the other users get the mediatrack that the producer is sharing but when creating a video element and setting its srcObject to the track. Everything is set successfully but when testing it locally with 2 different tabs, the video element is created with the srcObject but nothing is actually showing in the video. Not even a black screen.

mediasoup-config.js:

module.exports = {
mediaCodecs: [
    {
        kind: "audio",
        mimeType: "audio/opus",
        clockRate: 48000,
        channels: 2,
    },
    {
        kind: "video",
        mimeType: "video/VP8",
        clockRate: 90000,
        parameters: {
            "x-google-start-bitrate": 1000,
        },
    },
],
};

shareScreen function:

async function shareScreen() {
const screenStream = await navigator.mediaDevices.getDisplayMedia({
    video: true
});

const screenTrack = screenStream.getVideoTracks()[0];

await sendTransport.produce({
    track: screenTrack,
    appData: { mediaType: 'screen' }
});

console.log('Shared screen track readyState:', screenTrack.readyState);
console.log('Screen track:', screenTrack);

localVideo.srcObject = screenStream;
localVideo.autoplay = true;
localVideo.playsInline = true;
}

handleNewConsumer function:

function handleNewConsumer(consumerData) {
console.log(consumerData);

const { id, kind, rtpParameters, producerId } = consumerData;

console.log('New consumer', consumerData);

recvTransport.consume({
    id,
    producerId,
    kind,
    rtpParameters,
}).then(consumer => {
    const stream = new MediaStream();
    stream.addTrack(consumer.track);

    console.log('Remote stream tracks:', stream.getTracks());
    console.log('Consumer track:', consumer.track);
    console.log('Track readyState:', consumer.track.readyState);
    console.log('Track muted:', consumer.track.muted);

    const remoteVideo = document.createElement('video');
    remoteVideo.autoplay = true;
    remoteVideo.playsInline = true;

    remoteVideo.onerror = (event) => {
        console.error('Video playback error:', event, remoteVideo.error);
    };

    remoteVideo.addEventListener('loadedmetadata', () => {
        console.log('Remote video loaded metadata:', remoteVideo.videoWidth, remoteVideo.videoHeight, remoteVideo.duration);
        remoteVideo.play().catch(err => {
            console.error('Auto-play failed:', err);
        });
    });

    remoteVideo.addEventListener('resize', () => {
        console.log('Remote video resized to:', remoteVideo.videoWidth, remoteVideo.videoHeight);
    });

    consumer.track.addEventListener('unmute', () => {
        console.log('Track unmuted, setting srcObject');
        remoteVideo.srcObject = stream;
        console.log('Remote video srcObject set:', remoteVideo.srcObject);
    });

    if (consumer.track.readyState === 'live' && !consumer.track.muted) {
        console.log('Track already live and unmuted, setting srcObject');
        remoteVideo.srcObject = stream;
        console.log('Remote video srcObject set:', remoteVideo.srcObject);
    }

    remoteVideos.appendChild(remoteVideo);
    console.log('Remote video element appended:', remoteVideo);
});
}

Failed loki js test ci

Can someone help me with this error ? Failed loki test command as local as remote in github

loki test v0.35.0 (node:15993) NOTE: The AWS SDK for JavaScript (v2)
is in maintenance mode. SDK releases are limited to address critical
bug fixes and security issues only.

Please migrate your code to use AWS SDK for JavaScript (v3). For more
information, check the blog post at https://a.co/cUPnyil (Use node --trace-warnings ... to show where the warning was created) FAIL chrome.app
Fetching stories
Failed fetching stories because the server is down
Try starting it with “yarn storybook” or pass the –port or –host arguments if it’s not running at http://10.242.224.33:15504 Some visual tests failed to run

storybook builded fine but by some reason it can’t finish run loki test

here my scripts

"scripts": {
    "start": "webpack server --env port=3000",
    "build:prod": "webpack   --env mode=production",
    "build:dev": "webpack   --env mode=development",
    "lint:ts": "eslint "**/*{ts,tsx}"",
    "lint:ts:fix": "eslint "**/*{ts,tsx}" --fix",
    "lint:scss": " npx stylelint "**/*.scss"",
    "lint:scss:fix": " npx stylelint "**/*.scss" --fix",
    "test:unit": "jest --config ./config/jest/jest.config.ts",
    "test:ui": "npx loki test",
    "test:ui:ok": "npx loki approve",
    "test:ui:ci":  "npx loki --requireReference update --reactUri file:./storybook-static",
    "storybook": "storybook dev -p 6006 -c ./config/storybook",
    "storybook:build": "storybook build -c ./config/storybook"   
}

here my loki settings in json file

"loki": {
    "configurations": {
      "chrome.laptop": {
        "target": "chrome.app",
        "width": 1366,
        "height": 768,
        "deviceScaleFactor": 1,
        "mobile": false
      },
      "chrome.iphone7": {
        "target": "chrome.app",
        "preset": "iPhone 7"
      }
    }
  }

What is a recommended design pattern for processing data from an API response in Yii2?

My Yii2 application (in PHP 7, so unfortunately I do not have strict types) receives from an API call responses which should be nested arrays looking like:

$response = [
  'session' => [
     'userData' => ['user_id' => 232, ...],
     'sessionData => ['status' => 'ONGOING', 'details' => 'Some details here...', ...],
     'additionalData' => [...]
  ]
]

Currently in my application logic, when I receive the response I manually check that each field exists and is of the required type. So for example I start by checking that $response['session'] exists and is an array, and I throw an exception if it doesn’t, so something like:

if (!array_key_exists($response, 'session')) {
    throw new Exception('Invalid response: missing "session" key in response array.');
}
if (!is_array($response['session'])) {
   throw new Exception('Invalid response: "session" is not an array in response array.');
}

Then I check for $response['session']['userData'] and I check that it’s an array, and so on. When I am certain that $response['session']['userData']['user_id'] exists, I also check that it is an integer, so I also do validation for the primitive received types.

I would like to decouple this logic from my main application, by having a separate model which stores all the primitive properties I want from the response and validates the primitive ones.

My problem is that I’m uncertain what would be the best design pattern to implement this.

My initial thought was to build a DTO-Hydrator-like pattern, so:

  1. A model (DTO) having as fields only the ‘primitive’ properties such as user_id, status, details, etc, but not the arrays like session, userData. This model would only be responsible for validation of the primitive fields (so checking that user_id is an integer for instance, checking that ‘status’ is in an array of admissible statuses, etc.)
  2. A Hydrator model which is responsible for checking that all desired
    primitive fields exist, and of course, that everything ‘in-between’
    exists, and then loading the primitive fields into the DTO.

However, this approach seems overly complicated. For one thing, I would have to instantiate both the DTO and the Hydrator each time I have to handle a response in my application logic, which would still make that logic too cumbersome for what I want. Ideally I would like to only do something like
$model->process($response)
in my application logic, but in a way that is also an endorsed design pattern. So is there a way to do this without the (seeming) overcomplication of the DTO-Hydrator pattern?

Docker does not download the specified image

Docker-Compose is not downloading the specific version of PHP that I want. I want the version “php:8.4.5-fpm” and it only downloads the “latest” version. I tried several things, but I can’t get it to download the specific image, it only downloads the “latest” image.

docker-compose.yml

version: "3.9"

services:
  nginx:
    build:
      context: ../nginx  # Caminho para a pasta Nginx (relativo à pasta docker-compose)
    ports:
      - "80:80"
    volumes:
      - ../app:/var/www/html  # Monta a pasta app como /var/www/html (relativo à pasta docker-compose)
    depends_on:
      - php
    networks:
      - laravel-network

  php:
    build:
      context: ../php  # Caminho para a pasta PHP (relativo à pasta docker-compose)
    expose:
      - 9000
    volumes:
      - ../app:/var/www/html  # Monta a pasta app como /var/www/html (relativo à pasta docker-compose)
    depends_on:
      - db
    networks:
      - laravel-network

  db:
    image: mariadb:11.7.2
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: laravel
      MYSQL_USER: laravel
      MYSQL_PASSWORD: laravel
    volumes:
      - db_data:/var/lib/mysql
    networks:
      - laravel-network

  phpmyadmin:
    image: phpmyadmin:latest
    ports:
      - "8080:80"
    environment:
      PMA_HOST: db
      MYSQL_ROOT_PASSWORD: root
    depends_on:
      - db
    networks:
      - laravel-network

volumes:
  db_data:

networks:
  laravel-network:
    driver: bridge

Dockerfile PHP

FROM php:8.4.5-fpm

# Definir diretório de trabalho
WORKDIR /var/www/html

# Instalar dependências do sistema
RUN apt-get update && apt-get install -y 
    build-essential 
    libpng-dev 
    libjpeg62-turbo-dev 
    libfreetype6-dev 
    locales 
    zip 
    unzip 
    git 
    curl 
    libzip-dev 
    libonig-dev 
    libxml2-dev 
    && apt-get clean && rm -rf /var/lib/apt/lists/*

# Instalar extensões PHP necessárias para o Laravel
RUN docker-php-ext-install pdo_mysql mbstring zip exif pcntl soap
RUN docker-php-ext-configure gd --with-freetype --with-jpeg
RUN docker-php-ext-install gd

# Instalar o Composer
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

# Criar usuário para a aplicação Laravel
RUN groupadd -g 1000 www && useradd -u 1000 -ms /bin/bash -g www www

# Copiar o código da aplicação
COPY --chown=www:www . /var/www/html

# Alterar usuário
USER www

# Expor a porta 9000 para o PHP-FPM
EXPOSE 9000

CMD ["php-fpm"]

Dockerfile Nginx

FROM nginx:1.27.3

# Copiar a configuração do Nginx
COPY default.conf /etc/nginx/conf.d/default.conf