跪拜 Guibai
← Back to the summary

A Browser Extension That Knows When You Actually Left Your Desk

Foreword

How are your lower back and neck doing?

I basically spend over 8 hours a day staring at a screen now. A sore neck and back pain have become routine X﹏X

One morning, I woke up and my neck suddenly couldn't move — it hurt whenever I tried. I went to the hospital and got an X-ray......

The doctor took one look and asked: Are we in the same profession?

Me: I work in computers.

Doctor: That explains it. We both have jobs that require sitting for long periods. Aren't we practically colleagues?

The diagnosis said it was typical cervical spine straightening and lumbar muscle strain caused by prolonged sitting! The advice was to stand up and move around at regular intervals. The logic is easy to understand; the hard part is actually doing it. It's not that I don't want to stand up, I genuinely forget every time. Once I get busy, two or three hours have flown by before I even realize it.....

I really needed a tool to remind me to do this thing I "want to do but always forget."

So what exactly should I use for reminders? I thought about phone alarms and smartwatches, but both have drawbacks. A phone on silent sitting on a desk is inaudible, and I don't wear a watch that often.

Then one day while working late, I was staring blankly at the screen when it suddenly hit me: I spend more time with my computer every day than with anyone else. The first thing I do when I start work is turn on the computer; the last thing I do when I finish is close it. Isn't this thing the companion that's always with me? Instead of finding a new tool to remind me, why not just let the computer do it itself?

Why a browser extension?

As long as the browser is open during work, it can quietly guard my lumbar spine and cervical spine.

Repeatedly Discussing Product Requirements

To be honest, this extension didn't look like this from the start.

I talked about requirements with several friends who also sit for long periods, understanding their real office habits. Then we discussed user habits, product form, and interaction models. It went through many revisions before gradually taking its current shape.

After all those discussions, I realized that "reminding" is not just about setting a timer — locking the screen, meetings, rushing deadlines, taking breaks..... each scenario hides a different need. Core features like "Do Not Disturb when busy," "Lock screen equals rest," and "Continuous overtime tracking" were all added gradually.


How is it different from a regular timer?

In one sentence: It knows whether you've actually left your seat.

Think about your work habits — you lock the screen when going to the bathroom, getting water, or eating. Even if you forget to lock the screen, the computer will naturally enter an idle state after you leave. The extension captures these two signals to determine if you've stood up:

Scenario Regular Timer Sedentary Reminder
Bathroom break, screen locked for 1 min ❌ Unaware, pops up on schedule ✅ Lock screen = rest, count +1, countdown resets
Get water, back in 20 secs ❌ Pops up on schedule ✅ Below threshold, continues accumulating
Go to a meeting, screen locked ❌ Timer keeps running ✅ Idle = rest, starts from 0 again upon return
Computer idle for 5 mins ❌ Timer keeps running ✅ Detects idle → treated as rested, count +1
Rushing a deadline ❌ Forcefully interrupts with popup ✅ "Busy now" mode, no interruption but persistent warning
Computer shut down overnight ❌ Shows 1440 minutes the next day ✅ Auto-corrects, restarts
Changing work interval ❌ Starts over from scratch ✅ Retains accumulated time, new interval takes effect immediately

Simply put: The moment the computer enters an idle state, it's treated as you having stood up — screen lock, timeout with no operation, all count. The instant idle is detected, the rest count automatically +1, and the countdown resets to zero; a new timing cycle only begins once you return and become active again. Both thresholds are adjustable.


Feature Overview

📊 Toolbar Popup

Click the icon to see the full picture:

Today's Statistics: Four Core Metrics at a Glance

At the bottom of the popup are four statistics cards, reflecting your sedentary status for the day in real-time:

