<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Muhammad Sheharyar Butt</title>
    <description>The latest articles on DEV Community by Muhammad Sheharyar Butt (@shehari007).</description>
    <link>https://dev.to/shehari007</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3634036%2F1f6cc67a-7751-4732-9ea1-b0df8637c656.png</url>
      <title>DEV Community: Muhammad Sheharyar Butt</title>
      <link>https://dev.to/shehari007</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shehari007"/>
    <language>en</language>
    <item>
      <title>Five Characters Too Long: Auditing My Own URL Shortener</title>
      <dc:creator>Muhammad Sheharyar Butt</dc:creator>
      <pubDate>Mon, 17 Aug 2026 11:24:39 +0000</pubDate>
      <link>https://dev.to/shehari007/five-characters-too-long-auditing-my-own-url-shortener-pfm</link>
      <guid>https://dev.to/shehari007/five-characters-too-long-auditing-my-own-url-shortener-pfm</guid>
      <description>&lt;p&gt;I run &lt;a href="https://shorty.msyb.dev" rel="noopener noreferrer"&gt;Shorty&lt;/a&gt;, an open-source URL shortener I built and self-host.&lt;br&gt;
It does the usual things — short links, QR codes, click analytics — plus a moderation&lt;br&gt;
console with role-based access, an audit log and abuse triage. The whole thing is&lt;br&gt;
TypeScript: Next.js 16 on the front, Express 5 and Drizzle over MySQL/TiDB on the back.&lt;/p&gt;

&lt;p&gt;The README claimed, among other things, that &lt;em&gt;"identical error messages prevent account&lt;br&gt;
enumeration."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I sat down to audit my own code and found out that sentence was false. Here's what a&lt;br&gt;
careful pass over a codebase you wrote yourself actually turns up.&lt;/p&gt;


&lt;h2&gt;
  
  
  The bug that started it
&lt;/h2&gt;

&lt;p&gt;Here is the entire defect. It is one line, in the admin login handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Spend comparable time so a missing account is not detectably faster.&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;verifyPassword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$2b$12$invalidinvalidinvalidinvalidinvalidinvalidinvalidinvalidin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;genericFailure&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The intent is textbook. When someone tries to log in with an email that doesn't exist,&lt;br&gt;
you don't want to return instantly — that reveals the address isn't registered. So you&lt;br&gt;
burn the same CPU you would have burned verifying a real password, then return the same&lt;br&gt;
generic error.&lt;/p&gt;

&lt;p&gt;The comment is right. The code does not do what the comment says.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A bcrypt hash is exactly 60 characters.&lt;/strong&gt; That literal is 65. bcryptjs validates the&lt;br&gt;
shape of the hash before it does any work, and bails immediately when it doesn't parse.&lt;/p&gt;

&lt;p&gt;So I measured it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;unknown account (malformed hash):    0.04 ms
real account   (genuine hash):     264.73 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a &lt;strong&gt;6,000x difference&lt;/strong&gt;, from a single unauthenticated request, with no rate limit&lt;br&gt;
worth speaking of and no lockout on that path. You don't need statistics or timing&lt;br&gt;
analysis. You need a stopwatch. Feed it a list of candidate emails and the ones that come&lt;br&gt;
back slowly are real administrators.&lt;/p&gt;

&lt;p&gt;The fix is to stop hand-writing the hash and generate one, then assert the thing that&lt;br&gt;
actually matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;dummyPasswordHash&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dummyHash&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;dummyHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bcrypt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hashSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;generateOpaqueToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;BCRYPT_ROUNDS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dummyHash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`bcrypt returned a &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;dummyHash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-character hash; the login timing guard is broken`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;dummyHash&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After: &lt;code&gt;0.98x&lt;/code&gt;. Indistinguishable.&lt;/p&gt;

&lt;p&gt;There's a second-order lesson in the lazy initialisation. Deriving a cost-12 hash takes&lt;br&gt;
~250 ms, and the module is imported by the redirect path too — so computing it at import&lt;br&gt;
time would have added a quarter second to every cold start on a serverless deploy, in&lt;br&gt;
order to protect a login endpoint. Security fixes have a blast radius.&lt;/p&gt;


&lt;h2&gt;
  
  
  The one I'm most annoyed about
&lt;/h2&gt;

