Changelog
All notable changes to TimeTrax are documented here.
The focus camera fit a client's system with 60% of headroom beyond what was needed to fit its outermost orbit in frame, so a selection landed noticeably smaller on screen than intended, on both desktop and mobile. The fit margin is now close to 1, so the outermost planet's orbit sits at the edge of the frame rather than well inside it.
The coronas, the contact glow on a lit body, and the galactic bulge's own halo were all billboarded sprites sampling one shared 256x256 canvas gradient, which reads as a smooth fade only while it is minified. Magnified, two things show through: bilinear filtering turns 128 texels of falloff into a fan of straight segments meeting at texel boundaries, and 8-bit alpha means the outer skirt crosses only a handful of levels, each stretched across a wide ring of screen. The last of those levels was a 32-pixel band that then stopped dead, which is the hard edge. The falloff is now evaluated per fragment from the same curve rather than sampled from a bitmap, so it is exact at any magnification; it is windowed down to zero at the sprite's rim instead of having the gaussian's residue subtracted off as a flat constant, which zeroed it early and left a slope discontinuity; and it is dithered by under half an output level, so the last band boundary scatters across a stretch of pixels instead of drawing itself as a ring. The galaxy's own star field keeps the bitmap, being points a couple of pixels across that never leave minification.
That band is a back-face sphere a little larger than the body, so only the annulus past the body's own silhouette shows, and it was drawn at one flat opacity across the whole shell. A sphere's silhouette is a crisp circle, so the band simply ended: measured on a star with a 117-pixel disc, its last nine pixels dropped 82 of 255 levels while the corona either side of it was moving by less than one, which reads as a drawn ring at 1.1x the star's radius. Its opacity is now scaled by a power of the grazing angle, which is not a tuned falloff: for an envelope filling the shell whose density goes as `(1 - r^2/R^2)^m`, the light gathered along a line of sight works out as the grazing angle to the `2m + 1`, so the exponent carries the one real choice, how the envelope thins toward its edge. A constant-density envelope is not enough, because a ball of constant density has a hard boundary of its own: its column depth reaches zero with infinite slope, so the band's brightness was continuous across the silhouette while its slope was not, and at a close zoom that kink resolved into a fainter ring at the same radius. Thinning the envelope removes the boundary, and at `m = 3/2` the brightness, its slope and its curvature are all continuous where the band meets the corona, leaving nothing there for the eye to find. The band keeps its old strength where it meets the body's limb, so the only steep transition left is the body's own edge.
The individual numbers at the call sites stay the relative weights of the layers, the wide outer wash against the brighter inner glow against a lit body's contact sheen, and how each of those lifts on hover and selection; the new constant is the one place that says how much of all of it there is. Below about 70% the smaller stars start losing their halo and read as flat discs, which is what the coronas are doing at a galaxy-wide view: separating a client from a background star. The galactic bulge's halo is left alone, being unresolved starlight across the whole core rather than one body's corona, as is the rim shell, which is a body's own hot edge.
The move is now one continuous curve over a duration fitted to the ground it covers, with the pull-back and the dive-in as two segments that share a tangent where they meet, so there is no leg handoff to jerk at, no convergence threshold, and no timeout. This fixes the selection sliding sideways at the end of a swap between two entities: the old rig eased toward a target that was itself orbiting, which leaves a lag filter with a steady-state error it never closes, then declared itself landed on a distance threshold measured against a framing that was still moving — so it stopped with real error outstanding and handed that error to the landed follow, which discharged it as a visible slide, always in the same direction relative to the orbit. The path instead resolves both endpoints against the nodes' live positions every frame, so orbital motion is carried exactly and the pivot ends precisely on the destination, with nothing left for the landed follow to correct.
Two bodies spread horizontally on a wide screen, or stacked one behind the other along the view direction, need far less room than a sphere fit assumes, which is what made a swap between two projects in one client read as a wildly disproportionate zoom out. A pull-back is also never allowed to end closer than where the camera already was, so a swap between two entities already on screen together simply flies across without zooming out at all.
It was fitted with the same generous headroom used to frame a single selection at the end of a fly, though the pull-back only has to get both selections onto the screen, where neither is centred anyway; it now uses its own tighter margin, about a quarter closer. It was also uncapped, so a swap between two clients on opposite rims pulled back further than the distance at which the entire galaxy already fits in frame — past that point there is nothing further to reveal and the only effect is to shrink what the viewer is trying to follow. The pull-back is now capped at the whole-galaxy framing.
Holding both costs a distance set purely by how far apart they are, with no regard for how big either one is, so two small bodies far apart — two projects on different clients, say — needed a twelvefold zoom out relative to their own framing to show both at once, which read as the camera leaving the map for a move between two small things. Swaps between clients are unaffected, since their own framing is already wide enough that holding both costs less than the cap, and they still pull back to show both.
Below a tablet-width breakpoint, the desktop hover sidebar, floating inspector, and floating timesheet dock — which could cover the whole viewport at once on a narrow screen — are replaced by a top bar (search plus a menu button), a full screen client/project/team-member tree opened from that menu, and one bottom drawer with Inspector and Timesheet tabs. Selecting anything switches the drawer to its Inspector tab and dismisses the tree menu, but leaves the drawer's own open or collapsed state as it was — browsing several nodes in a row no longer pops it open on every one of them. The drawer caps at 65% of the viewport height (a project or team member's fuller content hit an earlier 80% cap, pushing its tab strip up near the top of the screen, hard to spot against the map behind it) and its tab strip has a more opaque background so it reads as a distinct band at any height. Also caps pixel ratio and drops the purely decorative starfield sprites on phones, since profiling found the scene fill-rate bound rather than CPU bound.
Below the same breakpoint, the header's links now collapse into a menu button instead.
Deploys now only happen when triggered on purpose, via a Netlify build hook, leaving deploy previews for other branches and PRs unaffected.
Selecting a client, project or team member — on the map, in the sidebar tree, or from the search box — opens an inspector beside the map with everything the old per-area screens offered: a client's contact details, projects, archive and delete; a project's billing, target days, commencement date, description, staff involved and full time log, plus close, reopen and delete; and a person's role, account status, where their time goes and their recent entries, with reset-link and deactivate for admins. New clients, projects and team members are created from buttons above the sidebar tree, and whatever you create is selected on the map as soon as it exists. A "my timesheet" dock along the bottom runs your timer (including the round-up/round-down prompt when you stop mid-increment), logs time manually, and shows your activity heatmap and recent entries, every one of them editable in place. Nothing in that list navigates: the 3D scene stays mounted throughout, and the map redraws itself as the data behind it changes.
Every write they make and every write the galaxy makes now run through one shared core in `src/lib/mutations`, so the two surfaces cannot drift apart on validation or permissions; the classic screens keep redirecting with `?error=`, while the galaxy renders the same message in place.
Each entry now also shows just its opening sentence by default, with the rest tucked behind a "Full details" disclosure, instead of dumping the whole often very long entry verbatim.
Selecting an entity there frames it exactly as clicking it on the map does. The "busy galaxy (demo data)" toggle now lives in this sidebar, alongside a new "show names" toggle that floats each entity's name next to it, anchored to its live position as it orbits. The sidebar can be pinned open or left to auto-hide, sliding back in on hover, and entity name labels are now plain text (no background) sized by distance from the camera, the same way everything else in the scene shrinks as you zoom away, and hidden while some nearer body sits between it and the camera.
The camera used to slowly orbit the scene on top of the galaxy's own rotation and then stop dead on selection, and while following a selected client it slid sideways with it on a fixed heading, sweeping the whole galaxy past the camera. The camera no longer orbits by itself, and now takes on most of the disc's rotation while following something in it, so the galaxy keeps turning the same way it does in the wide view, at a gentler rate, instead of either reversing or stopping dead. The disc itself turns a little faster to make up for the camera drift that's gone.
Only a minority of team members run backwards, as captured irregular satellites like Triton and Phoebe do, and those also sit further out on steeply inclined orbits. Team members that formed with their project now orbit close to its system's plane rather than at an unrelated angle.
Clients carry a real star surface, convection granules and dark starspots, where before their glow washed the texture out and left every one of them an identical smooth ball, and their color now runs from orange through yellow to white with the hours logged against them, the same measure that sets their size. Projects and team members no longer share a surface: big projects are banded gas giants, small ones rocky worlds with continents, and team members are pitted, matte moons, the smallest of them too small to have pulled themselves round. Projects and people that share a status are no longer identical shades of one color.
An active client sits on the main sequence, where a star's mass sets its size and colour together: the busiest are the largest, hottest and palest, down to small, cool red dwarfs. A client that has been busy and has gone quiet leaves the main sequence, swelling and reddening into a red giant over a year of silence, and an archived one is the white dwarf left behind, small, white and faint. A red dwarf never becomes either, since one burns for trillions of years.
Its halo takes that color too: the inner one used to be drawn in the star's surface color, which is near white on anything from yellow upward, so different stars ended up with much the same glow. The halo also fades along a smooth curve now, rather than a straight line off a kink partway out that read as a ring.
Whatever a body emitted itself lit it evenly all over and flattened that gradient away, so it is now down to a trace, and the ambient fill that did the same is much lower.
Projects used to start almost against their client's surface, the nearest clearing it by under half the star's own radius, and consecutive orbits were stacked barely half a unit apart, so a busy client read as a pile rather than a system. The gap to the star is now at least 1.3 star radii and typically 3, neighbouring orbits sit a median of 3.3 units apart instead of 0.5, and team members have similarly more room around their project. The galaxy is about half as wide again as a result, which the camera framing already scales to. A selected client also swells much less than other bodies, since a 40% pulse on a star closed most of that gap back up exactly when you had zoomed in to look at it.
A fixed count spread them thin across a large galaxy and packed the same number into the narrow ring of a small one, where they read as a solid band rather than as arms: a single-client galaxy's disc covers less than half the share of the view a full one's does. A galaxy that size now carries about a quarter of the stars it did, and a full one is unchanged.
Selecting a node flies the camera to it and shows a detail panel with a link into the existing client, project, or user page. Nodes glow with bloom lighting and halos, clients show an orbit ring for their projects, and a glowing core anchors the map, against a layered starfield background. Every node is now a true sphere, and the layout spaces clients out further as their number grows instead of overlapping. A "Busy galaxy" toggle swaps in a generated set of dummy clients, projects, and people (none of it linking anywhere real) to try the map out under a much heavier load. Toned the glow down, switched the orbit lines to silver, and gave every sphere a rim light and a visible highlight/shadow gradient so they read as round rather than flat discs. Positions now carry "galactic weight": a client with more projects sits closer to the galactic core, and a project with more hours logged sits closer to its client, instead of every client and project sitting on a fixed-radius ring regardless of how busy it is. Spaced solar systems out further so they no longer crowd each other, guaranteed a project's orbit can never dip inside its own client's sphere, and added a very faint light from each client onto the bodies in its system and a very faint wash from the galactic core onto everything. Fixed team members appearing far from the project they're connected to: a user was positioned at the average of their top three projects but only ever linked by a line to their single busiest one, so anyone splitting time across projects in different systems floated in the empty space between them — they're now anchored on that one project instead. Gave the galaxy real depth instead of reading as a flat disc: clients now drift above or below the galactic plane, and each client's own system of projects is tilted at its own random angle rather than lying flat, with its orbit rings tilted to match. Selecting a client or project now frames its whole system — its projects, or a project's connected team members — instead of zooming to a fixed distance that could crop them out. A project's distance from its client is now based on how recent its activity is rather than its total time logged, so a project gone quiet drifts outward while one worked on recently pulls in close; project size still reflects total hours logged, unchanged. Toned the glow down again — it was reading as an atmosphere around each body rather than a rim light. Projects now gently, continuously orbit their client, and team members orbit their project, instead of sitting frozen in place; the camera keeps tracking whatever's selected as it moves, rather than drifting back toward the galactic centre. Clearance between an orbiting body and whatever it orbits now scales with that body's own size, so big clients and projects no longer clip or crowd the smaller bodies around them. Shrank the rim light on every sphere further and dropped the idle "breathing" pulse. Fixed the camera fighting manual panning and zooming once something was selected: it now only steers toward a nicely-framed shot right after you pick something, then simply carries that view along as the target orbits, instead of continuously re-homing to a fixed offset and undoing whatever angle or distance you'd set yourself. Fixed orbiting bodies bumping into each other: a client's projects, and a project's team members, are now spaced apart by their own sizes so that even at their closest alignment their orbits can never bring two of them together, instead of only guarding each body against clipping the one thing it directly orbits. That still missed bumps two levels removed — a team member swinging into its project's own client, two team members on different projects of the same client meeting each other, or one client's whole system drifting into a neighbour's — so a project's spacing now accounts for how far its team members swing around it, and clients are nudged apart from each other until no two client systems, out to their farthest team member, overlap at all. Clients now actually orbit the galactic core, the same gentle way a project orbits its client, instead of sitting fixed in place — selecting one no longer looks like it stops moving, and each gets its own faint orbit ring around the core to match. Made the highlight on every sphere bigger and softer instead of a small, bright glint, then brought back more of its strength once it read as too flat, then spread it across noticeably more of each sphere's surface, twice over. Gave every sphere procedural surface detail instead of a flat, uniform "billiard ball" material: clients read as granular, star-like bodies, projects as banded worlds, and users as small cratered moons, each with six generated variations per kind so the galaxy doesn't repeat the same surface everywhere. That detail wasn't actually visible at first: it only changed how a sphere reacted to nearby light, so anything not close to one of the map's few real light sources still looked flat. Baked the same variation into each sphere's base color and glow directly, so the texture shows up everywhere regardless of lighting. Fixed the fog crushing the whole view toward black as soon as you zoomed out far enough to see more than one system — it was tuned for a much closer view than a full galaxy actually needs. Restored the core's own specular highlight, which the same surface-detail fix had accidentally washed out into a uniform, bloom-blown glow, by leaving its color and glow flat (it's already the brightest thing in the scene) while keeping its normal and roughness detail for a granulated look. Gave clients that same "mini sun" treatment — flat color and glow, brighter than before, with only their normal/roughness detail — since a client anchoring its own solar system is meant to read as self-luminous rather than a lit, textured body like its projects and team members. Split projects by size: a bigger, busier one now reads as a banded gas giant, a smaller one as a cratered rocky world, instead of every project using the same texture. Shrank team members further so they read as clearly smaller than the projects they orbit. The two lights nearly every sphere depended on for any reflection at all were falling off so fast (physically accurate inverse-square) that almost nothing outside the immediate core ever had real light to reflect, however the material was tuned — softened their falloff and extended their reach so a highlight actually survives out to a whole galaxy's worth of clients. Fixed the busy demo galaxy scattering clients hundreds of units from the core: spacing clients apart used the same worst-case-alignment guarantee that correctly keeps a handful of projects from colliding under one client, but that guarantee compounds badly across dozens of clients, since it ignores that they're already well separated by angle — switched to nudging only the pairs that actually overlap instead. A client's own light now clearly dominates its own projects and team members, rather than competing with (or losing to) the light from the far-off galactic core, and every client gets one rather than only the busiest few. Fixed the camera fighting a drag or scroll made in the second or two right after selecting something: it was still steering toward the fly-in framing during that window and only handed control back once settled or after a timeout, which read as the camera "rebounding" away from wherever you'd just moved it. It now hands off the instant you touch the controls. Also fixed the camera slowly losing its lock on the selected object over time: the orbit target was eased toward it during the fly-in and only nudged by how far it moved afterward, so any tiny gap left when the fly-in ended stuck around forever — it now always sits exactly on the live target. Reshaped the whole map to look like a galaxy: the centre is a dense bulge of thousands of stars instead of one big yellow sun, clients now read as actual suns (hot, bright, with a corona) rather than orange spheres, and the systems are laid out as a thin disc wound into two spiral arms instead of a fat ring — each one spaced along its arm by exactly the room its own projects and team members need, so the arms stay readable however many clients there are. The disc turns as a whole rather than each system drifting at its own rate, which is what keeps the arms from winding themselves into a smear. The disc is filled with its own stars along those same arms, so the client systems read as the notable stars within a galaxy rather than the only things in it. The camera now frames whatever size galaxy it's given, and re-frames when you switch to the much larger demo one.
Only the last 2 weeks still show a full, editable list; click any day in the grid to view or edit what was logged that day instead.
Commencement date defaults to the project's creation date.
Admins can edit entries there; visiting your own redirects to your regular timesheet.
Filtering to one project or client now defaults to everything ever logged, rather than just the current month.
Admins invite users from `/users`, and the first admin account is seeded from `ADMIN_EMAIL` (see README) instead of being whoever registers first.
Edge) by showing failed-login errors without a page navigation.
Admins can edit or delete any entry there; members can manage their own.
Format loosely follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).