Metric Meaning How it's derived
🕐 Today's Work Total accumulated time in front of the computer today Automatically sums sitting duration for each work cycle; idle periods are excluded
🔄 Rest Count Number of times you stood up and moved today Auto +1 when computer enters idle/lock screen, or manual +1 by clicking "Start Rest"
📏 Longest Sit The longest single continuous sitting session today Takes the max of the current sitting duration and the historical max upon each idle entry
💯 Health Score A comprehensive assessment from 0-100 Calculated in real-time based on rest count and longest sit (details below)

These four data points are interconnected: Work Duration determines the expected number of times you "should stand up," Rest Count shows how well you've met that, and Longest Sit reflects your worst single performance. All three feed into the Health Score algorithm to produce the final score.

Key implementation detail — the timing of updates for rest count and longest sit:

// When chrome.idle.onStateChanged detects idle/lock screen
let currentSitDuration = runtimeState.accumulatedWork || 0
if (runtimeState.workStartTime) {
  currentSitDuration += Math.floor((Date.now() - runtimeState.workStartTime) / 1000)
}

// Update today's longest sit (take historical max)
const newMaxSitDuration = Math.max(runtimeState.todayMaxSitDuration || 0, currentSitDuration)
// Accumulate today's total work time
const newTodayWorkTotal = (runtimeState.todayWorkTotal || 0) + currentSitDuration
// Rest count +1
const newRestCount = runtimeState.todayRestCount + 1

await updateRuntimeState({
  accumulatedWork: 0, // Countdown reset
  todayRestCount: newRestCount,
  todayWorkTotal: newTodayWorkTotal,
  todayMaxSitDuration: newMaxSitDuration,
})

All data is stored in chrome.storage.local and automatically clears at midnight each day to start a new day's statistics.

🧘 Scheduled Reminder: Not an Alarm, but a Dialogue

When the time is up, a centered popup window and a system notification appear simultaneously (you can choose which one you want in settings).

Browser Popup

System Notification

Button Effect
🛋️ Start Rest Enters rest countdown, closes popup
⏰ Wait 10 more minutes Delays reminder, pops up again after 10 minutes (adjustable)
✅ Just rested Marks as false reminder, resets timer
📌 Busy now, keep working Do Not Disturb mode, no more popups but retains overtime warning

Key Point: When you're rushing a deadline, you don't want to be interrupted, but you also don't want to pretend the health issue doesn't exist. After clicking "Busy now":

This way, you always know how long you've been over time, and you can go move as soon as you finish the task at hand.

🖥️ Speaking of this full-screen reminder popup — its color scheme also took quite a while to adjust 🫥 The reminder window has a dark background with a red warning bar and a blue "Start Rest" button. On my original screen, the dark background looked dull. It wasn't until I switched to the RD270Q that I realized it was a screen color accuracy issue — it's factory-calibrated, has a coding mode (with a colored paper mode) and zonal contrast. The color layers in dark themes were instantly elevated, and I no longer had to repeatedly use DevTools to pick colors when adjusting CSS. The panel's anti-reflective coating is also handy — during the day when someone opens a window next to my desk, or at night when the desk lamp shines on it, the dark popup doesn't blur into a patch of glare ✌️✌️✌️.

📈 Health Score

A health score from 0-100 is automatically calculated daily, using a weighted two-dimensional model based on the day's actual work duration (fair even if you start in the afternoon):

Design philosophy: Encourage standing > Punish sitting. Standing up one extra time shows a noticeable score increase, providing strong positive feedback.

Special handling:

Score Levels: 🏆≥90 Excellent | 💚≥80 Good | 💛≥60 Average | 🟠≥40 Poor | 🔴<40 Needs Improvement

📊 Health Log

A daily snapshot is automatically saved at midnight:

📦 Data Export / Import

No data loss when switching computers; one-click backup of all health records:

Export Content Description
Health Log All historical dailyLog data (up to 90 days)
User Settings All configurations: work interval, rest duration, reminder method, theme, etc.
Usage Days The starting timestamp for "Goodbye Sedentary · Day X"

Safety strategy during import:

Exported as a standard JSON file, highly readable, ready for backup and restoration anytime.

⚙️ Flexible Configuration

Config Item Description
Work Interval 1-120 minutes, default 45
Rest Duration 1-30 minutes, default 5
Idle Threshold 1-20 minutes, default 5
Reminder Method System Notification / Full-screen Popup / Both
Lock Screen Reset Threshold How many seconds of screen lock counts as standing, default 30 secs
Theme Light / Dark
Language Chinese / English
Delay Reminder 5-30 minutes adjustable, default 10

🖥️ Side note: Eyes dry after staring at the screen all day? It might not just be the sitting ಠಿ_ಠ When I was writing this extension, I switched to a BenQ RD270Q coding monitor. I originally just wanted a bigger screen, but I found that looking at code for long periods was surprisingly less tiring on the eyes (⊙o⊙). Especially when rushing a project late into the night, switching to night mode, combined with automatic smart brightness adjustment and low blue light, my eyes no longer felt as dry and sore as before.

Panoramic View

Night Mode

Low Blue Light


Technical Features

Entirely based on native technologies, zero build, zero dependencies, zero servers:

Let's dive into a few core technical points below.


Core 1: Idle Equals Rest — No need to manually click "Start Rest"

Most sedentary reminder tools only manage timing and popups, without caring whether you've actually stood up. My approach is to monitor the system idle state; once it detects you've left, it immediately counts it as a rest:

// Listen for system idle/lock screen events
chrome.idle.onStateChanged.addListener(async (newState) => {
  const runtimeState = await getRuntimeState()
  if (runtimeState.state === STATE.PAUSED) return
  if (runtimeState.state === STATE.RESTING) return

  if (newState === 'active') {
    // User is back: directly start a new countdown cycle (rest was already counted upon entering idle)
    if (runtimeState.state === STATE.IDLE) {
      await updateRuntimeState({
        state: STATE.WORKING,
        accumulatedWork: 0, // Start new cycle from 0
        workStartTime: Date.now(),
        restCountedInCurrentCycle: false,
      })
      await scheduleReminderAlarm()
    }
  } else {
    // idle or locked: treated as rest, rest count +1, countdown reset
    if (runtimeState.state !== STATE.IDLE) {
      let currentSitDuration = runtimeState.accumulatedWork || 0
      // Also include the time from workStartTime until now that hasn't been saved yet
      if (runtimeState.state === STATE.WORKING && runtimeState.workStartTime) {
        currentSitDuration += Math.floor((Date.now() - runtimeState.workStartTime) / 1000)
      }

      const newRestCount = runtimeState.restCountedInCurrentCycle
        ? runtimeState.todayRestCount
        : runtimeState.todayRestCount + 1

      await updateRuntimeState({
        state: STATE.IDLE,
        accumulatedWork: 0, // Countdown reset
        todayRestCount: newRestCount, // Rest count +1
        lastStandTime: Date.now(),
        restCountedInCurrentCycle: true,
      })
      await chrome.alarms.clear(ALARM_REMINDER_FIRE)
    }
  }
})

The state flow is one sentence: Idle → Count rest + Reset → Active → Start timing again.

Going to the bathroom and locking the screen, getting water, leaving for a meeting — as long as the computer enters idle, it automatically counts as you having stood up. After you come back and unlock, a new countdown starts from 0.


Core 2: Precise One-Shot Alarm — Fires on time, no polling

MV3's Service Worker can be suspended at any time; relying on setInterval polling is simply unreliable. My approach is to directly calculate how many seconds are left until the target time and create a one-shot alarm:

async function scheduleReminderAlarm() {
  await chrome.alarms.clear(ALARM_REMINDER_FIRE)
  const [runtimeState, config] = await Promise.all([getRuntimeState(), getConfig()])

  // Busy Do Not Disturb mode: no more auto-popups
  if (runtimeState.reminderDismissed) return
  // Only schedule in working state (idle state is already resting, no reminder needed)
  if (runtimeState.state !== STATE.WORKING) return

  const workDurationSeconds = config.workDuration * 60
  const accumulated = runtimeState.accumulatedWork || 0
  const elapsed = runtimeState.workStartTime ? Math.floor((Date.now() - runtimeState.workStartTime) / 1000) : 0

  const remainingSeconds = workDurationSeconds - accumulated - elapsed

  if (remainingSeconds <= 0) {
    await fireReminderNow(config) // Already due: trigger immediately
    return
  }

  // chrome.alarms minimum delay is 30 seconds; if longer, set by calculated delay
  const delayInMinutes = Math.max(remainingSeconds / 60, 0.5)
  chrome.alarms.create(ALARM_REMINDER_FIRE, { delayInMinutes })

  // When remaining < 30 seconds, alarm will be clamped; use setTimeout as immediate compensation
  if (remainingSeconds < 30) {
    setTimeout(() => fireReminderNow(), remainingSeconds * 1000)
  }
}

Any operation that changes the remaining duration (rest, pause, resume, config modification) calls this function to reschedule, ensuring precise triggering at the target time.


Core 3: Busy Do Not Disturb — "Busy now, keep working!"

When rushing a deadline, you don't want to be interrupted, but you also don't want to pretend the health issue doesn't exist. After clicking "Busy now," the timer doesn't stop, continuously accumulating overtime:

case 'DISMISS_REMINDER': {
  const dismissState = await getRuntimeState();

  // Calculate real sitting duration (including overtime part)
  let currentSitDuration = dismissState.accumulatedWork || 0;
  if (dismissState.state === STATE.WORKING && dismissState.workStartTime) {
    currentSitDuration += Math.floor((Date.now() - dismissState.workStartTime) / 1000);
  }

  await chrome.alarms.clear(ALARM_REMINDER_FIRE);
  await updateRuntimeState({
    reminderDismissed: true,
    accumulatedWork: currentSitDuration, // Don't reset! Keep real accumulated time
    workStartTime: Date.now(),
  });
  // Note: does not increase todayRestCount, does not update lastStandTime (user hasn't actually stood up)
}

Behavior during dismissed state:


Development Anecdotes: From "a timer" to a 1100+ line state machine

Honestly, I initially thought it was just this timer — setTimeout for 45 minutes, pop a notification, done.

Then I discovered one edge case after another:

Every solved problem introduced two new ones. In the end, service-worker.js grew to over 1100 lines, essentially a complete finite state machine: WORKING → IDLE → WORKING (normal cycle), WORKING → RESTING → WORKING (active rest), WORKING → PAUSED → WORKING (pause and resume)…… Each state transition needs to handle timing, notifications, icons, and storage synchronization.

🖥️ Around line 800, I started questioning my life choices, then looked up at the code — luckily the screen was big enough (●'◡'●)~ During that time, I was also tinkering with monitors. Previously, I coded on a laptop screen, scrolling back and forth through long files, constantly switching pages. When rushing projects late into the night, it was a real test of eyesight. Later, after switching to the BenQ RD270Q coding monitor, the strain on my eyes and neck from long coding sessions lessened somewhat. The RD270Q is a 27-inch 2K 3:2 productivity square screen (with a very high usable resolution). Using its companion software, I could split the editor into left and right columns, viewing service-worker.js on one side and popup.js on the other, finally eliminating the need to constantly Cmd+Tab between files. Before, writing this kind of cross-file state synchronization logic on a laptop screen, just switching windows would make me dizzy (◎﹏◎)..... Reading long code is even better ^o^/! You can rotate it with one hand, directly turning it vertical. One screen can display over a hundred lines of code, no more frantic mouse wheel scrolling QAQ~

Core 4: Health Score Algorithm — Encourage standing > Punish sitting

