<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="http://catswhisker.xyz/">
  <id>http://catswhisker.xyz/</id>
  <title>Atom Feed for 'project' Articles</title>
  <updated>2026-02-21T16:37:42Z</updated>
  <link rel="alternate" href="http://catswhisker.xyz/" type="text/html"/>
  <link rel="self" href="http://catswhisker.xyz/tags/project/atom.xml" type="application/atom+xml"/>
  <author>
    <name>A. Cynic</name>
    <uri>http://catswhisker.xyz/about/</uri>
  </author>
  <entry>
    <id>tag:catswhisker.xyz,2026-02-21:/log/2026/2/21/lichess_puzzle_timer/</id>
    <title type="html">Introducing Lichess Puzzle Timer: A browser extension to help you do chess puzzles slower</title>
    <published>2026-02-21T16:37:42Z</published>
    <updated>2026-02-21T16:37:42Z</updated>
    <link rel="alternate" href="http://catswhisker.xyz/log/2026/2/21/lichess_puzzle_timer/" type="text/html"/>
    <content type="html">&lt;div class="sect1"&gt;
&lt;h2 id="_introduction"&gt;Introduction&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;A friend asked me if I knew of any chess puzzle apps that had a timer to prevent him from trying to move so quickly.
I didn&amp;#8217;t, but introducing &lt;a href="https://github.com/cristoper/lichess-puzzle-timer"&gt;cristoper/lichess-puzzle-timer&lt;/a&gt;, my first browser extension!&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Unlike other timers, this one &lt;em&gt;prevents&lt;/em&gt; you from making a move until the timer expires to try to help you force yourself into actually calculating all variations.
(There is also a traditional "Blitz" mode if you prefer. If you like Puzzle Storm but wish you could lose real puzzle rating points, try Blitz mode with 10 seconds and "Jump to next puzzle immediately" option enabled.)&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Once installed it should add the timer above the move list on puzzle pages.
You can disable/enable it with the toggle to the left of the timer, and change settings by clicking on the gear icon to the right of the timer.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_where_to_get_it"&gt;Where to get it&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Chrome: &lt;a href="https://chromewebstore.google.com/detail/lichess-puzzle-timer/mmjgijgmfkhagpfnnafphidhndjjbicc"&gt;Lichess Puzzle Timer in Chrome web store&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Firefox: &lt;a href="https://addons.mozilla.org/en-US/firefox/addon/lichess-puzzle-wait-timer/"&gt;Lichess Puzzle Timer in Firefox Add-Ons Site&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Source code on Github: &lt;a href="https://github.com/cristoper/lichess-puzzle-timer" class="bare"&gt;https://github.com/cristoper/lichess-puzzle-timer&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The &lt;a href="https://raw.githubusercontent.com/cristoper/lichess-puzzle-timer/refs/heads/main/lctimer.js"&gt;lctimer.js&lt;/a&gt; file also works as a &lt;a href="https://www.tampermonkey.net/"&gt;Tampermonkey&lt;/a&gt; script, so you can install it on any browser supported by Tampermonkey.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;I&amp;#8217;ve also built an (unsigned) Safari extension. Download link and instructions are in the Github readme.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_how_to_use_it"&gt;How to use it&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Once installed, navigate to a lichess puzzle trainer page (like &lt;a href="https://lichess.org/training/daily"&gt;the daily puzzle&lt;/a&gt;) and you should see the timer sitting above the move list (circled in the screenshot below):&lt;/p&gt;
&lt;/div&gt;
&lt;div class="imageblock"&gt;
&lt;div class="content"&gt;
&lt;img src="https://raw.githubusercontent.com/cristoper/lichess-puzzle-timer/main/screenshot-full-circle.png" alt="screenshot full circle"&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Click the gear icon to the right of the timer to open the settings dialog:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="imageblock"&gt;
&lt;div class="content"&gt;
&lt;img src="https://raw.githubusercontent.com/cristoper/lichess-puzzle-timer/main/settings.png" alt="settings"&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_thinking_mode"&gt;Thinking mode&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;This is the default mode. In this mode the board will have a red outline and you will not be able to make a move until the timer expires.
Use this time to force yourself to calculate your candidate variations and consider all responses.
Once the timer reaches zero, the board outline will turn green and you may make your move.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;While the board outline is red you &lt;strong&gt;can&lt;/strong&gt; right click to draw arrows. To erase arrows, draw another one on top of the arrow you wish to erase.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_blitz_mode"&gt;Blitz mode&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;In this mode the board outline starts out green and you try to make your move &lt;strong&gt;before&lt;/strong&gt; the timer runs out.
For extra stakes, enable &amp;#8220;autofail&amp;#8221; mode so that if you have not solved the puzzle in time it is automatically failed.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_how_it_works"&gt;How it works&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The extension consists of a single javascript file (&lt;a href="https://github.com/cristoper/lichess-puzzle-timer/blob/main/lctimer.js"&gt;lctimer.js&lt;/a&gt;) and a single css file (&lt;a href="https://github.com/cristoper/lichess-puzzle-timer/blob/main/lctimer.css"&gt;lctimer.css&lt;/a&gt;)&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;It also contains a &lt;a href="https://github.com/cristoper/lichess-puzzle-timer/blob/main/manifest.json"&gt;manifest.json&lt;/a&gt; file which tells the browser which URLs to load the extension on (the extension you install consists of these files together in a .zip archive):&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="CodeRay highlight"&gt;&lt;code data-lang="json"&gt;&lt;span class="key"&gt;&lt;span class="delimiter"&gt;&amp;quot;&lt;/span&gt;&lt;span class="content"&gt;content_scripts&lt;/span&gt;&lt;span class="delimiter"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;: [
    {
      &lt;span class="key"&gt;&lt;span class="delimiter"&gt;&amp;quot;&lt;/span&gt;&lt;span class="content"&gt;matches&lt;/span&gt;&lt;span class="delimiter"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;: [&lt;span class="string"&gt;&lt;span class="delimiter"&gt;&amp;quot;&lt;/span&gt;&lt;span class="content"&gt;*://*.lichess.org/training*&lt;/span&gt;&lt;span class="delimiter"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;],
      &lt;span class="key"&gt;&lt;span class="delimiter"&gt;&amp;quot;&lt;/span&gt;&lt;span class="content"&gt;js&lt;/span&gt;&lt;span class="delimiter"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;: [&lt;span class="string"&gt;&lt;span class="delimiter"&gt;&amp;quot;&lt;/span&gt;&lt;span class="content"&gt;lctimer.js&lt;/span&gt;&lt;span class="delimiter"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;],
      &lt;span class="key"&gt;&lt;span class="delimiter"&gt;&amp;quot;&lt;/span&gt;&lt;span class="content"&gt;css&lt;/span&gt;&lt;span class="delimiter"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;: [&lt;span class="string"&gt;&lt;span class="delimiter"&gt;&amp;quot;&lt;/span&gt;&lt;span class="content"&gt;lctimer.css&lt;/span&gt;&lt;span class="delimiter"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;]
    }
  ]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The &lt;code&gt;matches&lt;/code&gt; line instructs the browser to only load the extension on URLs matching the pattern (lichess puzzle pages).&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The CSS tries to fit the timer into the lichess.org site. For the most part I try to use existing html `class`es so that controls adopt the lichess look automatically.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_starting_up"&gt;Starting up&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;By default, the browser will insert and run the javascript after the page is finished loading (this can be changed by specifying a &lt;a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/content_scripts#run_at"&gt;run_at&lt;/a&gt; key in the &lt;code&gt;manifest.json&lt;/code&gt; file).