&lt;p&gt;Same login handler, a few lines down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lockedUntil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lockedUntil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AppError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;423&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ACCOUNT_LOCKED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`Too many failed attempts. Try again in &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; minutes.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Account lockout is a good control. But look at what it returns: a &lt;strong&gt;423&lt;/strong&gt; with a countdown,&lt;br&gt;
where every other failure returns a generic 401.&lt;/p&gt;

&lt;p&gt;I built a careful anti-enumeration story on one path and then handed the answer away on&lt;br&gt;
another. Five failed logins against any address, and the status code tells you whether&lt;br&gt;
that address belongs to a real admin. The locked branch also returned &lt;em&gt;before&lt;/em&gt; doing any&lt;br&gt;
bcrypt work, so it was fast as well as differently-shaped.&lt;/p&gt;

&lt;p&gt;It now returns the same 401, after the same bcrypt spend, and writes the audit entry that&lt;br&gt;
path was silently missing. Legitimate admins lose the "try again in 12 minutes" message.&lt;br&gt;
That's the trade, and it's the right one.&lt;/p&gt;


&lt;h2&gt;
  
  
  When your defence has a formatting dependency
&lt;/h2&gt;

&lt;p&gt;The link-safety guard refuses destinations that point at private networks. It handled&lt;br&gt;
IPv4-mapped IPv6 addresses — the trick where you smuggle &lt;code&gt;127.0.0.1&lt;/code&gt; inside a v6 literal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mapped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;normalised&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/::ffff:&lt;/span&gt;&lt;span class="se"&gt;(\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\.\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\.\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\.\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mapped&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;isPrivateIpv4&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mapped&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reasonable. Except the check runs on a hostname that has already been through the WHATWG&lt;br&gt;
URL parser, and that parser &lt;strong&gt;rewrites the address&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://[::ffff:127.0.0.1]/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;
&lt;span class="c1"&gt;// '[::ffff:7f00:1]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dotted quad becomes hex. The regex requires dots. It never matched a single real&lt;br&gt;
request.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;[::ffff:a9fe:a9fe]&lt;/code&gt; — the cloud metadata endpoint, &lt;code&gt;169.254.169.254&lt;/code&gt;, wearing an IPv6&lt;br&gt;
hat — sailed straight through. Along with five other forms.&lt;/p&gt;

&lt;p&gt;The fix was to stop parsing addresses by hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PRIVATE_RANGES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BlockList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;PRIVATE_RANGES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addSubnet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;127.0.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ipv4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;PRIVATE_RANGES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addSubnet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;169.254.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ipv4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="c1"&gt;// 'ipv6' is deliberate even for an IPv4-mapped address: BlockList maps it&lt;/span&gt;
&lt;span class="c1"&gt;// back onto the IPv4 entries above, which is what closes the bypass.&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ipVersion&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;PRIVATE_RANGES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ipv6&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Node's built-in &lt;code&gt;net.BlockList&lt;/code&gt; resolves IPv4-mapped addresses against the IPv4 rules&lt;br&gt;
natively. Six bypasses closed, and — verified — nothing legitimate over-blocked.&lt;/p&gt;

&lt;p&gt;The pattern worth taking away: &lt;strong&gt;my check ran on a different string than the one I wrote it&lt;br&gt;
against.&lt;/strong&gt; Anywhere a value passes through a normaliser between validation and use, that's&lt;br&gt;
where the bug lives.&lt;/p&gt;


&lt;h2&gt;
  
  
  Controls that report success while doing nothing
&lt;/h2&gt;

&lt;p&gt;Three findings shared a shape, and it's the shape I now look for first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Revoke sessions" didn't revoke sessions.&lt;/strong&gt; The emergency force-sign-out button&lt;br&gt;
incremented a &lt;code&gt;tokenVersion&lt;/code&gt; column, which kills outstanding access tokens. But the refresh&lt;br&gt;
endpoint never compared sessions against that version — so a stolen refresh token could be&lt;br&gt;
exchanged straight back for a fresh access token carrying the &lt;em&gt;new&lt;/em&gt; version. The button&lt;br&gt;
returned &lt;code&gt;{revoked: true}&lt;/code&gt;, wrote a satisfying audit entry, and changed nothing an attacker&lt;br&gt;
would notice. For up to seven days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blocking a domain didn't block existing links.&lt;/strong&gt; The blocklist had exactly one reader:&lt;br&gt;
the create-link path. Block &lt;code&gt;evil.com&lt;/code&gt; and every link already pointing there kept&lt;br&gt;
redirecting, indefinitely, while the console displayed the domain as blocked. The&lt;br&gt;
moderation control worked perfectly for the case where nobody had attacked you yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate limiting three routes that had none.&lt;/strong&gt; In Express, &lt;code&gt;router.use()&lt;/code&gt; only runs for&lt;br&gt;
requests that reach it in stack order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;adminRouter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/auth/password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requireAdmin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;asyncHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;changePassword&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// ... more routes ...&lt;/span&gt;
&lt;span class="nx"&gt;adminRouter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requireAdmin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;adminApiLimiter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// never runs for anything above&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/auth/password&lt;/code&gt; runs two bcrypt operations per call — roughly 600 ms of single-threaded&lt;br&gt;
CPU — and any token holder, down to the lowest role, could loop it with a wrong password&lt;br&gt;
and stall the process that also serves every public redirect.&lt;/p&gt;

&lt;p&gt;A control that fails loudly gets fixed. A control that succeeds quietly while doing nothing&lt;br&gt;
can sit there for a year.&lt;/p&gt;




&lt;h2&gt;
  
  
  The rest, briefly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The login rate limiter was keyed on attacker-controlled input&lt;/strong&gt; (&lt;code&gt;ip:email&lt;/code&gt;). Vary the
email, get unlimited fresh buckets. It also never trimmed the email while the database
lookup did — so &lt;code&gt;"admin@x.com"&lt;/code&gt; and &lt;code&gt;"admin@x.com "&lt;/code&gt; were separate budgets hitting the
same row, which is enough to hold any known admin in a permanent lockout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A legacy compatibility endpoint bypassed every per-action limiter,&lt;/strong&gt; allowing ~80x the
intended abuse-report rate. Enough for a handful of hosts to auto-block arbitrary links
with no human involved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/health&lt;/code&gt; echoed raw driver errors&lt;/strong&gt; to anonymous callers — database hostname, port,
and &lt;code&gt;Access denied for user 'x'@'y'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The admin console had no CSRF protection.&lt;/strong&gt; The session cookies were &lt;code&gt;SameSite=Lax&lt;/code&gt;,
which I had filed as "handled". Lax is a same-&lt;em&gt;site&lt;/em&gt; control, and site means registrable
domain — which my API subdomain shares with the web app. Any page on any sibling
subdomain could drive every admin write with the operator's cookies attached.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The blocklist failed open.&lt;/strong&gt; A database error returned an empty list, so on a cold
serverless process whose first round-trip timed out, every blocked domain was accepted —
and &lt;em&gt;permanently&lt;/em&gt; minted, because nothing re-checked existing links.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ten findings. None of them were exotic. All of them were in code I wrote, reviewed, and&lt;br&gt;
shipped.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I actually changed about how I work
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Every security claim in a README is a test that doesn't exist yet.&lt;/strong&gt; I wrote "prevents&lt;br&gt;
account enumeration" and then never wrote the assertion. The claim had been false since the&lt;br&gt;
day it was committed. There are now 27 tests, and every bypass above is pinned as a&lt;br&gt;
regression case — because the SSRF guard in particular fails &lt;em&gt;silently&lt;/em&gt;. A hole in it looks&lt;br&gt;
exactly like a working shortener right up until someone points a link at your metadata&lt;br&gt;
endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comments describe intent, not behaviour.&lt;/strong&gt; &lt;code&gt;// Spend comparable time&lt;/code&gt; was accurate about&lt;br&gt;
what I meant and wrong about what ran. When reviewing, read the code as if the comment&lt;br&gt;
isn't there — then check whether they agree.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefer the boring standard-library primitive.&lt;/strong&gt; My hand-rolled IP range checks had a&lt;br&gt;
subtle formatting dependency. &lt;code&gt;net.BlockList&lt;/code&gt; has been correct for years and handles a&lt;br&gt;
case I didn't know existed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check whether your control has ever fired.&lt;/strong&gt; "Does this code run?" caught three findings.&lt;br&gt;
For any security control, trace one real request through it and confirm the guard is on the&lt;br&gt;
path. Middleware ordering, cache population, retroactive application — these are where&lt;br&gt;
things quietly do nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audit the seams.&lt;/strong&gt; Nearly every finding lived at a boundary: between a parser and a&lt;br&gt;
validator, between route registration and middleware, between two subdomains that a browser&lt;br&gt;
considers the same site. The code inside each module was fine. The bugs were in the joins.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Everything above is fixed and shipped in&lt;br&gt;
&lt;a href="https://github.com/shehari007/url-shorty/releases/tag/v3.0.1" rel="noopener noreferrer"&gt;v3.0.1&lt;/a&gt;. The full&lt;br&gt;
&lt;a href="https://github.com/shehari007/url-shorty/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;changelog&lt;/a&gt; has the&lt;br&gt;
per-file detail, and the code is &lt;a href="https://github.com/shehari007/url-shorty" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt; if&lt;br&gt;
you want to check my work — genuinely, please do.&lt;/p&gt;

&lt;p&gt;Two things I'd suggest if you're about to do this to your own project. Run your git history&lt;br&gt;
through a secret scanner before anything else; rotating a credential is cheap, and finding&lt;br&gt;
out later that it's been public for two years is not. And write the audit down as you go,&lt;br&gt;
findings and non-findings both. Half the value turned out to be the list of things I&lt;br&gt;
checked and &lt;em&gt;didn't&lt;/em&gt; find, because that's the list I don't have to check again.&lt;/p&gt;

&lt;p&gt;The most uncomfortable part wasn't any individual bug. It was that I'd written a security&lt;br&gt;
section in the README describing the system I intended to build, and never gone back to&lt;br&gt;
verify I'd built it.&lt;/p&gt;

</description>
      <category>codequality</category>
      <category>node</category>
      <category>express</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>🚀 Introducing SysPeek v2.0.0 - A Modern System Information Viewer for Windows, macOS, and Linux (New Release!)</title>
      <dc:creator>Muhammad Sheharyar Butt</dc:creator>
      <pubDate>Sat, 15 Aug 2026 21:16:08 +0000</pubDate>
      <link>https://dev.to/shehari007/introducing-syspeek-v200-a-modern-system-information-viewer-for-windows-macos-and-linux-1e4d</link>
      <guid>https://dev.to/shehari007/introducing-syspeek-v200-a-modern-system-information-viewer-for-windows-macos-and-linux-1e4d</guid>
      <description>&lt;h2&gt;
  
  
  Introducing SysPeek v2.0.0: A Modern Cross-Platform System Monitor Built with Electron &amp;amp; React
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;A complete architectural rewrite featuring Electron 43, React 19, TypeScript, Vite, automatic updates, live monitoring, and a secure Electron architecture.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I'm excited to announce the release of &lt;strong&gt;SysPeek v2.0.0&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;While previous versions focused on providing an intuitive hardware information viewer, version 2 represents a complete rebuild of the application from the ground up.&lt;/p&gt;

&lt;p&gt;This release modernizes every layer of the project—from the build system and security model to the user interface and update mechanism—creating a faster, more maintainable, and production-ready desktop application.&lt;/p&gt;

&lt;p&gt;Built with &lt;strong&gt;Electron&lt;/strong&gt;, &lt;strong&gt;React&lt;/strong&gt;, &lt;strong&gt;TypeScript&lt;/strong&gt;, and &lt;strong&gt;Vite&lt;/strong&gt;, SysPeek now delivers real-time system monitoring, detailed hardware information, automatic updates, and a hardened Electron architecture designed for modern desktop development.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is SysPeek?
&lt;/h2&gt;

&lt;p&gt;SysPeek is a modern desktop application that allows you to inspect your computer's hardware and monitor live system performance from a single interface.&lt;/p&gt;

&lt;p&gt;Rather than relying on multiple utilities, SysPeek combines hardware inspection, performance monitoring, system diagnostics, and productivity tools into one lightweight application.&lt;/p&gt;

&lt;p&gt;Whether you're a developer, IT administrator, technician, or simply curious about your hardware, SysPeek provides detailed insights in a clean and responsive interface.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's New in Version 2?
&lt;/h2&gt;

&lt;p&gt;Version 2 isn't simply an update—it's a complete architectural redesign.&lt;/p&gt;

&lt;p&gt;Every major subsystem has been rebuilt to improve performance, maintainability, scalability, and security.&lt;/p&gt;

&lt;h3&gt;
  
  
  Major Improvements
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Complete migration to &lt;strong&gt;electron-vite&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Fully rewritten using &lt;strong&gt;TypeScript&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Electron &lt;strong&gt;43&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;React &lt;strong&gt;19&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Ant Design &lt;strong&gt;6&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Vite &lt;strong&gt;7&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Automatic application updates&lt;/li&gt;
&lt;li&gt;Secure IPC communication layer&lt;/li&gt;
&lt;li&gt;Context isolation and sandbox enabled&lt;/li&gt;
&lt;li&gt;Live history charts&lt;/li&gt;
&lt;li&gt;System tray integration&lt;/li&gt;
&lt;li&gt;Theme customization&lt;/li&gt;
&lt;li&gt;Report exporting&lt;/li&gt;
&lt;li&gt;Notification system&lt;/li&gt;
&lt;li&gt;Better application startup performance&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Modern Technology Stack
&lt;/h2&gt;

&lt;p&gt;SysPeek is powered by modern desktop technologies.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Desktop Framework&lt;/td&gt;
&lt;td&gt;Electron 43&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;React 19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;TypeScript 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build Tool&lt;/td&gt;
&lt;td&gt;Vite 7 + electron-vite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI Framework&lt;/td&gt;
&lt;td&gt;Ant Design 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Charts&lt;/td&gt;
&lt;td&gt;uPlot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardware Information&lt;/td&gt;
&lt;td&gt;systeminformation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Updates&lt;/td&gt;
&lt;td&gt;electron-updater&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Packaging&lt;/td&gt;
&lt;td&gt;electron-builder&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This stack enables fast development while delivering native desktop performance.&lt;/p&gt;




&lt;h2&gt;
  
  
  Live Performance Dashboard
&lt;/h2&gt;

&lt;p&gt;SysPeek now features a completely redesigned Task Manager-inspired dashboard.&lt;/p&gt;

&lt;p&gt;Monitor your system in real time with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live CPU utilization&lt;/li&gt;
&lt;li&gt;Memory usage&lt;/li&gt;
&lt;li&gt;Disk I/O&lt;/li&gt;
&lt;li&gt;Network throughput&lt;/li&gt;
&lt;li&gt;System uptime&lt;/li&gt;
&lt;li&gt;Per-core processor load&lt;/li&gt;
&lt;li&gt;Historical performance charts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything updates continuously without overwhelming system resources.&lt;/p&gt;




&lt;h2&gt;
  
  
  Comprehensive Hardware Information
&lt;/h2&gt;

&lt;p&gt;Beyond performance monitoring, SysPeek provides detailed hardware inspection across every major component.&lt;/p&gt;

&lt;h3&gt;
  
  
  Available Pages
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;System&lt;/li&gt;
&lt;li&gt;CPU&lt;/li&gt;
&lt;li&gt;Memory&lt;/li&gt;
&lt;li&gt;Graphics&lt;/li&gt;
&lt;li&gt;Display&lt;/li&gt;
&lt;li&gt;Storage&lt;/li&gt;
&lt;li&gt;Operating System&lt;/li&gt;
&lt;li&gt;Network&lt;/li&gt;
&lt;li&gt;WiFi&lt;/li&gt;
&lt;li&gt;Battery&lt;/li&gt;
&lt;li&gt;Audio&lt;/li&gt;
&lt;li&gt;Bluetooth&lt;/li&gt;
&lt;li&gt;USB Devices&lt;/li&gt;
&lt;li&gt;Printers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each section presents hardware specifications in a structured and easy-to-read format.&lt;/p&gt;




&lt;h2&gt;
  
  
  Process Explorer
&lt;/h2&gt;

&lt;p&gt;Version 2 introduces a significantly improved process manager.&lt;/p&gt;

&lt;p&gt;Features include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Running process list&lt;/li&gt;
&lt;li&gt;Search&lt;/li&gt;
&lt;li&gt;Sorting&lt;/li&gt;
&lt;li&gt;CPU usage&lt;/li&gt;
&lt;li&gt;Memory consumption&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Making it easier to identify resource-intensive applications.&lt;/p&gt;




&lt;h2&gt;
  
  
  Live History Charts
&lt;/h2&gt;

&lt;p&gt;Instead of displaying only current values, SysPeek now records live history.&lt;/p&gt;

&lt;p&gt;Track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU activity&lt;/li&gt;
&lt;li&gt;Memory usage&lt;/li&gt;
&lt;li&gt;Network traffic&lt;/li&gt;
&lt;li&gt;Disk utilization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;using smooth real-time charts powered by &lt;strong&gt;uPlot&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  System Tray Integration
&lt;/h2&gt;

&lt;p&gt;SysPeek continues monitoring even when minimized.&lt;/p&gt;

&lt;p&gt;The new system tray includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live CPU usage&lt;/li&gt;
&lt;li&gt;Memory usage&lt;/li&gt;
&lt;li&gt;Quick actions&lt;/li&gt;
&lt;li&gt;Restore application&lt;/li&gt;
&lt;li&gt;Exit application&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Desktop Notifications
&lt;/h2&gt;

&lt;p&gt;Stay informed when your system exceeds configured limits.&lt;/p&gt;

&lt;p&gt;SysPeek can notify you when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU usage becomes too high&lt;/li&gt;
&lt;li&gt;CPU temperature exceeds thresholds&lt;/li&gt;
&lt;li&gt;Memory usage becomes critical&lt;/li&gt;
&lt;li&gt;Disk utilization reaches configured limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thresholds are fully customizable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Settings &amp;amp; Customization
&lt;/h2&gt;

&lt;p&gt;The new Settings panel allows you to personalize the application.&lt;/p&gt;

&lt;p&gt;Available options include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dark &amp;amp; Light themes&lt;/li&gt;
&lt;li&gt;Accent colors&lt;/li&gt;
&lt;li&gt;Refresh intervals&lt;/li&gt;
&lt;li&gt;Launch at startup&lt;/li&gt;
&lt;li&gt;Tray behavior&lt;/li&gt;
&lt;li&gt;Notification thresholds&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Export System Reports
&lt;/h2&gt;

&lt;p&gt;Need to share your hardware information?&lt;/p&gt;

&lt;p&gt;SysPeek can export a complete machine report as JSON, making it useful for troubleshooting, diagnostics, or technical support.&lt;/p&gt;




&lt;h2&gt;
  
  
  Automatic Updates
&lt;/h2&gt;

&lt;p&gt;One of the biggest additions in version 2 is seamless application updates.&lt;/p&gt;

&lt;p&gt;SysPeek now includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub Releases integration&lt;/li&gt;
&lt;li&gt;Background update downloads&lt;/li&gt;
&lt;li&gt;Restart &amp;amp; Install workflow&lt;/li&gt;
&lt;li&gt;Multi-platform release pipeline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping the application up-to-date is now effortless.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security First
&lt;/h2&gt;

&lt;p&gt;Desktop applications often overlook security.&lt;/p&gt;

&lt;p&gt;SysPeek follows Electron's recommended security practices.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Context Isolation enabled&lt;/li&gt;
&lt;li&gt;Sandbox enabled&lt;/li&gt;
&lt;li&gt;Node Integration disabled&lt;/li&gt;
&lt;li&gt;Secure preload bridge&lt;/li&gt;
&lt;li&gt;Typed IPC communication&lt;/li&gt;
&lt;li&gt;Strict Content Security Policy&lt;/li&gt;
&lt;li&gt;Whitelisted API exposure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;System information is collected only in the Electron main process, while the React interface communicates through a carefully controlled IPC bridge.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cross-Platform Support
&lt;/h2&gt;

&lt;p&gt;SysPeek supports all major desktop operating systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Available Platforms
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Windows&lt;/li&gt;
&lt;li&gt;macOS&lt;/li&gt;
&lt;li&gt;Linux&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Production installers are available for each platform.&lt;/p&gt;




&lt;h2&gt;
  
  
  Development Experience
&lt;/h2&gt;

&lt;p&gt;The migration to &lt;strong&gt;electron-vite&lt;/strong&gt; dramatically improves development speed.&lt;/p&gt;

&lt;p&gt;Benefits include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Instant hot reload&lt;/li&gt;
&lt;li&gt;Faster builds&lt;/li&gt;
&lt;li&gt;Type-safe IPC&lt;/li&gt;
&lt;li&gt;Better project organization&lt;/li&gt;
&lt;li&gt;Shared TypeScript models&lt;/li&gt;
&lt;li&gt;Cleaner architecture&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project is now significantly easier to maintain and extend.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;Clone the repository and install dependencies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/shehari007/SysPeek-hwinfo-react-electron-app.git

&lt;span class="nb"&gt;cd &lt;/span&gt;SysPeek-hwinfo-react-electron-app

npm &lt;span class="nb"&gt;install

&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The development server launches Electron with hot reload for both the renderer and the main process.&lt;/p&gt;




&lt;h2&gt;
  
  
  Build Production Installers
&lt;/h2&gt;

&lt;p&gt;Generate installers for your platform using:&lt;/p&gt;

&lt;h3&gt;
  
  
  Windows
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build:win
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Linux
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build:linux
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  macOS
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build:mac
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Source Code
&lt;/h2&gt;

&lt;p&gt;SysPeek is completely open source and licensed under the MIT License.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Repository
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/shehari007/SysPeek-hwinfo-react-electron-app" rel="noopener noreferrer"&gt;https://github.com/shehari007/SysPeek-hwinfo-react-electron-app&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Latest Releases
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/shehari007/SysPeek-hwinfo-react-electron-app/releases" rel="noopener noreferrer"&gt;https://github.com/shehari007/SysPeek-hwinfo-react-electron-app/releases&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I Built SysPeek
&lt;/h2&gt;

&lt;p&gt;Most system information tools are either outdated, overly technical, or cluttered with interfaces that haven't evolved in years.&lt;/p&gt;

&lt;p&gt;I wanted to build something different—a modern desktop application that combines detailed hardware information with real-time monitoring, while following current Electron best practices for security, performance, and maintainability.&lt;/p&gt;

&lt;p&gt;SysPeek is the result of that vision.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;Future development will focus on expanding SysPeek even further.&lt;/p&gt;

&lt;p&gt;Planned features include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GPU benchmarking&lt;/li&gt;
&lt;li&gt;Historical analytics&lt;/li&gt;
&lt;li&gt;Plugin architecture&lt;/li&gt;
&lt;li&gt;Remote system monitoring&lt;/li&gt;
&lt;li&gt;Performance recording&lt;/li&gt;
&lt;li&gt;CSV &amp;amp; PDF report export&lt;/li&gt;
&lt;li&gt;Multi-language support&lt;/li&gt;
&lt;li&gt;Custom dashboard widgets&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;SysPeek v2.0.0 is by far the biggest release since the project began.&lt;/p&gt;

&lt;p&gt;From the migration to Electron + Vite and full TypeScript support to automatic updates, secure IPC communication, live monitoring, and a redesigned interface, every part of the application has been modernized.&lt;/p&gt;

&lt;p&gt;If you're looking for an open-source, cross-platform system monitor built with modern web technologies, I hope SysPeek becomes a useful addition to your toolkit.&lt;/p&gt;

&lt;p&gt;If you enjoy the project, consider giving it a ⭐ on GitHub.&lt;/p&gt;

&lt;p&gt;Every star, issue, feature request, and contribution helps shape future releases.&lt;/p&gt;

&lt;p&gt;Happy monitoring! 🚀&lt;/p&gt;

</description>
      <category>systeminformation</category>
      <category>electron</category>
      <category>hardwareinfo</category>
      <category>node</category>
    </item>
    <item>
      <title>⚡ViteDash 2.2: I halved my dashboard bundle, right after doubling it</title>
      <dc:creator>Muhammad Sheharyar Butt</dc:creator>
      <pubDate>Sat, 15 Aug 2026 21:12:19 +0000</pubDate>
      <link>https://dev.to/shehari007/vitedash-22-i-halved-my-dashboard-bundle-right-after-doubling-it-484c</link>
      <guid>https://dev.to/shehari007/vitedash-22-i-halved-my-dashboard-bundle-right-after-doubling-it-484c</guid>
      <description>&lt;p&gt;I maintain &lt;a href="https://github.com/shehari007/vitedash-vite-antd-dashboard-template" rel="noopener noreferrer"&gt;ViteDash&lt;/a&gt;, a free admin dashboard template built with React 19, Vite 8, and Ant Design 6. It has a clean sidebar I am genuinely proud of, twenty something pages, and a light and dark mode that runs entirely through Ant Design's theme algorithm rather than a pile of CSS overrides.&lt;/p&gt;

&lt;p&gt;It also shipped a single 1.6 MB JavaScript file and had a dependency it never declared.&lt;/p&gt;

&lt;p&gt;Version 2.2 is out. Here is what was broken, what I added, and the one performance lesson that surprised me enough to write about.&lt;/p&gt;

&lt;h2&gt;
  
  
  🐛 The bug that made the template unusable for some people
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@ant-design/icons&lt;/code&gt; was imported in 22 files. It was not in &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It worked fine on my machine. It works fine for anyone using npm, because npm flattens &lt;code&gt;node_modules&lt;/code&gt; and antd depends on the icons package, so the import resolves through a copy that happens to be sitting at the top level.&lt;/p&gt;

&lt;p&gt;It does not work with pnpm. pnpm keeps a strict &lt;code&gt;node_modules&lt;/code&gt; where a package can only import what it actually declared. Same story with Yarn PnP. Those users cloned the repo, ran install, ran dev, and got:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Failed to resolve import "@ant-design/icons"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My README told people to use pnpm. I had been shipping a template that a chunk of my audience could not start.&lt;/p&gt;

&lt;p&gt;The fix is one line in &lt;code&gt;package.json&lt;/code&gt;. The lesson is not. If you maintain anything that other people install, test the install with pnpm at least once. A transitive dependency you never asked for is not a dependency you have.&lt;/p&gt;

&lt;h2&gt;
  
  
  📦 The bundle: 1.6 MB in one file
&lt;/h2&gt;

&lt;p&gt;The production build was one chunk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dist/assets/index-BkAqHA3M.js   1,620,471 bytes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything. The sign in screen waited on the Kanban board, the invoice drawer, and all twenty something other pages before it painted.&lt;/p&gt;

&lt;p&gt;The fix is the boring, correct one. Every route becomes a lazy import:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Charts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/pages/charts/Charts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/pages/products/Products&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with a Suspense boundary inside the shell rather than around it, so the sidebar and header stay on screen while the next page downloads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MainLayout&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ErrorBoundary&lt;/span&gt; &lt;span class="na"&gt;resetKey&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PageLoader&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Outlet&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;ErrorBoundary&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;MainLayout&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That part went exactly how you would expect. The next part did not.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚠️ The chunking advice that made it worse
&lt;/h2&gt;

&lt;p&gt;Search for "Vite bundle too large" and you will find the same answer over and over: split your vendor code by library with &lt;code&gt;manualChunks&lt;/code&gt;. Group React here, antd there, charts somewhere else. Long term caching, smaller chunks, everyone wins.&lt;/p&gt;

&lt;p&gt;So I wrote it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;manualChunks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/node_modules&lt;/span&gt;&lt;span class="se"&gt;\/(&lt;/span&gt;&lt;span class="sr"&gt;recharts|d3-&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;charts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node_modules/@ant-design/icons&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;icons&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/node_modules&lt;/span&gt;&lt;span class="se"&gt;\/(&lt;/span&gt;&lt;span class="sr"&gt;antd|rc-|@rc-component&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;antd&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/node_modules&lt;/span&gt;&lt;span class="se"&gt;\/(&lt;/span&gt;&lt;span class="sr"&gt;react|react-dom|scheduler&lt;/span&gt;&lt;span class="se"&gt;)\/&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vendor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks tidy. It builds. The chunk list looks like a job well done.&lt;/p&gt;

&lt;p&gt;Then I measured what the browser actually downloads before first paint, by reading the modulepreload tags out of the built &lt;code&gt;index.html&lt;/code&gt; and adding up the files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eager raw   : 2,086 kB
eager gzip  : 643 kB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worse than shipping nothing at all. My route splitting had done its job, and my chunking config had undone it.&lt;/p&gt;

&lt;p&gt;Here is why. &lt;code&gt;manualChunks&lt;/code&gt; is a hard instruction, not a hint. When you say "every antd module goes in the antd chunk", the bundler obeys, including for the antd components that only the Kanban board imports. And a chunk is eager if &lt;em&gt;any&lt;/em&gt; module in it is reachable from the entry. The app shell uses &lt;code&gt;Layout&lt;/code&gt;, &lt;code&gt;Menu&lt;/code&gt;, and &lt;code&gt;Button&lt;/code&gt;, so the antd chunk is eager, so &lt;code&gt;Table&lt;/code&gt;, &lt;code&gt;Splitter&lt;/code&gt;, &lt;code&gt;Calendar&lt;/code&gt; and everything else rides along.&lt;/p&gt;

&lt;p&gt;Same story with Recharts. Only the Charts page imports it. Naming it as a chunk pulled it into the entry graph anyway, and 427 kB of chart library was downloading for people who never opened a chart.&lt;/p&gt;

&lt;p&gt;I deleted the whole thing and let the bundler decide:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eager raw   : 1,004 kB across 12 files
eager gzip  : 336 kB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Half. From deleting configuration.&lt;/p&gt;

&lt;p&gt;Automatic code splitting already knows what it is doing. A module reachable from the entry lands in the entry chunk. A module used by two lazy routes lands in a shared chunk that loads when either one does. A module used by one lazy route lands in that route's chunk. That is the behaviour you want, and &lt;code&gt;manualChunks&lt;/code&gt; overrides it with your guess.&lt;/p&gt;

&lt;p&gt;There are still good reasons to reach for it. If the analyzer shows one library genuinely duplicated across many chunks, group that library. But group it because you measured, not because a blog post said vendor splitting is good practice. I left this comment in &lt;code&gt;vite.config.js&lt;/code&gt; so future me does not repeat it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The obvious move is to group node_modules by library ("all of antd in one chunk"), and it is a trap. Measured on this app that grouping costs 643 kB gzipped on first load. Letting the bundler decide brings it down to 336 kB.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Final numbers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Before 2.2&lt;/th&gt;
&lt;th&gt;After 2.2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First load, uncompressed&lt;/td&gt;
&lt;td&gt;1,620 kB in one file&lt;/td&gt;
&lt;td&gt;1,004 kB across 12 files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First load, gzipped&lt;/td&gt;
&lt;td&gt;one chunk, everything&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;336 kB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chart library&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;only on &lt;code&gt;/dashboard/charts&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total JavaScript&lt;/td&gt;
&lt;td&gt;1,620 kB&lt;/td&gt;
&lt;td&gt;2,183 kB across 83 chunks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The total went up because the template gained charts, three locales, and seven pages. That is fine. What matters is that none of it loads until someone asks for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔐 Roles, because a template without them teaches the wrong habit
&lt;/h2&gt;

&lt;p&gt;Most free dashboard templates give you pages. Then you add your second user type and discover there is nowhere obvious to put that logic.&lt;/p&gt;

&lt;p&gt;2.2 has role based access control in three layers.&lt;/p&gt;

&lt;p&gt;The navigation tree declares who can see what:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/dashboard/roles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;labelKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nav.items.roles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ROLES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ADMIN&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The router enforces it for anyone who types the URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Route&lt;/span&gt; &lt;span class="na"&gt;element&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RequireRole&lt;/span&gt; &lt;span class="na"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ROLES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ADMIN&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Route&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"roles"&lt;/span&gt; &lt;span class="na"&gt;element&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Roles&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And there is a Roles and Permissions page showing the matrix, so the concept is visible rather than buried in a config file.&lt;/p&gt;

&lt;p&gt;One thing I made sure to write down in both the code and the security policy: &lt;strong&gt;none of this is security&lt;/strong&gt;. It runs on the user's machine. It stops honest people from wandering into the wrong screen, and it stops nobody with the developer tools open. The same rules have to exist on your server. A template that implies otherwise is doing real damage to whoever learns from it.&lt;/p&gt;

&lt;p&gt;Sign in with &lt;code&gt;admin@vitedash.dev&lt;/code&gt; or &lt;code&gt;editor@vitedash.dev&lt;/code&gt; on the demo and watch the sidebar change.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔌 A data layer you can actually replace
&lt;/h2&gt;

&lt;p&gt;Every page used to hardcode its data in an array at the top of the file. Nobody ships that, so the template was teaching a pattern that gets deleted on day one.&lt;/p&gt;

&lt;p&gt;Now everything that touches data lives in &lt;code&gt;src/services/&lt;/code&gt;, and each function returns mock data in the shape a real endpoint would:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getProducts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageSize&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// filtering, sorting, and paging happen here, the way a backend does them&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageSize&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swapping in a real API changes this file and nothing else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getProducts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;apiClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/products?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pages consume it through a small &lt;code&gt;useAsync&lt;/code&gt; hook, so loading, error, and empty states are the default rather than something you bolt on later. The hook has one detail worth stealing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;asyncFunction&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;asyncFunction&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That guard is why searching does not glitch. Without it, a slow response for "sh" can land after a fast one for "shoes" and quietly replace the newer results with the older ones. It is a two line fix for a bug that is miserable to reproduce.&lt;/p&gt;

&lt;p&gt;The Products page has a switch that makes the request fail on purpose, so you can see the error path without unplugging your network.&lt;/p&gt;

&lt;h2&gt;
  
  
  🌍 Three languages, one of them right to left
&lt;/h2&gt;

&lt;p&gt;English, Spanish, and Arabic, with the whole layout mirroring for Arabic.&lt;/p&gt;

&lt;p&gt;The good news is that Ant Design does most of the work. Its &lt;code&gt;ConfigProvider&lt;/code&gt; takes a &lt;code&gt;direction&lt;/code&gt; prop and flips every component. The interesting part is everything that is not a component:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The layout had &lt;code&gt;marginLeft&lt;/code&gt; pushing content past the sidebar. That becomes &lt;code&gt;marginInlineStart&lt;/code&gt;, which follows the document direction.&lt;/li&gt;
&lt;li&gt;The sidebar's box shadow points left. Physical, not logical, so it needs an explicit &lt;code&gt;[dir='rtl']&lt;/code&gt; override.&lt;/li&gt;
&lt;li&gt;The active indicator's border radius is &lt;code&gt;0 3px 3px 0&lt;/code&gt;. Also physical. Also needs mirroring.&lt;/li&gt;
&lt;li&gt;The mobile drawer's &lt;code&gt;placement&lt;/code&gt; prop is physical. It has to be flipped by hand.&lt;/li&gt;
&lt;li&gt;Recharts does not mirror at all. It lays axes out in document order, so inheriting RTL puts labels on the wrong side of a plot whose bars did not move. Every chart container is pinned to &lt;code&gt;dir="ltr"&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding a language is now one JSON file and one row in an array. If it is right to left, you set &lt;code&gt;dir: 'rtl'&lt;/code&gt; and everything above already handles it.&lt;/p&gt;

&lt;h2&gt;
  
  
  📏 The 40px axis, which I still like best
&lt;/h2&gt;

&lt;p&gt;This one is from the previous release, but it is the piece of the template I am fondest of, and it now has a test guarding it.&lt;/p&gt;

&lt;p&gt;The sidebar collapses from 248px to 80px. In most dashboards the icons visibly jump sideways when that happens, because Ant Design positions menu icons with two unrelated formulas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;expanded&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;itemMarginInline&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;inlineIndent&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;iconSize&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="nx"&gt;collapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;collapsedWidth&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing makes those equal. Unless you solve for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;  &lt;span class="o"&gt;===&lt;/span&gt;  &lt;span class="mi"&gt;80&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;  &lt;span class="o"&gt;===&lt;/span&gt;  &lt;span class="mi"&gt;40&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every icon centre sits at exactly 40px in both states, so collapsing is pure clipping with no re-layout and no drift. There is now a test that fails if anyone breaks the identity, which felt like the right way to protect a number that looks arbitrary until you know why it is not.&lt;/p&gt;

&lt;p&gt;Two traps if you ever tune it. &lt;code&gt;inlineIndent&lt;/code&gt; is a Menu &lt;strong&gt;prop&lt;/strong&gt;, not a theme token, because rc-menu writes &lt;code&gt;padding-left&lt;/code&gt; as an inline style that beats every class rule. And &lt;code&gt;collapsedIconSize&lt;/code&gt; must stay greater than or equal to &lt;code&gt;iconSize&lt;/code&gt;, because Ant Design's collapsed rule overrides the icon's &lt;code&gt;font-size&lt;/code&gt; but not its &lt;code&gt;min-width&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  ✨ Everything else
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Command palette&lt;/strong&gt; on Ctrl+K, indexed from the navigation tree and filtered by role, so new pages are searchable with nothing to register&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Theme customizer&lt;/strong&gt; for primary colour, corner radius, and density, all through &lt;code&gt;ConfigProvider&lt;/code&gt; tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Charts&lt;/strong&gt; with a &lt;code&gt;useChartTheme&lt;/code&gt; hook and a custom tooltip, because Recharts ships a white tooltip with a hardcoded border that looks broken the moment you switch to dark mode&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error boundaries&lt;/strong&gt;, one global and one per route keyed on the pathname, so a page that throws leaves the shell usable and navigating away recovers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seven new pages&lt;/strong&gt;: Charts, Products, Roles, Notifications, Activity Log, Lock Screen, Maintenance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;51 tests&lt;/strong&gt; with Vitest, and &lt;code&gt;npm run check&lt;/code&gt; runs lint, format, and tests together&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self hosted Inter&lt;/strong&gt; instead of a Google Fonts link, which unblocks first paint, works offline, and stops sending visitor IPs to a third party&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy configs&lt;/strong&gt; for Vercel, Netlify, and Docker with nginx&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🙅 What I left out on purpose
&lt;/h2&gt;

&lt;p&gt;The most common feedback on a template like this is "add TypeScript". I did not, and I want to say why.&lt;/p&gt;

&lt;p&gt;ViteDash is a lot of people's first React project. Plain JSX means every file can be read top to bottom without also knowing generics, utility types, and how to type a component that takes children. I added a &lt;code&gt;jsconfig.json&lt;/code&gt; so you still get autocomplete and path aliases in your editor, and left the type layer to whoever wants it.&lt;/p&gt;

&lt;p&gt;Same reasoning for TanStack Query, MSW, Storybook, and a state management library. Each one is a good tool and each one is another concept between a beginner and a working dashboard. The &lt;code&gt;useAsync&lt;/code&gt; hook is 45 lines and teaches the same lesson with zero dependencies.&lt;/p&gt;

&lt;p&gt;Small on purpose is a feature. It is also the thing most likely to get me argued with, and I am fine with that.&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Demo&lt;/strong&gt;: &lt;a href="https://vitedash.msyb.dev" rel="noopener noreferrer"&gt;vitedash.msyb.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source&lt;/strong&gt;: &lt;a href="https://github.com/shehari007/vitedash-vite-antd-dashboard-template" rel="noopener noreferrer"&gt;github.com/shehari007/vitedash-vite-antd-dashboard-template&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx degit shehari007/vitedash-vite-antd-dashboard-template my-dashboard
&lt;span class="nb"&gt;cd &lt;/span&gt;my-dashboard
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MIT licensed. Issues and pull requests welcome, and beginner questions are explicitly welcome too. That is who it is for.&lt;/p&gt;

&lt;p&gt;If you take one thing from this post, make it the boring one: measure what the browser downloads before first paint, not what the build log prints. Those are different numbers, and I had been reading the wrong one for a year.&lt;/p&gt;

</description>
      <category>vite</category>
      <category>antdesign</category>
      <category>dashboard</category>
      <category>adminpanel</category>
    </item>
    <item>
      <title>⚡Building AppBox: 36 Tools, One Tab</title>
      <dc:creator>Muhammad Sheharyar Butt</dc:creator>
      <pubDate>Sat, 15 Aug 2026 21:08:54 +0000</pubDate>
      <link>https://dev.to/shehari007/building-appbox-36-tools-one-tab-590n</link>
      <guid>https://dev.to/shehari007/building-appbox-36-tools-one-tab-590n</guid>
      <description>&lt;h2&gt;
  
  
  36 Tools, One Tab: Building AppBox
&lt;/h2&gt;

&lt;p&gt;I had a bookmarks folder called &lt;strong&gt;utils&lt;/strong&gt;. Thirty-something links to single-purpose&lt;br&gt;
websites: a percentage calculator, a JSON prettifier, a JWT decoder, a cron expression&lt;br&gt;
parser. Every one of them worked. Every one of them also shipped several megabytes of&lt;br&gt;
ads, a cookie banner, three analytics scripts, and a newsletter modal that appeared&lt;br&gt;
exactly when I pasted my token into the box.&lt;/p&gt;

&lt;p&gt;That last part is the one that bothered me. I was pasting things I should not have been&lt;br&gt;
pasting into pages I had never read the privacy policy of.&lt;/p&gt;

&lt;p&gt;So I deleted the folder and built &lt;a href="https://appbox.msyb.dev" rel="noopener noreferrer"&gt;AppBox&lt;/a&gt; 36 utilities in one&lt;br&gt;
app, no ads, no analytics, no account, and every calculation running on your own device.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8tb7eqjdxolvcn70ng42.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8tb7eqjdxolvcn70ng42.webp" alt="The AppBox home page, showing all 36 tools grouped by category" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  The constraint that shaped everything
&lt;/h2&gt;

&lt;p&gt;Here is the whole design brief: &lt;strong&gt;the app should work with the network cable unplugged.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That sounds like a feature. It is really a filter. Once "must work offline" is&lt;br&gt;
non-negotiable, a lot of decisions stop being decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No analytics, because there is nobody to send events to.&lt;/li&gt;
&lt;li&gt;No account system, because there is no server to hold accounts.&lt;/li&gt;
&lt;li&gt;No CDN fonts, no third-party embeds, no remote config.&lt;/li&gt;
&lt;li&gt;Your data lives in &lt;code&gt;localStorage&lt;/code&gt;, on your machine, and never leaves it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;34 of the 36 tools honour this absolutely, because there is genuinely nothing they need a&lt;br&gt;
server for. Converting 40 °C to Fahrenheit is arithmetic. Hashing a string is arithmetic.&lt;br&gt;
Parsing &lt;code&gt;*/15 9-17 * * 1-5&lt;/code&gt; into "every 15 minutes, 9am–5pm, weekdays" is a&lt;br&gt;
string-processing problem someone solved decades ago.&lt;/p&gt;

&lt;p&gt;The two exceptions are weather and currency. Both use free, key-less APIs, both cache&lt;br&gt;
their last successful response, and both show you how old that cached result is instead&lt;br&gt;
of pretending it is live. On a train, you still get something useful.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmv9r9lbu9v6droe51z3j.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmv9r9lbu9v6droe51z3j.webp" alt="Compound interest projection chart showing contributions versus growth" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  One codebase, two very different targets
&lt;/h2&gt;

&lt;p&gt;AppBox ships as a static website &lt;em&gt;and&lt;/em&gt; as a native desktop app for Windows, macOS and&lt;br&gt;
Linux. Same UI code, same logic, two builds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/                  Next.js 16 App Router — the renderer
├─ app/               one route per tool, ~8 lines each
├─ components/        tools, UI primitives, hand-rolled SVG charts
├─ lib/               domain logic, framework-free and unit-testable
└─ types/             the IPC contract, shared by preload and renderer

electron/             compiled by Vite → dist-electron/
├─ main.ts            window, app:// protocol, IPC, hardening
├─ preload.ts         the only bridge into the renderer
└─ menu.ts            native menu, generated from the registry

out/                  next build → Vercel  AND  → Electron over app://
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;People sometimes ask why there are two bundlers in one repo. They do different jobs and&lt;br&gt;
never overlap. &lt;strong&gt;Next.js&lt;/strong&gt; builds the interface, and its &lt;code&gt;output: 'export'&lt;/code&gt; mode emits&lt;br&gt;
real HTML per route — which is what gives each tool its own &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;, meta description,&lt;br&gt;
canonical URL and JSON-LD. The previous version of this project was a single&lt;br&gt;
client-rendered page, and it simply could not have that. &lt;strong&gt;Vite&lt;/strong&gt; compiles the Electron&lt;br&gt;
main and preload processes from TypeScript. It never touches the UI.&lt;/p&gt;
&lt;h3&gt;
  
  
  A registry as the single source of truth
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;src/lib/tools.ts&lt;/code&gt; holds every tool's slug, name, description, category, keywords and&lt;br&gt;
icon. Reading from that one array: the sidebar, the home grid, the command palette, the&lt;br&gt;
native desktop menu, &lt;code&gt;sitemap.xml&lt;/code&gt;, the PWA shortcuts, and every route's metadata.&lt;/p&gt;

&lt;p&gt;Adding a tool is one registry entry plus one eight-line route file. Navigation, search,&lt;br&gt;
the desktop menu, the sitemap and the SEO tags all pick it up on their own. There is no&lt;br&gt;
checklist of six files you must remember to touch — which matters, because the version of&lt;br&gt;
me who adds tool number 37 will not remember the checklist.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa9xyje40dvyolw3iwlxi.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa9xyje40dvyolw3iwlxi.webp" alt="The command palette searching across all tools" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Four decisions I would defend in code review
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. No charting library
&lt;/h3&gt;

&lt;p&gt;The finance and health tools needed charts. Recharts would have added roughly 100 KB to&lt;br&gt;
routes whose entire selling point is loading instantly.&lt;/p&gt;

&lt;p&gt;AppBox needs four chart shapes. Four. So &lt;code&gt;components/charts/&lt;/code&gt; is about 500 lines of&lt;br&gt;
inline SVG, and that is the end of it. The series colours are a fixed, colourblind-checked&lt;br&gt;
three-slot palette, deliberately kept independent of the per-tool accent colour — so&lt;br&gt;
"the green line" always means the same thing whether you are looking at a loan&lt;br&gt;
amortisation schedule or a calorie breakdown.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6thvnbj091kh388nufqa.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6thvnbj091kh388nufqa.webp" alt="Loan and EMI calculator with its amortisation chart and full schedule" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  2. No mathjs
&lt;/h3&gt;

&lt;p&gt;The scientific calculator needs to evaluate expressions. &lt;code&gt;mathjs&lt;/code&gt; does that beautifully,&lt;br&gt;
in about 500 KB.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;lib/expression.ts&lt;/code&gt; is a tokeniser plus a shunting-yard evaluator — a few hundred lines —&lt;br&gt;
handling degree/radian trig, postfix factorial and implicit multiplication. The size win&lt;br&gt;
is nice. The real win is error messages: because I own the parser, a malformed expression&lt;br&gt;
produces something specific enough to show the user, instead of a generic library throw.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2rocjuig596e5u2nvpp4.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2rocjuig596e5u2nvpp4.webp" alt="Calculator in scientific mode with reusable history" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  3. MD5, written by hand
&lt;/h3&gt;

&lt;p&gt;Web Crypto deliberately excludes MD5, and it is right to. But verifying a downloaded ISO&lt;br&gt;
against a published checksum is exactly the legacy case people still hit, and telling&lt;br&gt;
them "use a different algorithm" does not help when the checksum on the vendor's page is&lt;br&gt;
MD5.&lt;/p&gt;

&lt;p&gt;So it is implemented from scratch and fuzz-checked against Node's &lt;code&gt;crypto&lt;/code&gt; across 139&lt;br&gt;
inputs. Not glamorous. Correct.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Rendered Markdown gets sanitised
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;marked&lt;/code&gt; passes raw HTML straight through by default. Both the Markdown preview tool and&lt;br&gt;
the Notes tool run their output through DOMPurify before it reaches the DOM. In an app&lt;br&gt;
with no server, XSS is still absolutely a real problem — the attacker just has to get you&lt;br&gt;
to paste something.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Tailwind v4 bug that cost me an evening
&lt;/h2&gt;

&lt;p&gt;Every category has its own accent colour. A tool sets one &lt;code&gt;data-accent&lt;/code&gt; attribute and&lt;br&gt;
everything inside inherits that palette through CSS custom properties, so no tool&lt;br&gt;
component ever names a colour directly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc9assv6pe05hj4ty3v7t.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc9assv6pe05hj4ty3v7t.webp" alt="Colour tools showing formats, palettes and WCAG contrast checking" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This needs &lt;code&gt;@theme inline&lt;/code&gt; in Tailwind v4. With a plain &lt;code&gt;@theme&lt;/code&gt;, the substitution is&lt;br&gt;
frozen at &lt;code&gt;:root&lt;/code&gt; — so every tool dutifully renders in exactly the same colour and you&lt;br&gt;
spend an hour convinced your data attributes are not applying. They were. One keyword.&lt;/p&gt;


&lt;h2&gt;
  
  
  Hardening the desktop build
&lt;/h2&gt;

&lt;p&gt;The Electron renderer runs with &lt;code&gt;contextIsolation: true&lt;/code&gt;, &lt;code&gt;nodeIntegration: false&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;sandbox: true&lt;/code&gt;. It reaches the host only through the narrow, typed surface in&lt;br&gt;
&lt;code&gt;electron/preload.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The packaged app is served from a registered &lt;code&gt;app://&lt;/code&gt; scheme rather than &lt;code&gt;file://&lt;/code&gt;. That&lt;br&gt;
gives it a stable origin, which means &lt;code&gt;localStorage&lt;/code&gt;, &lt;code&gt;history.pushState&lt;/code&gt; and &lt;code&gt;fetch&lt;/code&gt;&lt;br&gt;
behave exactly the way they do on the web — so the desktop build is not quietly a&lt;br&gt;
different application with different bugs.&lt;/p&gt;


&lt;h2&gt;
  
  
  One deployment gotcha, in case you hit it
&lt;/h2&gt;

&lt;p&gt;Vercel's Next.js preset understands &lt;code&gt;output: 'export'&lt;/code&gt; on its own. &lt;code&gt;vercel.json&lt;/code&gt; only adds&lt;br&gt;
what the framework does not: cache headers and a CSP matching the desktop one.&lt;/p&gt;

&lt;p&gt;Do &lt;strong&gt;not&lt;/strong&gt; set &lt;code&gt;outputDirectory&lt;/code&gt; to &lt;code&gt;out&lt;/code&gt;. It reads as "look for the Next build in&lt;br&gt;
&lt;code&gt;out/&lt;/code&gt;", and the deploy fails with a missing &lt;code&gt;routes-manifest.json&lt;/code&gt; — that file lives in&lt;br&gt;
&lt;code&gt;.next/&lt;/code&gt;, which is exactly where the preset was already looking.&lt;/p&gt;

&lt;p&gt;Any static host works too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build
npx serve out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What I would tell past me
&lt;/h2&gt;

&lt;p&gt;Pick the constraint first. "Works offline" did more architectural work than any framework&lt;br&gt;
choice I made, because it answered a dozen questions before I got around to asking them.&lt;/p&gt;

&lt;p&gt;And put the registry in early. Every project accumulates a mental list of files you must&lt;br&gt;
update in lockstep, and every one of those lists eventually gets something wrong. Turning&lt;br&gt;
that list into one array is not clever. It is just the thing that lets a side project&lt;br&gt;
survive being ignored for three months.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://appbox.msyb.dev" rel="noopener noreferrer"&gt;appbox.msyb.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/shehari007/mini-react-electron-desktop-app" rel="noopener noreferrer"&gt;github.com/shehari007/appbox-multi-toolkit&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Desktop builds:&lt;/strong&gt; Windows (NSIS), macOS (DMG), Linux (AppImage + .deb)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MIT licensed. Issues and pull requests welcome — and if you add a tool, the sidebar,&lt;br&gt;
search, menu and sitemap will find it without you asking.&lt;/p&gt;

</description>
      <category>util</category>
      <category>nextjs</category>
      <category>electron</category>
      <category>vite</category>
    </item>
    <item>
      <title>🍬 Candy Logger v2.1.0 - The Correctness Release</title>
      <dc:creator>Muhammad Sheharyar Butt</dc:creator>
      <pubDate>Sat, 15 Aug 2026 21:06:00 +0000</pubDate>
      <link>https://dev.to/shehari007/candy-logger-v210-the-correctness-release-kbn</link>
      <guid>https://dev.to/shehari007/candy-logger-v210-the-correctness-release-kbn</guid>
      <description>&lt;p&gt;&lt;strong&gt;Candy Logger v2.1.0 is now available.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This release focuses on making Candy Logger &lt;strong&gt;correct, secure, reliable, and production-ready&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Version 2.1.0 improves serialization, security, console interception, persistence, UI behavior, accessibility, mobile support, and the underlying architecture. It also introduces a cleaner core API that allows Candy Logger to be used without rendering the UI.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/shehari007/candy-logger" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/candy-logger" rel="noopener noreferrer"&gt;npm Package&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Is Candy Logger?
&lt;/h2&gt;

&lt;p&gt;Candy Logger is a lightweight JavaScript and TypeScript logging library that provides a visual debugging experience directly inside your application.&lt;/p&gt;

&lt;p&gt;Instead of relying entirely on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Candy Logger provides a structured logging interface with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔎 Search and filtering&lt;/li&gt;
&lt;li&gt;🏷️ Tags&lt;/li&gt;
&lt;li&gt;📊 Log levels and counters&lt;/li&gt;
&lt;li&gt;📌 Pinned entries&lt;/li&gt;
&lt;li&gt;🧩 Structured object inspection&lt;/li&gt;
&lt;li&gt;📦 JSON export&lt;/li&gt;
&lt;li&gt;🎨 Themes&lt;/li&gt;
&lt;li&gt;📱 Mobile support&lt;/li&gt;
&lt;li&gt;♿ Accessibility features&lt;/li&gt;
&lt;li&gt;🔌 Custom sinks&lt;/li&gt;
&lt;li&gt;💾 Persistent logs&lt;/li&gt;
&lt;li&gt;🛡️ CSP-safe UI&lt;/li&gt;
&lt;li&gt;⚡ Dynamic UI loading&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Make application logging easier to understand without turning your project into a heavy logging framework.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why Version 2.1.0?
&lt;/h2&gt;

&lt;p&gt;The previous versions of Candy Logger focused primarily on building the logging experience.&lt;/p&gt;

&lt;p&gt;Version 2.1.0 focuses on what happens when real applications start sending &lt;strong&gt;real-world JavaScript values and runtime conditions&lt;/strong&gt; to the logger.&lt;/p&gt;

&lt;p&gt;Circular objects.&lt;/p&gt;

&lt;p&gt;Errors.&lt;/p&gt;

&lt;p&gt;Maps.&lt;/p&gt;

&lt;p&gt;Sets.&lt;/p&gt;

&lt;p&gt;DOM nodes.&lt;/p&gt;

&lt;p&gt;BigInts.&lt;/p&gt;

&lt;p&gt;Symbols.&lt;/p&gt;

&lt;p&gt;Strict Content Security Policies.&lt;/p&gt;

&lt;p&gt;React StrictMode.&lt;/p&gt;

&lt;p&gt;Hot Module Replacement.&lt;/p&gt;

&lt;p&gt;Corrupted &lt;code&gt;localStorage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Touch devices.&lt;/p&gt;

&lt;p&gt;Large log volumes.&lt;/p&gt;

&lt;p&gt;These edge cases expose whether a developer tool is actually robust.&lt;/p&gt;

&lt;p&gt;That's why version 2.1.0 is a &lt;strong&gt;correctness release&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's New in 2.1.0
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Safer Object Serialization
&lt;/h3&gt;

&lt;p&gt;Logging should never break the application being debugged.&lt;/p&gt;

&lt;p&gt;Previously, circular objects could cause a &lt;code&gt;TypeError&lt;/code&gt; to escape into the calling code.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;circularObject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

&lt;span class="nx"&gt;circularObject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;circularObject&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;circularObject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Candy Logger 2.1.0 safely handles circular references instead of allowing them to crash the logging operation.&lt;/p&gt;




&lt;h3&gt;
  
  
  Better Error Serialization
&lt;/h3&gt;

&lt;p&gt;JavaScript &lt;code&gt;Error&lt;/code&gt; objects contain valuable debugging information that isn't always enumerable.&lt;/p&gt;

&lt;p&gt;Previously:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Something went wrong&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;could effectively render as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Version 2.1.0 properly serializes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;message&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stack&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cause&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Custom Error properties&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Database connection failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;cause&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Connection timeout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DB_CONNECTION_FAILED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Candy Logger can now display the information developers actually need when diagnosing failures.&lt;/p&gt;




&lt;h3&gt;
  
  
  More JavaScript Types
&lt;/h3&gt;

&lt;p&gt;The serializer has been expanded to safely handle a much wider range of JavaScript values.&lt;/p&gt;

&lt;p&gt;Candy Logger 2.1.0 supports serialization of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Map&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Set&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Date&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RegExp&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BigInt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Symbol&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Functions&lt;/li&gt;
&lt;li&gt;DOM nodes&lt;/li&gt;
&lt;li&gt;Circular objects&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Error&lt;/code&gt; objects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means developers don't have to manually convert everything into plain JSON before logging it.&lt;/p&gt;




&lt;h3&gt;
  
  
  Security and XSS Protection
&lt;/h3&gt;

&lt;p&gt;Security is one of the most important parts of this release.&lt;/p&gt;

&lt;p&gt;Version 2.1.0 addresses XSS risks involving dynamically rendered values such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Object keys&lt;/li&gt;
&lt;li&gt;Tag labels&lt;/li&gt;
&lt;li&gt;Tag colors&lt;/li&gt;
&lt;li&gt;Log levels&lt;/li&gt;
&lt;li&gt;Custom action labels&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;img src=x onerror=alert(1)&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The value is treated as data rather than executable HTML.&lt;/p&gt;

&lt;p&gt;This makes the logging interface significantly safer when handling dynamically generated or untrusted values.&lt;/p&gt;




&lt;h3&gt;
  
  
  Strict CSP Support
&lt;/h3&gt;

&lt;p&gt;Candy Logger 2.1.0 is fully functional under a strict &lt;strong&gt;Content Security Policy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The panel no longer relies on inline handlers such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;onclick="..."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and no longer depends on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__candy&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;global variables.&lt;/p&gt;

&lt;p&gt;UI interactions are handled through proper event listeners and encapsulated runtime behavior.&lt;/p&gt;




&lt;h3&gt;
  
  
  Idempotent Console Override
&lt;/h3&gt;

&lt;p&gt;Modern development environments can execute initialization code more than once.&lt;/p&gt;

&lt;p&gt;React StrictMode and Hot Module Replacement are common examples.&lt;/p&gt;

&lt;p&gt;Previously, repeatedly calling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;overrideConsole&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;could result in stacked panels or duplicated logs.&lt;/p&gt;

&lt;p&gt;Version 2.1.0 makes &lt;code&gt;overrideConsole()&lt;/code&gt; &lt;strong&gt;idempotent&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Calling it multiple times no longer creates additional interception layers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;overrideConsole&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;overrideConsole&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;overrideConsole&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The console remains correctly overridden.&lt;/p&gt;




&lt;h3&gt;
  
  
  Safer Console Restoration
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;restoreConsole()&lt;/code&gt; has also been improved.&lt;/p&gt;

&lt;p&gt;It can no longer leave the browser's &lt;code&gt;console&lt;/code&gt; permanently hijacked after restoration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;overrideConsole&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Application code&lt;/span&gt;

&lt;span class="nf"&gt;restoreConsole&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The original console behavior is properly restored.&lt;/p&gt;




&lt;h3&gt;
  
  
  Accurate Log Counters
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;maxLogs&lt;/code&gt; previously caused level counters to drift upward indefinitely.&lt;/p&gt;

&lt;p&gt;For example, even after old entries had been evicted, counters could continue increasing.&lt;/p&gt;

&lt;p&gt;Version 2.1.0 keeps the counters synchronized with the actual core store.&lt;/p&gt;




&lt;h3&gt;
  
  
  Pinned Logs
&lt;/h3&gt;

&lt;p&gt;Pinned logs are now exempt from normal log eviction.&lt;/p&gt;

&lt;p&gt;If:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;maxLogs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is configured, pinned entries remain available even after the limit is reached.&lt;/p&gt;

&lt;p&gt;The same behavior applies to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pinned entries are preserved instead of being silently removed from &lt;code&gt;localStorage&lt;/code&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  Improved Search
&lt;/h3&gt;

&lt;p&gt;Search now matches text inside collapsed objects.&lt;/p&gt;

&lt;p&gt;Previously, searching depended on the rendered DOM, which meant content inside collapsed objects could be missed.&lt;/p&gt;

&lt;p&gt;Now the search operates against the underlying serialized data.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;developer@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The email can be found even while the object remains collapsed.&lt;/p&gt;




&lt;h3&gt;
  
  
  Improved JSON Highlighting
&lt;/h3&gt;

&lt;p&gt;JSON rendering has also been improved.&lt;/p&gt;

&lt;p&gt;Version 2.1.0 correctly highlights:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keys&lt;/li&gt;
&lt;li&gt;Strings&lt;/li&gt;
&lt;li&gt;Values&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Apostrophes are also rendered correctly instead of appearing as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;amp;#039;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Improved Light Theme
&lt;/h3&gt;

&lt;p&gt;The light theme now defines its own level badge colors.&lt;/p&gt;

&lt;p&gt;Previously, some level badges had poor contrast, reaching approximately &lt;strong&gt;1.6:1&lt;/strong&gt; in certain cases.&lt;/p&gt;

&lt;p&gt;Version 2.1.0 improves the contrast and readability of the light theme.&lt;/p&gt;




&lt;h3&gt;
  
  
  Mobile and Touch Support
&lt;/h3&gt;

&lt;p&gt;Candy Logger now provides a dedicated mobile experience.&lt;/p&gt;

&lt;p&gt;On screens below &lt;strong&gt;640px&lt;/strong&gt;, the panel becomes a full-screen sheet.&lt;/p&gt;

&lt;p&gt;Dragging also works with touch through pointer events, with the panel automatically clamped to the visible screen.&lt;/p&gt;




&lt;h3&gt;
  
  
  Accessibility Improvements
&lt;/h3&gt;

&lt;p&gt;Version 2.1.0 includes an accessibility pass covering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accessible names&lt;/li&gt;
&lt;li&gt;&lt;code&gt;aria-pressed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Live regions&lt;/li&gt;
&lt;li&gt;Focus rings&lt;/li&gt;
&lt;li&gt;Reduced-motion support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is to make the debugging interface usable with different input methods and accessibility settings.&lt;/p&gt;




&lt;h2&gt;
  
  
  New APIs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;destroy()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Both the logger and panel now support:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This removes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DOM elements&lt;/li&gt;
&lt;li&gt;Event listeners&lt;/li&gt;
&lt;li&gt;Injected stylesheets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is useful when loggers are dynamically mounted and unmounted.&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;code&gt;captureConsole()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;captureConsole()&lt;/code&gt; allows console output to be routed into an existing logger without replacing the entire logging architecture.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createLogger&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dispose&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;captureConsole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// console.log()&lt;/span&gt;
&lt;span class="c1"&gt;// console.warn()&lt;/span&gt;
&lt;span class="c1"&gt;// console.error()&lt;/span&gt;

&lt;span class="nf"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function returns a disposer for clean lifecycle management.&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;code&gt;addSink()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Logs can now be piped into external systems using sinks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addSink&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Send entry to your backend&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes integrations with services such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sentry&lt;/li&gt;
&lt;li&gt;Custom APIs&lt;/li&gt;
&lt;li&gt;Monitoring systems&lt;/li&gt;
&lt;li&gt;Analytics platforms&lt;/li&gt;
&lt;li&gt;Test spies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;much easier.&lt;/p&gt;




&lt;h3&gt;
  
  
  Core Logger API
&lt;/h3&gt;

&lt;p&gt;Version 2.1.0 introduces a cleaner core API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;createLogger&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;attachUI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;detachUI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;showPanel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;hidePanel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;isConsoleOverridden&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separates the logging core from the visual panel.&lt;/p&gt;

&lt;p&gt;The logger can therefore be used without rendering the UI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Console Format Specifiers
&lt;/h2&gt;

&lt;p&gt;Candy Logger now supports common console formatting specifiers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%s
%d
%i
%f
%o
%O
%j
%c
%%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;User %s has %d points&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Muhammad&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes Candy Logger behave more naturally when capturing or replacing native console output.&lt;/p&gt;




&lt;h2&gt;
  
  
  Better Scrolling
&lt;/h2&gt;

&lt;p&gt;The panel now uses sticky-bottom scrolling.&lt;/p&gt;

&lt;p&gt;If you're already at the bottom, new entries keep you at the bottom.&lt;/p&gt;

&lt;p&gt;If you've intentionally scrolled upward, new logs don't force you back down.&lt;/p&gt;

&lt;p&gt;Instead, Candy Logger displays an:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;N new
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;indicator.&lt;/p&gt;

&lt;p&gt;You can then decide when to return to the newest entries.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exported Serializer Primitives
&lt;/h2&gt;

&lt;p&gt;Developers can now use Candy Logger's serialization utilities independently.&lt;/p&gt;

&lt;p&gt;The following primitives are exported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;safeStringify&lt;/span&gt;
&lt;span class="nx"&gt;normalize&lt;/span&gt;
&lt;span class="nx"&gt;serializeError&lt;/span&gt;
&lt;span class="nx"&gt;formatArgs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the serialization system useful outside the panel itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  New Configuration Options
&lt;/h2&gt;

&lt;p&gt;Version 2.1.0 introduces several configuration options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;retainArgs
persistPins
dimWhenIdle
maxDepth
maxString
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These provide more control over memory usage, serialization depth, persistence, and panel behavior.&lt;/p&gt;

&lt;p&gt;The panel is now &lt;strong&gt;opaque by default&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The previous dimmed behavior can be enabled using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;dimWhenIdle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Build Formats
&lt;/h2&gt;

&lt;p&gt;Candy Logger now provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ESM&lt;/li&gt;
&lt;li&gt;CJS&lt;/li&gt;
&lt;li&gt;IIFE&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CDN builds are also available through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unpkg&lt;/li&gt;
&lt;li&gt;jsDelivr&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This allows Candy Logger to be used directly with a script tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://unpkg.com/candy-logger"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;without requiring a bundler.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;p&gt;Version 2.1.0 includes &lt;strong&gt;118 tests&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The tests cover important areas including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Serialization&lt;/li&gt;
&lt;li&gt;Console interception&lt;/li&gt;
&lt;li&gt;Restoration&lt;/li&gt;
&lt;li&gt;Persistence&lt;/li&gt;
&lt;li&gt;Log limits&lt;/li&gt;
&lt;li&gt;Pinned entries&lt;/li&gt;
&lt;li&gt;UI behavior&lt;/li&gt;
&lt;li&gt;Edge cases&lt;/li&gt;
&lt;li&gt;Configuration behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not just to add functionality, but to make that functionality predictable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture Improvements
&lt;/h2&gt;

&lt;p&gt;One of the most important changes in 2.1.0 is the separation between the logging core and the visual panel.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Store
&lt;/h3&gt;

&lt;p&gt;These methods now read directly from the core store:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;getLogs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;getStats&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They work even when no panel is attached.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createLogger&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogs&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getStats&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Dynamic Panel Loading
&lt;/h3&gt;

&lt;p&gt;The panel is now dynamically imported.&lt;/p&gt;

&lt;p&gt;This allows bundlers to remove the panel code from builds where the UI is never enabled.&lt;/p&gt;

&lt;p&gt;The architecture is effectively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
    │
    ├── Logger Core
    │
    └── Optional Panel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The visual interface is now an optional presentation layer rather than a mandatory part of the logging core.&lt;/p&gt;




&lt;h2&gt;
  
  
  Removed Legacy Code
&lt;/h2&gt;

&lt;p&gt;Version 2.1.0 removes unnecessary legacy functionality.&lt;/p&gt;

&lt;h3&gt;
  
  
  Removed &lt;code&gt;tableView&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;tableView&lt;/code&gt; option has been removed because there was no alternative view.&lt;/p&gt;

&lt;h3&gt;
  
  
  Removed v1 Terminal UI
&lt;/h3&gt;

&lt;p&gt;The old:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terminal-ui.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;implementation from v1 has also been removed.&lt;/p&gt;

&lt;p&gt;Candy Logger 2.x is now focused entirely on the newer architecture.&lt;/p&gt;




&lt;h2&gt;
  
  
  Backward Compatibility
&lt;/h2&gt;

&lt;p&gt;Candy Logger 2.1.0 maintains compatibility with the important v2 APIs.&lt;/p&gt;

&lt;p&gt;The following continue to work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;overrideConsole()
restoreConsole()
candy
CandyLogger
tagged()
getLogs()
getStats()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All v2.0 options continue to work except:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tableView
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The existing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;forceUI&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;option is also still accepted as an alias for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;enabled&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;Upgrade to version 2.1.0 with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;candy-logger@2.1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or install the latest release:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;candy-logger
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;A basic setup remains simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;overrideConsole&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;candy-logger&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;overrideConsole&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can then continue using the native console API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Application started&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;User authenticated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cache is getting full&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Something went wrong&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Candy Logger handles the rest.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;Candy Logger 2.1.0 establishes a stronger foundation for future development.&lt;/p&gt;

&lt;p&gt;Future improvements can build on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A separated logging core&lt;/li&gt;
&lt;li&gt;Safer serialization&lt;/li&gt;
&lt;li&gt;Extensible sinks&lt;/li&gt;
&lt;li&gt;Better lifecycle management&lt;/li&gt;
&lt;li&gt;Optional UI loading&lt;/li&gt;
&lt;li&gt;Improved accessibility&lt;/li&gt;
&lt;li&gt;Better mobile behavior&lt;/li&gt;
&lt;li&gt;Stronger test coverage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is to keep Candy Logger lightweight while making it increasingly useful as a developer tool.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Candy Logger 2.1.0 isn't defined by one huge feature.&lt;/p&gt;

&lt;p&gt;Instead, it is a collection of improvements that make the entire library more reliable.&lt;/p&gt;

&lt;p&gt;Better serialization.&lt;/p&gt;

&lt;p&gt;Better security.&lt;/p&gt;

&lt;p&gt;Better console interception.&lt;/p&gt;

&lt;p&gt;Better persistence.&lt;/p&gt;

&lt;p&gt;Better accessibility.&lt;/p&gt;

&lt;p&gt;Better mobile behavior.&lt;/p&gt;

&lt;p&gt;Better architecture.&lt;/p&gt;

&lt;p&gt;And &lt;strong&gt;118 tests&lt;/strong&gt; helping make sure those improvements continue working.&lt;/p&gt;

&lt;p&gt;That's why I consider &lt;strong&gt;2.1.0 the correctness release&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🍬 &lt;strong&gt;Candy Logger — because debugging doesn't have to be boring.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;⭐ &lt;a href="https://github.com/shehari007/candy-logger" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;a href="https://www.npmjs.com/package/candy-logger" rel="noopener noreferrer"&gt;npm Package&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📝 &lt;a href="https://dev.to/shehari007/candy-logger-v2-is-here-a-browser-logger-with-a-real-ui-bl2"&gt;Candy Logger v2 Announcement&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>npm</category>
      <category>logger</category>
      <category>debugging</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Rebuilding BICO v3.0.0 from scratch: worker threads, GPU shaders, and a contrast formula that got it wrong</title>
      <dc:creator>Muhammad Sheharyar Butt</dc:creator>
      <pubDate>Sat, 15 Aug 2026 21:02:57 +0000</pubDate>
      <link>https://dev.to/shehari007/rebuilding-bico-v300-from-scratch-worker-threads-gpu-shaders-and-a-contrast-formula-that-got-1ki5</link>
      <guid>https://dev.to/shehari007/rebuilding-bico-v300-from-scratch-worker-threads-gpu-shaders-and-a-contrast-formula-that-got-1ki5</guid>
      <description>&lt;h2&gt;
  
  
  Rebuilding BICO from scratch
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/shehari007/BICO-bulk-image-converter-optimizer-tool" rel="noopener noreferrer"&gt;BICO&lt;/a&gt; is a desktop app that&lt;br&gt;
converts and optimises images in bulk. Everything runs on your machine and nothing is uploaded.&lt;/p&gt;

&lt;p&gt;Version 2 worked, and I used it. It also had a problem I could not design around: the interface&lt;br&gt;
froze during every batch. Version 3 is a full rewrite, and this is what the rewrite actually&lt;br&gt;
involved, including the two bugs that taught me the most.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/shehari007/BICO-bulk-image-converter-optimizer-tool/releases" rel="noopener noreferrer"&gt;Download it&lt;/a&gt; ·&lt;br&gt;
&lt;a href="https://github.com/shehari007/BICO-bulk-image-converter-optimizer-tool" rel="noopener noreferrer"&gt;Source&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The freeze was architectural
&lt;/h2&gt;

&lt;p&gt;In version 2, &lt;a href="https://sharp.pixelplumbing.com" rel="noopener noreferrer"&gt;sharp&lt;/a&gt; ran on the renderer thread. Every conversion&lt;br&gt;
competed with the thing drawing the window, so the app stopped repainting for the length of a batch.&lt;br&gt;
No amount of spinners fixes that, because there is no thread left to animate the spinner.&lt;/p&gt;

&lt;p&gt;Version 3 moves encoding into a pool of native worker threads sized to the machine, off both the UI&lt;br&gt;
thread and the main process. The window now stays at full frame rate through a run, and the queue&lt;br&gt;
shows per file progress because there is a thread free to report it.&lt;/p&gt;

&lt;p&gt;That single change is most of why the app feels different, and it is not a feature anybody sees in a&lt;br&gt;
screenshot.&lt;/p&gt;
&lt;h2&gt;
  
  
  Putting the GPU to work without pretending
&lt;/h2&gt;

&lt;p&gt;Image codecs are CPU bound. A GPU cannot encode a JPEG for you. What it can do is the parts that&lt;br&gt;
genuinely parallelise: resize, sharpen, blur, colour adjustments, watermark compositing.&lt;/p&gt;

&lt;p&gt;BICO runs those as &lt;a href="https://www.w3.org/TR/webgpu/" rel="noopener noreferrer"&gt;WebGPU&lt;/a&gt; compute shaders. Machines with a discrete&lt;br&gt;
and an integrated adapter can drive both at once, each on its own lane. On an NVIDIA card that work&lt;br&gt;
executes on the CUDA cores.&lt;/p&gt;

&lt;p&gt;Two decisions matter more than the shaders:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fallback is silent and automatic.&lt;/strong&gt; Any device loss, driver reset or unsupported operation moves&lt;br&gt;
that image to the CPU pipeline. A converter that fails because a driver hiccupped is worse than one&lt;br&gt;
that is briefly slower.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The app tells you where each file ran.&lt;/strong&gt; Every row in the queue carries a GPU or CPU badge, and the&lt;br&gt;
run summary shows the split. If I claim acceleration, you get to check it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Shipping codecs that libvips does not have
&lt;/h2&gt;

&lt;p&gt;I wanted JPEG XL and JPEG 2000. No prebuilt copy of &lt;a href="https://www.libvips.org" rel="noopener noreferrer"&gt;libvips&lt;/a&gt; includes&lt;br&gt;
either, and asking users to compile a custom libvips is not a real answer for a desktop app.&lt;/p&gt;

&lt;p&gt;So BICO carries its own WebAssembly builds: &lt;a href="https://github.com/jamsinclair/jSquash" rel="noopener noreferrer"&gt;@jsquash/jxl&lt;/a&gt;&lt;br&gt;
for JPEG XL and &lt;a href="https://github.com/dlemstra/magick-wasm" rel="noopener noreferrer"&gt;ImageMagick compiled to WASM&lt;/a&gt; for&lt;br&gt;
JPEG 2000. They cost a few megabytes and behave identically on Windows, macOS and Linux.&lt;/p&gt;

&lt;p&gt;I tested ImageMagick for JPEG XL too, since it was already there. It produced &lt;em&gt;larger&lt;/em&gt; files at&lt;br&gt;
higher quality settings, which is inverted, so JPEG XL kept its own encoder. Measuring beats&lt;br&gt;
assuming, and it took one afternoon to find out.&lt;/p&gt;

&lt;p&gt;The About screen reports what your build actually resolved at startup rather than what the formats&lt;br&gt;
support in theory, and anything unavailable is greyed out in the picker instead of failing mid run.&lt;/p&gt;
&lt;h2&gt;
  
  
  The contrast formula that got it wrong
&lt;/h2&gt;

&lt;p&gt;Here is my favourite bug.&lt;/p&gt;

&lt;p&gt;BICO lets you pick any accent colour, so it has to choose black or white text to sit on top. I did&lt;br&gt;
the responsible thing and used the WCAG 2 contrast ratio, picking whichever scored higher.&lt;/p&gt;

&lt;p&gt;On the default blue, &lt;code&gt;#4c8dff&lt;/code&gt;, it chose &lt;strong&gt;black&lt;/strong&gt;. It looked awful.&lt;/p&gt;

&lt;p&gt;The maths was not wrong, it was just measuring the wrong thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;black on #4c8dff   6.56 : 1
white on #4c8dff   3.20 : 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;WCAG 2 relative luminance weights green at 0.7152 and blue at 0.0722. A saturated blue therefore&lt;br&gt;
scores as a &lt;em&gt;light&lt;/em&gt; colour, and black wins the ratio test on it. Every mainstream design system puts&lt;br&gt;
white on a blue button, and every one of them is right.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Myndex/SAPC-APCA" rel="noopener noreferrer"&gt;APCA&lt;/a&gt;, the perceptual model drafted for WCAG 3, gets it right:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;white on #4c8dff   Lc 64.2
black on #4c8dff   Lc 45.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the foreground is now chosen perceptually. But fixing that surfaced a second, better bug.&lt;/p&gt;

&lt;p&gt;Ant Design paints all three button states with &lt;strong&gt;one&lt;/strong&gt; text colour and three different backgrounds,&lt;br&gt;
and its hover shade is lighter while its pressed shade is darker. On the Nord and Dracula themes the&lt;br&gt;
pressed shade landed dark enough that the black text chosen for the resting colour was stranded on a&lt;br&gt;
mid tone.&lt;/p&gt;

&lt;p&gt;The fix was to stop letting the shades drift: hover and pressed are now generated in&lt;br&gt;
&lt;a href="https://bottosson.github.io/posts/oklab/" rel="noopener noreferrer"&gt;Oklab&lt;/a&gt;, moving lightness while keeping hue and chroma, and&lt;br&gt;
always &lt;em&gt;away&lt;/em&gt; from the text colour. Contrast rises as you interact with a button rather than falling.&lt;/p&gt;

&lt;p&gt;Then a third thing. The rendered button was not even the colour I was checking. Ant Design's dark&lt;br&gt;
algorithm regenerates the seed against a dark background, so &lt;code&gt;#4c8dff&lt;/code&gt; goes in and &lt;code&gt;#447bdc&lt;/code&gt; is&lt;br&gt;
painted. I had been judging a colour that never reached the screen, which is what left Nord and&lt;br&gt;
Dracula at a genuinely poor Lc 52 and 45.&lt;/p&gt;

&lt;p&gt;I verified the final version across every accent the colour picker can produce: 24,389 colours by&lt;br&gt;
three states, &lt;strong&gt;73,167 measurements&lt;/strong&gt;, zero states where the text colour disagrees with the&lt;br&gt;
background it is painted on.&lt;/p&gt;

&lt;p&gt;Three bugs stacked on one wrong assumption, and the only reason I found the second and third is that&lt;br&gt;
I measured the rendered DOM instead of trusting the theme config.&lt;/p&gt;

&lt;h2&gt;
  
  
  The installer that installed twice
&lt;/h2&gt;

&lt;p&gt;The other bug worth writing down.&lt;/p&gt;

&lt;p&gt;Version 3 changed the application id. Windows keys its uninstall entry on a GUID derived from that&lt;br&gt;
id, so as far as Windows was concerned, version 3 was an unrelated product. Installing it left two&lt;br&gt;
BICOs: two entries in Apps and Features, two Start Menu shortcuts, two program folders, neither&lt;br&gt;
aware of the other.&lt;/p&gt;

&lt;p&gt;The tempting fix is a custom uninstall script. The better fix was smaller: electron-builder already&lt;br&gt;
has a tested routine that finds a previous install, runs its uninstaller with retries, handles the&lt;br&gt;
app being open, and only then writes the new files. It just never fired, because it looks up the&lt;br&gt;
&lt;em&gt;current&lt;/em&gt; id's GUID.&lt;/p&gt;

&lt;p&gt;Pinning the installer's GUID back to the one version 2 registered under makes that existing machinery&lt;br&gt;
do the work. One line of configuration instead of a hundred lines of NSIS I would have had to debug&lt;br&gt;
on a virtual machine.&lt;/p&gt;

&lt;p&gt;The lesson I keep relearning: before writing the thing, check whether the tool already does it and&lt;br&gt;
you have simply not given it what it needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the interface speak three languages
&lt;/h2&gt;

&lt;p&gt;BICO ships in English, Turkish and Arabic, and Arabic flips the whole layout to right to left. Not a&lt;br&gt;
mirrored font, an actual mirror: panels, tables, sliders and drawers all change side.&lt;/p&gt;

&lt;p&gt;The part I would recommend to anyone doing this: the dictionaries are typed. Every language is&lt;br&gt;
declared as &lt;code&gt;Record&amp;lt;TranslationKey, string&amp;gt;&lt;/code&gt;, so a string added in English fails the build until&lt;br&gt;
Turkish and Arabic supply it. Nothing can quietly fall back.&lt;/p&gt;

&lt;p&gt;That caught a real bug months later. The format descriptions were being rendered straight from a&lt;br&gt;
capability table in shared code rather than from the dictionaries, so they stayed English in both&lt;br&gt;
translations and sat outside the contract entirely. The moment I moved those words into the&lt;br&gt;
dictionaries, the build broke with &lt;em&gt;missing 18 properties&lt;/em&gt;, which is exactly what should happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cutting the words back out
&lt;/h2&gt;

&lt;p&gt;Late on, I read the settings sidebar as a user rather than as its author, and it was exhausting.&lt;br&gt;
Every slider carried a paragraph explaining why the setting existed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Higher effort means smaller files and slower encoding. The image itself is unchanged, the encoder&lt;br&gt;
just searches harder for a cheaper way to store it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is 149 characters for one slider. It is now:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Higher effort means smaller files and slower encoding.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Across the interface the helper text went from roughly 26,000 characters to 15,800, and the sidebar&lt;br&gt;
average dropped from 117 characters a hint to 69. The facts and the warnings stayed. The reasoning&lt;br&gt;
went, because a hint is read while you are trying to do something else, not studied.&lt;/p&gt;

&lt;h2&gt;
  
  
  What version 3 is made of
&lt;/h2&gt;

&lt;p&gt;Electron 43, React 19, TypeScript 5.9, &lt;a href="https://ant.design" rel="noopener noreferrer"&gt;Ant Design&lt;/a&gt; 6, sharp on libvips 8.18,&lt;br&gt;
built with &lt;a href="https://electron-vite.org" rel="noopener noreferrer"&gt;electron-vite&lt;/a&gt;. Node integration is gone from the renderer,&lt;br&gt;
context isolation is on, and every path the interface touches crosses a typed IPC contract compiled&lt;br&gt;
by all four processes.&lt;/p&gt;

&lt;p&gt;The interface is set in &lt;a href="https://www.ibm.com/plex/" rel="noopener noreferrer"&gt;IBM Plex&lt;/a&gt;, bundled rather than fetched, chosen&lt;br&gt;
because its Arabic is a designed companion to the Latin rather than an unrelated fallback face.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Download:&lt;/strong&gt; &lt;a href="https://github.com/shehari007/BICO-bulk-image-converter-optimizer-tool/releases" rel="noopener noreferrer"&gt;releases&lt;/a&gt;
for Windows, macOS and Linux&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source and issues:&lt;/strong&gt;
&lt;a href="https://github.com/shehari007/BICO-bulk-image-converter-optimizer-tool" rel="noopener noreferrer"&gt;github.com/shehari007/BICO&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Me:&lt;/strong&gt; &lt;a href="https://github.com/shehari007" rel="noopener noreferrer"&gt;github.com/shehari007&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is MIT licensed. If you convert images in bulk and something annoys you about it, open an issue.&lt;br&gt;
The bugs above were all found by being annoyed at my own software and then actually measuring why.&lt;/p&gt;

</description>
      <category>electron</category>
      <category>vite</category>
      <category>react</category>
      <category>imageprocessing</category>
    </item>
    <item>
      <title>🩺 MD Doctor - A Modern Open-Source Markdown Editor Built with Next.js 16 &amp; React 19</title>
      <dc:creator>Muhammad Sheharyar Butt</dc:creator>
      <pubDate>Mon, 06 Jul 2026 08:33:41 +0000</pubDate>
      <link>https://dev.to/shehari007/md-doctor-a-modern-open-source-markdown-editor-built-with-nextjs-16-react-19-1mbk</link>
      <guid>https://dev.to/shehari007/md-doctor-a-modern-open-source-markdown-editor-built-with-nextjs-16-react-19-1mbk</guid>
      <description>&lt;p&gt;I built &lt;strong&gt;MD Doctor&lt;/strong&gt;, a professional Markdown workspace designed for developers, technical writers, students, and anyone who loves writing in Markdown.&lt;/p&gt;

&lt;p&gt;Unlike many editors, &lt;strong&gt;MD Doctor uses a custom editor built from scratch&lt;/strong&gt; instead of Monaco, CodeMirror, or TipTap, giving complete control over the editing experience while remaining lightweight and fast.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✨ Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;📝 Custom Markdown editor built from scratch&lt;/li&gt;
&lt;li&gt;⚡ Live GitHub-Flavored Markdown preview&lt;/li&gt;
&lt;li&gt;📊 Mermaid diagram support&lt;/li&gt;
&lt;li&gt;➗ KaTeX mathematical expressions&lt;/li&gt;
&lt;li&gt;💻 Syntax-highlighted code blocks&lt;/li&gt;
&lt;li&gt;📄 Import Markdown and HTML (HTML is converted to Markdown)&lt;/li&gt;
&lt;li&gt;📤 Export to Markdown, standalone HTML, and PDF&lt;/li&gt;
&lt;li&gt;🔒 Offline-first with IndexedDB storage&lt;/li&gt;
&lt;li&gt;🌍 English, Türkçe, العربية, and اردو with full RTL support&lt;/li&gt;
&lt;li&gt;🎨 Built with Next.js 16, React 19, Material UI v9, Motion, and TypeScript&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🔗 Links
&lt;/h3&gt;

&lt;p&gt;🌐 &lt;strong&gt;Live Demo:&lt;/strong&gt; &lt;a href="https://mddoctor.msyb.dev" rel="noopener noreferrer"&gt;https://mddoctor.msyb.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;📦 &lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://github.com/shehari007/md-doctor-next-js" rel="noopener noreferrer"&gt;https://github.com/shehari007/md-doctor-next-js&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👤 &lt;strong&gt;GitHub Profile:&lt;/strong&gt; &lt;a href="https://github.com/shehari007" rel="noopener noreferrer"&gt;https://github.com/shehari007&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you try it out, I'd love to hear your feedback. Feature requests, bug reports, and pull requests are always welcome!&lt;/p&gt;

&lt;h1&gt;
  
  
  nextjs #react #typescript #opensource #markdown #webdev #frontend #materialui #javascript #developer
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>markdown</category>
      <category>nextjs</category>
      <category>react</category>
    </item>
    <item>
      <title>⚡Stop Building Admin Dashboards from Scratch Meet ViteDash</title>
      <dc:creator>Muhammad Sheharyar Butt</dc:creator>
      <pubDate>Wed, 01 Jul 2026 11:23:15 +0000</pubDate>
      <link>https://dev.to/shehari007/stop-building-admin-dashboards-from-scratch-meet-vitedash-3mjl</link>
      <guid>https://dev.to/shehari007/stop-building-admin-dashboards-from-scratch-meet-vitedash-3mjl</guid>
      <description>&lt;p&gt;🚀 Just udpated &lt;strong&gt;ViteDash&lt;/strong&gt; a modern, lightweight Admin Dashboard Template built for developers who want to prototype, build, and ship faster.&lt;/p&gt;

&lt;p&gt;Instead of starting every project from scratch, ViteDash gives you a clean, responsive foundation with everything needed to kick off your next application.&lt;/p&gt;

&lt;p&gt;✨ Built with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⚛️ React 19&lt;/li&gt;
&lt;li&gt;⚡ Vite 8&lt;/li&gt;
&lt;li&gt;🎨 Ant Design 6&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Features
&lt;/h3&gt;

&lt;p&gt;✅ Fully Responsive Layout&lt;br&gt;
✅ Light &amp;amp; Dark Mode&lt;br&gt;
✅ Authentication Pages (Sign In, Sign Up &amp;amp; Forgot Password)&lt;br&gt;
✅ Dashboard, Analytics, Tables, Forms, Users, Profile &amp;amp; Settings Pages&lt;br&gt;
✅ Reusable Components&lt;br&gt;
✅ Clean &amp;amp; Scalable Project Structure&lt;br&gt;
✅ Minimal Dependencies&lt;br&gt;
✅ TypeScript Ready&lt;/p&gt;

&lt;p&gt;Perfect for building:&lt;br&gt;
• Admin Panels&lt;br&gt;
• SaaS Dashboards&lt;br&gt;
• ERP &amp;amp; CRM Systems&lt;br&gt;
• Internal Business Tools&lt;br&gt;
• Management Portals&lt;/p&gt;

&lt;p&gt;🌐 &lt;strong&gt;Live Demo&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://vitedash.msyb.dev" rel="noopener noreferrer"&gt;https://vitedash.msyb.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💻 &lt;strong&gt;GitHub&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://github.com/shehari007/vitedash-vite-antd-dashboard-template" rel="noopener noreferrer"&gt;https://github.com/shehari007/vitedash-vite-antd-dashboard-template&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're looking for a fast starting point for your next React project, give it a try. Feedback, feature requests, and GitHub ⭐️s are always appreciated!&lt;/p&gt;

&lt;h1&gt;
  
  
  React #Vite #AntDesign #Dashboard #AdminTemplate #OpenSource #TypeScript #Frontend #WebDevelopment #ReactJS #JavaScript #UI #Developer
&lt;/h1&gt;

</description>
    </item>
    <item>
      <title>🚀 Candy Logger v2 is here — a browser logger with a real UI</title>
      <dc:creator>Muhammad Sheharyar Butt</dc:creator>
      <pubDate>Sat, 28 Mar 2026 17:20:27 +0000</pubDate>
      <link>https://dev.to/shehari007/candy-logger-v2-is-here-a-browser-logger-with-a-real-ui-bl2</link>
      <guid>https://dev.to/shehari007/candy-logger-v2-is-here-a-browser-logger-with-a-real-ui-bl2</guid>
      <description>&lt;p&gt;I just shipped &lt;strong&gt;Candy Logger v2&lt;/strong&gt; — a major rewrite of my JavaScript/TypeScript logging library.&lt;/p&gt;

&lt;p&gt;Candy Logger is now a &lt;strong&gt;browser-first logger&lt;/strong&gt; with a floating UI that makes debugging much easier during development. Instead of just printing plain console messages, v2 gives you a structured table view, tagged logs, color-coded levels, real-time search/filtering, dark/light theme support, JSON export, and draggable/resizable UI controls. It is also &lt;strong&gt;zero-dependency&lt;/strong&gt;. (&lt;a href="https://github.com/shehari007/candy-logger" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;One important change in this release: &lt;strong&gt;v2 is browser-only&lt;/strong&gt;. I removed the old Node.js / terminal support, so if you still need the legacy terminal experience, &lt;code&gt;v1.x&lt;/code&gt; is the version to stay on. This release is focused on giving frontend developers a much better in-browser debugging workflow. (&lt;a href="https://github.com/shehari007/candy-logger" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s new in v2?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Complete browser-focused rewrite&lt;/li&gt;
&lt;li&gt;New &lt;strong&gt;table-view logger UI&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Custom &lt;strong&gt;tagged logging&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;6 color-coded log levels&lt;/li&gt;
&lt;li&gt;Per-row actions like copy, bookmark, and delete&lt;/li&gt;
&lt;li&gt;Real-time search and filtering&lt;/li&gt;
&lt;li&gt;Dark / light theme toggle&lt;/li&gt;
&lt;li&gt;JSON export&lt;/li&gt;
&lt;li&gt;Pin, drag, and resize support&lt;/li&gt;
&lt;li&gt;Collapsible JSON previews&lt;/li&gt;
&lt;li&gt;TypeScript support&lt;/li&gt;
&lt;li&gt;Works with React, Vue, Angular, Svelte, and Next.js client-side setups (&lt;a href="https://github.com/shehari007/candy-logger" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;overrideConsole&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;candy-logger&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;overrideConsole&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;forceUI&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello World!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;User signed in&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Disk usage &amp;gt; 90%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Payment failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CARD_DECLINED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is basically it — once enabled, your browser logs become much more visual and easier to inspect. (&lt;a href="https://github.com/shehari007/candy-logger" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I made this
&lt;/h2&gt;

&lt;p&gt;A lot of logging tools are either too minimal, terminal-focused, or not pleasant to use when debugging frontend apps. I wanted something lightweight but still practical: something that feels modern, looks clean, and helps organize logs instead of turning them into noise.&lt;/p&gt;

&lt;p&gt;Candy Logger v2 is my attempt to make browser debugging more usable and a bit more fun.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;code&gt;shehari007/candy-logger&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;code&gt;candy-logger&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version:&lt;/strong&gt; &lt;code&gt;2.0.0&lt;/code&gt; (&lt;a href="https://github.com/shehari007/candy-logger" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’d really appreciate feedback, feature ideas, and stars on the repo.&lt;br&gt;
If you try it in one of your projects, let me know what you think.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>npm</category>
      <category>node</category>
      <category>typescript</category>
    </item>
    <item>
      <title>🚀 Arduino Live System Monitor Using Node.js (jhonny-five)</title>
      <dc:creator>Muhammad Sheharyar Butt</dc:creator>
      <pubDate>Fri, 02 Jan 2026 09:09:31 +0000</pubDate>
      <link>https://dev.to/shehari007/arduino-live-system-monitor-using-nodejs-jhonny-five-4aec</link>
      <guid>https://dev.to/shehari007/arduino-live-system-monitor-using-nodejs-jhonny-five-4aec</guid>
      <description>&lt;h2&gt;
  
  
  Arduino Live System Monitor Using Node.js
&lt;/h2&gt;

&lt;p&gt;A small hobby project that connects &lt;strong&gt;Node.js&lt;/strong&gt; with &lt;strong&gt;Arduino UNO R3&lt;/strong&gt; to display &lt;strong&gt;live system statistics&lt;/strong&gt; on an &lt;strong&gt;I2C LCD module&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
The entire project is written in &lt;strong&gt;pure JavaScript&lt;/strong&gt;, using &lt;strong&gt;Johnny-Five&lt;/strong&gt; for serial communication and &lt;strong&gt;Standard Firmata&lt;/strong&gt; on the Arduino side.&lt;/p&gt;

&lt;p&gt;The goal is to provide a lightweight, hardware-based system monitor that runs independently of any desktop UI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;CPU usage (%)&lt;/li&gt;
&lt;li&gt;Memory usage (%)&lt;/li&gt;
&lt;li&gt;Download speed (MB/s)&lt;/li&gt;
&lt;li&gt;Upload speed (MB/s)&lt;/li&gt;
&lt;li&gt;Automatically detects the active network adapter&lt;/li&gt;
&lt;li&gt;Live updates via serial communication&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Screenshot
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhc3q5qqzq2sfihn7y4si.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhc3q5qqzq2sfihn7y4si.jpg" alt="Arduino Live System Monitor" width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Motivation
&lt;/h2&gt;

&lt;p&gt;Most system monitoring tools are software-based and tied to the operating system UI.&lt;br&gt;&lt;br&gt;
This project explores a different approach by moving system metrics to a &lt;strong&gt;physical display&lt;/strong&gt;, controlled entirely through JavaScript.&lt;/p&gt;

&lt;p&gt;It also demonstrates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Using JavaScript for hardware control&lt;/li&gt;
&lt;li&gt;Real-time serial communication with Arduino&lt;/li&gt;
&lt;li&gt;Practical usage of Johnny-Five beyond basic LEDs and sensors&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Node.js (&amp;gt;= 18, LTS recommended)&lt;/li&gt;
&lt;li&gt;Johnny-Five&lt;/li&gt;
&lt;li&gt;Arduino UNO R3&lt;/li&gt;
&lt;li&gt;Standard Firmata&lt;/li&gt;
&lt;li&gt;I2C LCD Module&lt;/li&gt;
&lt;li&gt;PCF8574T I2C Controller (configurable)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 18 or higher&lt;/li&gt;
&lt;li&gt;Arduino UNO R3 with &lt;strong&gt;Standard Firmata&lt;/strong&gt; flashed
&lt;a href="https://www.instructables.com/Arduino-Installing-Standard-Firmata/" rel="noopener noreferrer"&gt;https://www.instructables.com/Arduino-Installing-Standard-Firmata/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;I2C LCD Module&lt;/li&gt;
&lt;li&gt;PCF8574T I2C Controller
(Controller address can be changed in &lt;code&gt;index.js&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Possible Improvements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GPU usage support&lt;/li&gt;
&lt;li&gt;Temperature and fan speed monitoring&lt;/li&gt;
&lt;li&gt;Support for other boards (Mega, Nano, ESP32)&lt;/li&gt;
&lt;li&gt;Configurable refresh intervals&lt;/li&gt;
&lt;li&gt;Multi-page LCD views&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Source Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/shehari007/node-arduino-system-monitor" rel="noopener noreferrer"&gt;Link&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Contributions and improvements are welcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running Locally
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Connect the Arduino to your PC via USB&lt;/li&gt;
&lt;li&gt;Install dependencies and start the app:&lt;/li&gt;
&lt;/ol&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
bash
npm install &amp;amp;&amp;amp; node index.js

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>programming</category>
      <category>node</category>
      <category>arduino</category>
      <category>javascript</category>
    </item>
    <item>
      <title>🚀 AppBox — An Open, All-in-One Utility Toolbox (React + Electron)</title>
      <dc:creator>Muhammad Sheharyar Butt</dc:creator>
      <pubDate>Thu, 18 Dec 2025 13:47:24 +0000</pubDate>
      <link>https://dev.to/shehari007/appbox-an-open-all-in-one-utility-toolbox-react-electron-2c0j</link>
      <guid>https://dev.to/shehari007/appbox-an-open-all-in-one-utility-toolbox-react-electron-2c0j</guid>
      <description>&lt;p&gt;As developers, we constantly rely on small utility tools — calculators, converters, timers, clocks, quick todos, weather checks. Most of them live as separate apps or browser tabs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AppBox&lt;/strong&gt; is an attempt to bring all of those everyday utilities into &lt;strong&gt;one extensible, open-source application&lt;/strong&gt; — and more importantly, to make it &lt;strong&gt;easy for others to contribute new utilities&lt;/strong&gt; and grow it into a true all-in-one toolbox.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Live demo (web preview):&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://appbox.msyb.dev/" rel="noopener noreferrer"&gt;Link&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;GitHub repository:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/shehari007/mini-react-electron-desktop-app" rel="noopener noreferrer"&gt;Link&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What is AppBox?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;AppBox&lt;/strong&gt; is a modern utility suite built with &lt;strong&gt;React&lt;/strong&gt;, &lt;strong&gt;Ant Design&lt;/strong&gt;, and &lt;strong&gt;Electron&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
It runs as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🖥 A &lt;strong&gt;desktop app&lt;/strong&gt; (Windows / macOS / Linux)&lt;/li&gt;
&lt;li&gt;🌐 A &lt;strong&gt;web app&lt;/strong&gt;, using the same codebase&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The idea is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One app. Many small tools. Easy to extend.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why AppBox?
&lt;/h2&gt;

&lt;p&gt;Most utility apps try to do everything themselves. AppBox takes a different approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each utility is &lt;strong&gt;self-contained&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;New utilities can be added without touching the core logic&lt;/li&gt;
&lt;li&gt;No backend required — everything persists locally&lt;/li&gt;
&lt;li&gt;Designed to &lt;strong&gt;grow over time with community contributions&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AppBox is not “finished” — it is intentionally &lt;strong&gt;open-ended&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Current Utilities
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🧮 Calculator
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Basic and scientific modes&lt;/li&gt;
&lt;li&gt;Calculation history&lt;/li&gt;
&lt;li&gt;Full keyboard support&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🔁 Converters
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Length, weight, temperature, speed&lt;/li&gt;
&lt;li&gt;Time, storage, area, volume&lt;/li&gt;
&lt;li&gt;Instant conversion with unit swapping&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ✅ Todo List
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Add, complete, delete tasks&lt;/li&gt;
&lt;li&gt;Filters: all / active / completed&lt;/li&gt;
&lt;li&gt;Clear completed or reset all&lt;/li&gt;
&lt;li&gt;Local persistence&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ⏱ Clock &amp;amp; Timer
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Digital clock&lt;/li&gt;
&lt;li&gt;Stopwatch&lt;/li&gt;
&lt;li&gt;Countdown timer&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🌍 World Clock
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Add and remove cities&lt;/li&gt;
&lt;li&gt;Multi-timezone overview&lt;/li&gt;
&lt;li&gt;Persisted locally&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ☁️ Weather
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;City-based weather lookup&lt;/li&gt;
&lt;li&gt;Configurable API key&lt;/li&gt;
&lt;li&gt;Saved default city&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Vision: A True All-in-One Toolbox
&lt;/h2&gt;

&lt;p&gt;The real goal of AppBox is not the current feature list — it’s what &lt;strong&gt;can be added next&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Examples of future utilities that would fit perfectly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Password generator&lt;/li&gt;
&lt;li&gt;UUID / hash generator&lt;/li&gt;
&lt;li&gt;JSON / JWT viewer&lt;/li&gt;
&lt;li&gt;Color picker &amp;amp; palette tools&lt;/li&gt;
&lt;li&gt;Regex tester&lt;/li&gt;
&lt;li&gt;Markdown previewer&lt;/li&gt;
&lt;li&gt;Notes or scratchpad&lt;/li&gt;
&lt;li&gt;Developer-focused utilities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If it’s a &lt;strong&gt;small, useful tool&lt;/strong&gt;, it belongs in AppBox.&lt;/p&gt;




&lt;h2&gt;
  
  
  Contributing New Utilities 🚀
&lt;/h2&gt;

&lt;p&gt;Contributions are highly encouraged — especially &lt;strong&gt;new utility modules&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You don’t need to refactor the app or understand everything to contribute:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Utilities are UI-driven and isolated&lt;/li&gt;
&lt;li&gt;Most contributions can live in their own component/page&lt;/li&gt;
&lt;li&gt;State is local and persistence is simple (&lt;code&gt;localStorage&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;No backend or database required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have ever built:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a small React tool,&lt;/li&gt;
&lt;li&gt;a side utility,&lt;/li&gt;
&lt;li&gt;or a “weekend project” feature,&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…it can probably be added to AppBox.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to Contribute
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Fork the repository
&lt;/li&gt;
&lt;li&gt;Create a new utility module/page
&lt;/li&gt;
&lt;li&gt;Register it in the navigation
&lt;/li&gt;
&lt;li&gt;Submit a pull request&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Even small utilities are welcome.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; React 19&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI:&lt;/strong&gt; Ant Design 6&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Desktop:&lt;/strong&gt; Electron&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Math utilities:&lt;/strong&gt; mathjs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build &amp;amp; packaging:&lt;/strong&gt; react-scripts, electron-builder&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Running the Project
&lt;/h2&gt;

&lt;p&gt;Clone the repo:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
bash
git clone https://github.com/shehari007/mini-react-electron-desktop-app.git
cd mini-react-electron-desktop-app
npm install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>opensource</category>
      <category>showdev</category>
      <category>productivity</category>
      <category>react</category>
    </item>
    <item>
      <title>🚀 Shorty URL — A Modern, Secure URL Shortener Built with React &amp; Express (New Release)</title>
      <dc:creator>Muhammad Sheharyar Butt</dc:creator>
      <pubDate>Mon, 15 Dec 2025 07:29:17 +0000</pubDate>
      <link>https://dev.to/shehari007/shorty-url-a-modern-secure-url-shortener-built-with-react-express-new-release-50e8</link>
      <guid>https://dev.to/shehari007/shorty-url-a-modern-secure-url-shortener-built-with-react-express-new-release-50e8</guid>
      <description>&lt;p&gt;I’m excited to introduce Shorty URL, a modern full-stack URL shortener designed for real-world use. The goal of this project was to go beyond basic link shortening and deliver a secure, analytics-driven, and visually polished experience.&lt;/p&gt;

&lt;p&gt;Shorty focuses on performance, security, and usability while maintaining a clean and scalable architecture.&lt;/p&gt;

&lt;p&gt;✨ Features&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔗 URL Shortening — Create short, memorable links instantly&lt;/li&gt;
&lt;li&gt;📱 QR Code Generation — Download QR codes for any shortened link&lt;/li&gt;
&lt;li&gt;📊 Link Analytics — Track clicks, unique visitors, and performance metrics&lt;/li&gt;
&lt;li&gt;🌙 Dark Mode — Elegant light and dark themes&lt;/li&gt;
&lt;li&gt;🔒 Security Built-In&lt;/li&gt;
&lt;li&gt;HTTPS-only URL validation&lt;/li&gt;
&lt;li&gt;Rate limiting to prevent abuse&lt;/li&gt;
&lt;li&gt;Full input sanitization&lt;/li&gt;
&lt;li&gt;📝 Link History — Local storage–based history of created links&lt;/li&gt;
&lt;li&gt;🚨 Report System — Report suspicious or malicious URLs&lt;/li&gt;
&lt;li&gt;📧 Contact Form — Built-in contact functionality&lt;/li&gt;
&lt;li&gt;📱 Responsive Design — Works seamlessly on desktop and mobile&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;🌐 Live Demo&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frontend:&lt;/strong&gt; &lt;a href="https://shorty.msyb.dev" rel="noopener noreferrer"&gt;https://shorty.msyb.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🛠️ Tech Stack&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frontend&lt;/li&gt;
&lt;li&gt;React 19&lt;/li&gt;
&lt;li&gt;React Router 7&lt;/li&gt;
&lt;li&gt;Ant Design 6&lt;/li&gt;
&lt;li&gt;CSS3&lt;/li&gt;
&lt;li&gt;Backend&lt;/li&gt;
&lt;li&gt;Node.js&lt;/li&gt;
&lt;li&gt;Express&lt;/li&gt;
&lt;li&gt;MySQL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Security &amp;amp; Infrastructure&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Helmet.js for secure HTTP headers&lt;/li&gt;
&lt;li&gt;API rate limiting&lt;/li&gt;
&lt;li&gt;CORS with origin whitelisting&lt;/li&gt;
&lt;li&gt;Input validation and sanitization&lt;/li&gt;
&lt;li&gt;SQL injection and XSS protection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Deployment&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Vercel with serverless-ready architecture&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🧩 Architecture Overview&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The project is split into two clear layers:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A React-based frontend responsible for UI, theming, routing, and user interactions&lt;/p&gt;

&lt;p&gt;An Express-based backend handling URL generation, redirection, analytics, reporting, and security enforcement&lt;/p&gt;

&lt;p&gt;This separation keeps the system scalable, maintainable, and easy to extend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📡 API Capabilities&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;URL creation and redirection&lt;/li&gt;
&lt;li&gt;Global and per-link analytics&lt;/li&gt;
&lt;li&gt;QR code tracking&lt;/li&gt;
&lt;li&gt;Contact submissions&lt;/li&gt;
&lt;li&gt;Malicious link reporting&lt;/li&gt;
&lt;li&gt;Health monitoring&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;🔒 Security First Approach&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security was a primary focus while building Shorty:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Strict HTTPS validation for all URLs&lt;/li&gt;
&lt;li&gt;Rate limiting to mitigate abuse&lt;/li&gt;
&lt;li&gt;Sanitized inputs across all endpoints&lt;/li&gt;
&lt;li&gt;Parameterized database queries&lt;/li&gt;
&lt;li&gt;Hardened HTTP headers and controlled CORS access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;🔗 Open Source&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The project is fully open source and available on GitHub:&lt;br&gt;
&lt;a href="https://github.com/shehari007/url-shorty" rel="noopener noreferrer"&gt;Link&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📌 Final Notes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Shorty URL is built as a production-ready URL shortener, not just a demo project. It showcases how modern frontend frameworks and a well-structured Express API can work together to deliver a secure and scalable application.&lt;/p&gt;

&lt;p&gt;Feedback, feature suggestions, and contributions are always welcome.&lt;br&gt;
If you find it useful, a star ⭐ would mean a lot.&lt;/p&gt;

&lt;p&gt;Thanks for reading.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>react</category>
    </item>
  </channel>
</rss>