The health score is calculated based on the day's actual work duration (fair even if you start in the afternoon), using a 60/40 weighted model:

function calcHealthScore(todayWorkTotalSeconds, restCount, maxSitDuration, workDurationMinutes) {
  const workDurationSeconds = (workDurationMinutes || 45) * 60

  // Grace period: Within the first full work cycle, before any reminder has triggered, no judgment
  if (todayWorkTotalSeconds < workDurationSeconds) return 100

  // Dimension 1: Rest Frequency Score (out of 60) — Current cycle is exempt; only require rests for past cycles
  const completedCycles = Math.floor(todayWorkTotalSeconds / workDurationSeconds)
  const expectedRests = Math.max(1, completedCycles - 1)
  const restRatio = Math.min(1, restCount / expectedRests)
  const restScore = restRatio * 60

  // Dimension 2: Longest Sit Penalty (out of 40) — More overtime means faster deduction
  let sitScore = 40
  if (maxSitDuration > workDurationSeconds) {
    const overRatio = Math.min(1, (maxSitDuration - workDurationSeconds) / workDurationSeconds)
    // Linear penalty: Overtime ratio directly maps to deduction
    sitScore = 40 * (1 - overRatio)
  }

  return Math.min(100, Math.max(0, Math.round(restScore + sitScore)))
}

Why linear instead of non-linear?

The effect of positive reinforcement: Standing up one extra time shows a noticeable score increase, which changes behavior more effectively than negative prompts like "You've been sitting too long again."


Core 5: Wake-Up Correction — No false alarms after shutting down overnight

After the Service Worker is suspended or the computer is shut down, workStartTime might point to several hours ago or even yesterday. If not handled, opening it the next day would display "Continuously sat for 840 minutes."

async function sanitizeStateOnWakeup(runtimeState, config) {
  const workDurationSeconds = (config.workDuration || 45) * 60
  const abnormalThresholdSeconds = workDurationSeconds * 2 // Exceeding 2x work duration = abnormal

  const updates = {}

  // If time since last stand exceeds 2x work duration → treat as just stood up (shut down/lunch break)
  if (runtimeState.lastStandTime) {
    const sinceLastStand = Math.floor((Date.now() - runtimeState.lastStandTime) / 1000)
    if (sinceLastStand >= abnormalThresholdSeconds) {
      updates.lastStandTime = Date.now()
    }
  }

  // If workStartTime is more than 5 minutes ago → reset (prevents tick from calculating abnormally large elapsed time)
  if (runtimeState.workStartTime) {
    const sinceWorkStart = Math.floor((Date.now() - runtimeState.workStartTime) / 1000)
    if (sinceWorkStart > 300) {
      updates.workStartTime = Date.now()
    }
  }

  if (Object.keys(updates).length > 0) {
    await updateRuntimeState(updates)
  }
}

This function is called every time the Service Worker activates and during every tick heartbeat, ensuring the timing is reasonable whether you shut down overnight or come back from lunch.


Core 6: Hot Configuration Update — Changing settings doesn't lose progress

If you change the work interval (e.g., from 45 minutes to 40 minutes), the 20 minutes you've already sat won't be lost:

case 'UPDATE_CONFIG': {
  await chrome.storage.sync.set(payload);
  if (payload.workDuration !== undefined) {
    // Don't reset accumulatedWork, let the ongoing countdown continue
    await updateRuntimeState({ reminderDismissed: false });
    await initAlarms(); // Reschedule reminder alarm based on new duration
  }
}

Already sat for 20 minutes + new interval of 40 minutes = 20 minutes left until reminder. If the time already sat exceeds the new interval, the alarm triggers immediately.


Privacy Statement

This extension does not connect to the internet:

Works exactly the same offline.


Final Words

Honestly, the reason for making this extension wasn't technical interest, but because my body had already raised a red flag.

Cervical spine straightening, lumbar muscle strain... I used to think these terms were far removed from me, until the moment I got the examination report and realized: the damage from prolonged sitting is irreversible, and it had already happened without me even noticing.