While testing, and in the first version of the extension I released, I initialized the extension code by listening for the window &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event"&gt;load event&lt;/a&gt; that is fired once the html page has been loaded, like I would do if developing javascript loaded by the page itself. But because the extension is inserted &lt;em&gt;after&lt;/em&gt; the page is loaded by default, waiting for the &lt;code&gt;load&lt;/code&gt; event is not reliable. The first version of the extension worked in Chrome and &lt;em&gt;sometimes&lt;/em&gt; in Firefox; but sometimes it would not load at all in Firefox because the window &lt;code&gt;load&lt;/code&gt; event was fired before the extension code was loaded.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The current version checks whether the document is already ready when the extension loads (and if not then it waits on &lt;code&gt;load&lt;/code&gt; as usual). That way it will always initialize regardless of when the browser inserts the extension:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="CodeRay highlight"&gt;&lt;code data-lang="javascript"&gt;&lt;span class="keyword"&gt;if&lt;/span&gt; (document.readyState === &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;complete&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;) {
    startExt();
} &lt;span class="keyword"&gt;else&lt;/span&gt; {
    window.addEventListener(&lt;span class="string"&gt;&lt;span class="delimiter"&gt;&amp;quot;&lt;/span&gt;&lt;span class="content"&gt;load&lt;/span&gt;&lt;span class="delimiter"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;, startExt);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The first thing the script does when loaded is looks for the &lt;code&gt;&amp;lt;div class="puzzle__tools"&amp;gt;&lt;/code&gt; element and adds an html div to the top of that and then uses that as a container for the timer:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="CodeRay highlight"&gt;&lt;code data-lang="javascript"&gt;const puzzle_tools = document.querySelector(&lt;span class="string"&gt;&lt;span class="delimiter"&gt;&amp;quot;&lt;/span&gt;&lt;span class="content"&gt;.puzzle__tools&lt;/span&gt;&lt;span class="delimiter"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;);
const timer = document.createElement(&lt;span class="string"&gt;&lt;span class="delimiter"&gt;&amp;quot;&lt;/span&gt;&lt;span class="content"&gt;div&lt;/span&gt;&lt;span class="delimiter"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;)
timer.className = &lt;span class="string"&gt;&lt;span class="delimiter"&gt;&amp;quot;&lt;/span&gt;&lt;span class="content"&gt;lctimer-container&lt;/span&gt;&lt;span class="delimiter"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;;
puzzle_tools.prepend(timer);

const settings = &lt;span class="keyword"&gt;new&lt;/span&gt; LCSettings();
const app = &lt;span class="keyword"&gt;new&lt;/span&gt; LCPuzzleTimer(timer, settings);

app.newPuzzle();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The entire javascript extension consists pretty much of the &lt;code&gt;LCSettings&lt;/code&gt; and &lt;code&gt;LCPuzzleTimer&lt;/code&gt; classes.
I don&amp;#8217;t do much frontend development, but when I do, these classes are typical of the approach I take to implementing simple components:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;The &lt;code&gt;constructor()&lt;/code&gt; accepts an HTMLElement to use as a container, adds its own html to it, and keeps references to any DOM nodes it needs to update in instance variables. Also sets up any event listeners.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Has a &lt;code&gt;render()&lt;/code&gt; method that updates its DOM nodes based on state of instance variables&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Has setters to set state which in turn call &lt;code&gt;render()&lt;/code&gt; so the component stays in sync with its data&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;This is fast, easy to get started, uses no build step, no framework, no observables, no virtual DOM diffing, and is mostly easy to reason about. I think for small (to medium) web apps it represents a good compromise between DOM-manipulating spaghetti code and a reactive framework. Though probably using &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components"&gt;web components&lt;/a&gt;, which has good browser support, is an improved version of this es6-class-as-component technique.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_timer"&gt;Timer&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;To do the actual timing, which is important for a timer, the &lt;code&gt;LCPuzzleTimer&lt;/code&gt; class manages a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/setInterval"&gt;setInterval&lt;/a&gt; timer. Here is its &lt;code&gt;start()&lt;/code&gt; method:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="CodeRay highlight"&gt;&lt;code data-lang="javascript"&gt;start() {
    &lt;span class="keyword"&gt;if&lt;/span&gt; (!&lt;span class="local-variable"&gt;this&lt;/span&gt;.settings.enabled) {
        &lt;span class="keyword"&gt;return&lt;/span&gt;;
    }
    &lt;span class="local-variable"&gt;this&lt;/span&gt;.running = &lt;span class="predefined-constant"&gt;true&lt;/span&gt;;
    &lt;span class="local-variable"&gt;this&lt;/span&gt;.startTime = Date.now();
    &lt;span class="local-variable"&gt;this&lt;/span&gt;.lastTick = &lt;span class="local-variable"&gt;this&lt;/span&gt;.startTime;
    &lt;span class="local-variable"&gt;this&lt;/span&gt;.timer = setInterval(() =&amp;gt; {
        &lt;span class="local-variable"&gt;this&lt;/span&gt;.tick();
    }, &lt;span class="integer"&gt;100&lt;/span&gt;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;So that when the timer is running, the &lt;code&gt;tick()&lt;/code&gt; method gets called every 100ms:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="CodeRay highlight"&gt;&lt;code data-lang="javascript"&gt;tick() {
    &lt;span class="keyword"&gt;if&lt;/span&gt; (&lt;span class="local-variable"&gt;this&lt;/span&gt;.running) {
        const now = Date.now();
        const diff = now - &lt;span class="local-variable"&gt;this&lt;/span&gt;.lastTick;
        &lt;span class="local-variable"&gt;this&lt;/span&gt;.time -= diff;
        &lt;span class="local-variable"&gt;this&lt;/span&gt;.lastTick = now;
        &lt;span class="keyword"&gt;if&lt;/span&gt; (&lt;span class="local-variable"&gt;this&lt;/span&gt;.time &amp;lt;= &lt;span class="integer"&gt;0&lt;/span&gt;) {
            &lt;span class="local-variable"&gt;this&lt;/span&gt;.expired();
        }
    }
    &lt;span class="local-variable"&gt;this&lt;/span&gt;.render();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The &lt;code&gt;tick()&lt;/code&gt; method calculates how much time has actually elapsed since the last &lt;code&gt;tick()&lt;/code&gt; rather than blindly trusting the browser to call every 100ms. That way any jitter in the javascript event loop doesn&amp;#8217;t add error to our timer. It then calls &lt;code&gt;expire()&lt;/code&gt; if the timer has expired; otherwise it calls &lt;code&gt;render()&lt;/code&gt; which actually updates the timer.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;In the &lt;code&gt;render()&lt;/code&gt; method, the time displayed to the user is decremented once per second until it is below 10 seconds, then it is updated every 100ms (this mimics the lichess game timer which starts showing fractions of a second once the clock is low enough).
For better accuracy we could set the interval to something faster than 100, even 0 (which would run &lt;code&gt;tick()&lt;/code&gt; as quickly as the JavaScript event loop runs, though most browsers will throttle it to 4ms after a few ticks). But there&amp;#8217;s no sense wasting CPU time running our code more often than we need to.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Note that for better precision we could have used &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame"&gt;requestAnimationFrame()&lt;/a&gt; which calls a function before every screen refresh (so you can be sure your animations don&amp;#8217;t tear/flicker). But that is overkill for our 100ms timer needs.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_preventing_board_clicks"&gt;Preventing board clicks&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;To actually prevent the user from making a move while the thinking timer is running, we add a &lt;code&gt;mousedown&lt;/code&gt; event listener to the &lt;code&gt;&amp;lt;cg-board&amp;gt;&lt;/code&gt; element and run &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation"&gt;stopPropagation&lt;/a&gt; so that the lichess components never see the click. Here is the full method called by the &lt;code&gt;mousedown&lt;/code&gt; callback:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="CodeRay highlight"&gt;&lt;code data-lang="javascript"&gt;clickedBoard(event) {
    &lt;span class="comment"&gt;// user tried to click board while locked, flash the background&lt;/span&gt;
    &lt;span class="comment"&gt;// and block event&lt;/span&gt;
    const leftButton = event.button === &lt;span class="integer"&gt;0&lt;/span&gt;;
    &lt;span class="keyword"&gt;if&lt;/span&gt; (&lt;span class="local-variable"&gt;this&lt;/span&gt;.running &amp;amp;&amp;amp; &lt;span class="local-variable"&gt;this&lt;/span&gt;.settings.enabled &amp;amp;&amp;amp; &lt;span class="local-variable"&gt;this&lt;/span&gt;.settings.slowMode &amp;amp;&amp;amp; leftButton) {
        &lt;span class="local-variable"&gt;this&lt;/span&gt;.flashBG = &lt;span class="predefined-constant"&gt;true&lt;/span&gt;;
        setTimeout(() =&amp;gt; {
            &lt;span class="local-variable"&gt;this&lt;/span&gt;.flashBG = &lt;span class="predefined-constant"&gt;false&lt;/span&gt;;
        }, &lt;span class="integer"&gt;100&lt;/span&gt;);
        event.stopPropagation(); &lt;span class="comment"&gt;// prevent making moves on the board&lt;/span&gt;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Notice that it only prevents &lt;code&gt;leftButton&lt;/code&gt; clicks so that the user can still draw arrows with the right mouse button while thinking. The callback also has logic to flash the border for 100ms if the user tries to click before the timer has expired.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_detecting_when_puzzles_start_and_end"&gt;Detecting when puzzles start and end&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;One challenge for this extension is detecting when the user completes, fails, and starts a new puzzle so that we can stop/reset the timer.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;We solve it by using the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver"&gt;MutationObserver()&lt;/a&gt; interface that all browsers support.
We pass &lt;code&gt;MutationObserver()&lt;/code&gt; a callback that is called every time the DOM changes. We then traverse the list of removed/added nodes and check if any of them correspond to the puzzle feedback messages lichess displays when a puzzle is completed. This is probably one of the more fragile dependencies we have on the lichess page&amp;#8230;&amp;#8203; if they ever change how messages are displayed it will break the extension and we&amp;#8217;ll have to update it accordingly.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;This is the full callback called by &lt;code&gt;MutationObserver()&lt;/code&gt;:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="CodeRay highlight"&gt;&lt;code data-lang="javascript"&gt;const cgCallback = (mutationsList, observer) =&amp;gt; {
    &lt;span class="keyword"&gt;for&lt;/span&gt; (let mutation of mutationsList) {
        &lt;span class="keyword"&gt;if&lt;/span&gt; (mutation.type === &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;childList&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;) {
            &lt;span class="comment"&gt;// if div.puzzle__feedback.after is removed, assume new puzzle&lt;/span&gt;
            &lt;span class="keyword"&gt;for&lt;/span&gt; (let node of mutation.removedNodes) {
                &lt;span class="keyword"&gt;if&lt;/span&gt; (node.classList &amp;amp;&amp;amp; node.classList.contains(&lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;puzzle__feedback&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
                    &amp;amp;&amp;amp; node.classList.contains(&lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;after&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;))
                {
                    &lt;span class="local-variable"&gt;this&lt;/span&gt;.newPuzzle();
                    &lt;span class="keyword"&gt;return&lt;/span&gt;;
                }
            }
            &lt;span class="comment"&gt;// check for success and fail feedback messages&lt;/span&gt;
            &lt;span class="keyword"&gt;for&lt;/span&gt; (let node of mutation.addedNodes) {
                &lt;span class="keyword"&gt;if&lt;/span&gt; (node.classList &amp;amp;&amp;amp; node.classList.contains(&lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;puzzle__feedback&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;)
                    &amp;amp;&amp;amp; node.classList.contains(&lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;fail&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;))
                {
                    &lt;span class="local-variable"&gt;this&lt;/span&gt;.puzzleFailed();
                    &lt;span class="keyword"&gt;return&lt;/span&gt;;
                }
                &lt;span class="keyword"&gt;if&lt;/span&gt; (node.classList &amp;amp;&amp;amp; node.classList.contains(&lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;puzzle__feedback&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;) &amp;amp;&amp;amp; node.classList.contains(&lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;after&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt;))
                {
                    &lt;span class="local-variable"&gt;this&lt;/span&gt;.puzzleSucceeded();
                    &lt;span class="keyword"&gt;return&lt;/span&gt;;
                }
            }
        }
    }
};&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Note that &lt;code&gt;puzzleFailed()&lt;/code&gt; and &lt;code&gt;puzzleSucceeded()&lt;/code&gt; are only relevant to the Blitz mode where the user can make moves while the timer is running. Further, &lt;code&gt;puzzleFailed()&lt;/code&gt; doesn&amp;#8217;t actually do anything: we allow the user to keep trying to solve the puzzle while the timer is running. But we detect it in case we want to react to that event some how in a future version.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_autofail"&gt;Autofail&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The Blitz mode has a feature called "Autofail" whereupon the extension will fail the puzzle as soon as the timer runs out.
To implement this, we simply click the "view the solution" button on the user&amp;#8217;s behalf when the timer expires:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="CodeRay highlight"&gt;&lt;code data-lang="javascript"&gt;&lt;span class="keyword"&gt;function&lt;/span&gt; &lt;span class="function"&gt;clickViewSolution&lt;/span&gt;() {
    document.querySelector(&lt;span class="string"&gt;&lt;span class="delimiter"&gt;&amp;quot;&lt;/span&gt;&lt;span class="content"&gt;.view_solution&lt;/span&gt;&lt;span class="delimiter"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;).querySelectorAll(&lt;span class="string"&gt;&lt;span class="delimiter"&gt;&amp;quot;&lt;/span&gt;&lt;span class="content"&gt;a&lt;/span&gt;&lt;span class="delimiter"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;)[&lt;span class="integer"&gt;1&lt;/span&gt;].click();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_settings"&gt;Settings&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;The settings dialog is implemented using the native &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog"&gt;&amp;lt;dialog&amp;gt; element&lt;/a&gt; supported by all modern browsers.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;When the dialog is closed, the settings are used to update the state of the &lt;code&gt;LCPuzzleTimer&lt;/code&gt; instance. They are also persisted to &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage"&gt;localStorage&lt;/a&gt; which is an alternative to cookies for storing data so that it is not sent to the server on every request.
When the extension is loaded, it initializes its settings from any found in &lt;code&gt;localStorage&lt;/code&gt;, that way the user&amp;#8217;s last settings always take effect even between sessions.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;One difficulty with implementing setting persistence is that I wanted the extension to also be installable as a Tampermonkey userscript, but userscripts do not have direct access to the &lt;code&gt;localStorage&lt;/code&gt; API and instead must use the &lt;code&gt;GM_setValue()&lt;/code&gt; and &lt;code&gt;GM_getValue()&lt;/code&gt; functions.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;In order to use either storage interface, I use a little abstraction class:&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="CodeRay highlight"&gt;&lt;code data-lang="javascript"&gt;&lt;span class="reserved"&gt;class&lt;/span&gt; Storage {
    &lt;span class="reserved"&gt;static&lt;/span&gt; setItem(key, value) {
        &lt;span class="keyword"&gt;if&lt;/span&gt; (&lt;span class="keyword"&gt;typeof&lt;/span&gt; chrome !== &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;undefined&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt; &amp;amp;&amp;amp; chrome.storage) {
            const val = {};
            val[key] = value;
            &lt;span class="keyword"&gt;return&lt;/span&gt; chrome.storage.local.set(val);
        } &lt;span class="keyword"&gt;else&lt;/span&gt; {
            &lt;span class="keyword"&gt;return&lt;/span&gt; GM_setValue(key, value);
        }
    }

    &lt;span class="reserved"&gt;static&lt;/span&gt; getItem(key) {
        &lt;span class="keyword"&gt;if&lt;/span&gt; (&lt;span class="keyword"&gt;typeof&lt;/span&gt; chrome !== &lt;span class="string"&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;span class="content"&gt;undefined&lt;/span&gt;&lt;span class="delimiter"&gt;'&lt;/span&gt;&lt;/span&gt; &amp;amp;&amp;amp; chrome.storage) {
            &lt;span class="keyword"&gt;return&lt;/span&gt; chrome.storage.local.get(key);
        } &lt;span class="keyword"&gt;else&lt;/span&gt; {
            let result = {};
            const v = GM_getValue(key);
            result[key] = v;
            &lt;span class="keyword"&gt;return&lt;/span&gt; Promise.resolve(result);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;If &lt;code&gt;chrome.storage.local&lt;/code&gt; is available (which is Chrome&amp;#8217;s asynchronous version of &lt;code&gt;localStorage&lt;/code&gt; and also supported by Firefox) we use that; otherwise we assume the script is installed in Tampermonkey and use the &lt;code&gt;GM_getValue&lt;/code&gt;/&lt;code&gt;GM_setValue&lt;/code&gt; functions (and wrap the return value of &lt;code&gt;GM_getValue&lt;/code&gt; in a Promise so that the calling code doesn&amp;#8217;t have to care whether it is using &lt;code&gt;chrome.storage.local.get()&lt;/code&gt; or &lt;code&gt;GM_getValue()&lt;/code&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_conclusion"&gt;Conclusion&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Those are the highligts. Of course read &lt;a href="https://github.com/cristoper/lichess-puzzle-timer/blob/main/lctimer.js"&gt;lctimer.js&lt;/a&gt; to see how everything fits together.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;I wrote the code by hand, and I&amp;#8217;m not very fast at coding, because hobby, but I am a little curious how quickly and well an LLM could knock a simple extension like this out :shrug:&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</content>
    <summary type="html">I created a little browser extension that adds a timer to the lichess puzzle trainer. Unlike other timers, this one prevents you from making a move until the timer expires to try to help you force yourself into actually calculating all variations.</summary>
  </entry>
</feed>

