Dansday

dansday.com

dansday.com

This is the site you are reading. A SvelteKit front end over a Laravel CMS, with a terminal you can talk to, a search that blends keywords with meaning, and a live feed of what I have been committing. It went up in March 2026.

Two things the first version of this page got wrong

It said the interface blends Ubuntu and Windows 11. That was true when it was written and then stopped being true four days earlier than the page went up — on 24 March a commit titled Changing to WSL look moved it to a WSL aesthetic, which is a third thing: a Linux shell living inside a Windows chrome. Appropriate for the content, and not what the page claimed.

It also described an animated interface. A pull request adding an elastic scroll effect was opened on 24 March 2026 and is still open, unmerged, five months later. Meanwhile a separate commit in the same week removed animation. So the honest position is that I was undecided about motion, one branch is still waiting, and neither the page nor the site should imply that question was settled.

The terminal is a retrieval interface, not a chat box

The terminal answers questions about my work, and the interesting part is what it is allowed to see. It has nine tools: search and read for articles and for projects, search and statistics over my GitHub activity, the about sections, general site information, and counts. Which tools exist at all depends on which section of the site you are in, so the terminal in the articles area is not offered project tools it has no business calling.

Two design decisions I would defend in a review:

First, every content query in every tool filters on enable = 1. Unpublished and disabled content is invisible to the terminal as a property of the SQL, not as a request in a prompt. A semantic hit on a draft still gets dropped when the row is fetched. That is the difference between a boundary and a good intention, and it is the distinction I care most about in anything with a model in it.

Second, anything the browser sends with a system role is stripped before the conversation is assembled. A visitor cannot supply their own instructions and have them treated as mine.

The honest counterpart: the terminal's actual behaviour — tone, what it declines, how it presents me — lives in an editable text field in the admin panel, with a placeholder for today's date substituted in at request time. That is convenient and it is also a prompt, which means it is a request rather than a rule. The data boundary is enforced; the behavioural one is asked for politely. I know which of those two survives an adversarial visitor.

Hybrid search, tuned by hand

Search runs two retrievals and fuses them. MySQL full-text gives keyword matches. Embeddings give semantic ones. They are combined with reciprocal rank fusion, and the constants are all hand-set:

SEMANTIC_TOP_N       50      candidates from the vector side
SEMANTIC_THRESHOLD   0.5     cosine floor below which a hit is ignored
RRF_K                40      rank-fusion damping constant
SEMANTIC_WEIGHT      2.0     semantic ranks count double
MAX_FT_ROWS          200     cap on the full-text side
MAX_QUERY_WORDS      30      cap on the query itself

Before either retrieval runs, the query is expanded: the model is asked for three to five short variants, and those are searched too. This is why searching for a concept in one language finds an article written about it in another, and it is also an extra model round trip on the critical path of every search.

Now the part that will break first, stated plainly. The semantic side loads the entire embeddings table into the Node process, parses every vector out of JSON and computes every norm, then does the cosine arithmetic in application code. There is no vector index. At the size of one person's portfolio that is genuinely fine and measurably fast. It is a full table scan wearing a search engine's clothes, it grows linearly with everything I ever write, and when this page gets slow, that will be why. I would rather write that down now than discover it as a mystery later.

The contribute page, and what it costs a third party

There is a live feed of my GitHub activity: commits, pull requests, reviews, a contribution calendar, per-year totals, sortable server-side. It is cached in MySQL and Redis rather than fetched per visitor.

The sync interval is six minutes. It was ten, and a commit on 21 March titled Reduce sync to 6 minutes changed it — which reduced the interval and therefore increased the polling, and is a small lesson in reading my own commit messages carefully. It runs on a timer and is also checked lazily on request, so the data is fresh whether or not anyone has visited.

Which means this site polls somebody else's API every six minutes, permanently, whether a human is looking or not. Nobody asked me to be considerate about that and nothing stops me. It is a small cost imposed on a service I do not pay for, and the polite version of this feature is event-driven rather than scheduled.

There is a second question on this page that I have not resolved well. The feed includes activity from private repositories, and how much detail about private work belongs on a public page is a real question with someone other than me on the other side of it. An earlier version of the code was more conservative than the current one. I am not going to argue myself into a comfortable answer here; it is an open item, I am currently on the wrong side of it, and it is being dealt with directly rather than described.

One evening in April, and then five months of silence

April produced twenty commits on the default branch. Sixteen of them landed in one sitting, between 17:09 and 19:27 UTC on 10 April: the embedding worker and its supervisor process, two migration fixes, a foreign key fix, a chunk size change, sorting moved to the server, and a loading spinner. That is the whole retrieval backend of this site arriving in two hours and eighteen minutes.

Then: one commit in May. Nothing in June. Nothing in July. Sixteen commits in August. Over the same stretch the Discord bot took several hundred. I am not going to dress that up — the site that presents my work went untouched for three months while I built something more fun, and the only reason it did not rot visibly is that its content comes from a database rather than from code.

Stack

Front end   SvelteKit 5, Svelte 5, TypeScript
Data        MySQL, read directly; Redis for caching
Retrieval   MySQL full-text plus embeddings, fused with RRF in app code
AI          any OpenAI-compatible endpoint, configured in the admin panel
Content     Laravel CMS behind it, see the admin.dansday.com project
Telemetry   OpenTelemetry
Hosting     Docker on a single VPS, see the DISNUT project

What it demonstrates

A retrieval system built rather than bought: two retrievals, a fusion function, query expansion, and every constant chosen by looking at results. Tool-calling where the tool set is scoped to context and the data boundary is in SQL rather than in a prompt. A live third-party integration with caching, server-side sorting and a schedule. And a page that tells you where it will break before you find out, which is the only kind of technical writing about your own project that is worth anything.