Health is the 1; everything else is the 0s that follow. Without your body, writing more code or doing more projects means nothing. We always think, "I'll stand up as soon as I finish writing this bit," and then two or three hours slip by. Day after day, by the time the body sounds the alarm, it's already too late.

🖥️ One last serious note: If you stare at a screen for over 8 hours a day, besides standing up regularly, you should also protect your eyes~ Why not choose a monitor specifically designed for programmers ( •̀ ω •́ )y. During the few weeks I spent writing this extension, I was sitting in front of the RD270Q, then being reminded to stand up by my own extension. Cervical spine issues can't be solved just by standing up; screen height, angle, and lighting all have an impact (⊙o⊙). This monitor's stand can be raised, lowered, and rotated (I turn it vertical for long files, over a hundred lines on one screen without scrolling back and forth). It has a built-in ambient light sensor for automatic brightness adjustment, a dedicated coding mode, a dark theme and colored paper mode designed for black backgrounds, and a light theme for white backgrounds. I quite like this colored paper mode; when turned on, characters are exceptionally clear, and it doesn't feel uncomfortable to look at for long periods. Combined with the anti-reflective panel, it's really comfortable. It can significantly help alleviate the soreness and strain on our necks and eyes from staring intently at the screen for long periods~ One-key switching via hotkeys, very convenient OSD joystick settings entry, no need to navigate through layers of menus 😭; Five TÜV Rheinland eye-care certifications, the fatigue from staring at code for long periods is indeed much lighter ╰(°▽°)╯! Also, the Flow feature in the companion software Display Pilot 2, simply put, lets the monitor automatically switch modes following your rhythm, eliminating the need to constantly press monitor buttons. A few points I find quite nice:

  1. Auto scene switching: Open Trae to write code, the monitor automatically jumps to 'Coding - Light Theme'; switch to the browser to view design drafts, it automatically switches to True Color...... No manual adjustment, seamless switching, hassle-free.
  2. One-click selection for common modes: Focus Max, Work, True Color, Game, Movie — switch with one click.
  3. Scheduled activation: For example, I set a work period 10:00–19:00, it automatically takes effect on time and exits after work, super suitable for working folks.

What this tool does is very simple — remind you when you forget to stand up. It won't forcefully interrupt when you're busy, nor will it pop up unnecessarily when you've already stood up. It just quietly stays there, gently reminding you when needed: Time to stand up and move around~

I hope everyone like me who sits in front of a computer for long hours every day can take their health seriously. Don't wait until your body sounds the alarm to start taking action.

Looking back over these few weeks, the extension grew from a setTimeout timer into a 1100+ line state machine, and the RD270Q accompanied me through every late night when things wouldn't compile. I quite agree with BenQ's approach to this coding monitor — not just stacking specs, but considering coding display, eye care, and ergonomics from the moment a developer sits down to write code. Empowering developers, helping us write code longer and more steadily, it takes this seriously.

I've been using this extension for about three weeks now, and my experience is that it has greatly improved things. Actually, going to the bathroom takes about 1-2 minutes. Although the time is short, standing itself is very beneficial for the body; you don't have to maintain one posture all the time.

This extension will be open-sourced later; those interested can follow for updates.

It's currently still in the testing and optimization phase. Before that, everyone is welcome to leave comments and exchange ideas — anything that needs optimization, features you want, you can mention in the comments section. I will read them one by one and work together to make it better.


If this article was helpful to you, don't forget to give a 👍 and ⭐~

The BenQ RD270Q used in the article is a 27-inch 2K coding monitor, 3:2 productivity square screen, 144Hz high refresh rate, supporting coding mode (colored paper mode), zonal contrast, Owl mode, MoonHalo smart halo, five TÜV Rheinland eye-care certifications, ergonomic stand, suitable for developers who code for long hours.