Compare commits

..
44 Commits
Author SHA1 Message Date
Tobias Gesellchen 68f8efce4e Improve parity with upstream (#155)
See https://github.com/gesellix/Bose-SoundTouch/issues/135
2026-04-07 14:44:05 +02:00
Tobias GesellchenandJunie 276d01fe42 feat(spotify): improve Spotify registration flow and speaker notification
- Implement full SoundTouch app flow for Spotify registration in the Web UI.
- Update `/mgmt/spotify/init` to pass `accountID` via OAuth `state`.
- Add "Connect Spotify" button to Local Account tab in Web UI with polling.
- Implement legacy and Marge-sync fallbacks for speaker notifications (Error 1029).
- Add support for parsing multi-error XML responses (`<errors>`) from speakers.
- Add `NotifySourcesUpdated` to client for triggering manual source synchronization.
- Improve test coverage for error parsing and Spotify initialization handlers.

Co-authored-by: Junie <junie@jetbrains.com>
2026-04-06 22:39:30 +02:00
Tobias Gesellchen 4de7911817 Fix data race in TestSpotifyBridge 2026-04-06 21:15:15 +02:00
Tobias Gesellchen 5d933f7ebc Use a constant prefix for our internal token 2026-04-06 21:15:15 +02:00
Tobias Gesellchen 153d387aaf Fix a complete flow for Spotify registration, preset 2026-04-06 21:15:15 +02:00
Tobias Gesellchen fea6df32f3 Implement the Spotify source bridge 2026-04-06 21:15:15 +02:00
Tobias Gesellchen 3b1c639892 Completely ignore integration testdata 2026-04-06 15:22:06 +02:00
Tobias Gesellchen 740cf54b9d Cleanup Spotify tests 2026-04-06 15:22:06 +02:00
Tobias Gesellchen e5b94158e6 Use modern docker compose command syntax 2026-04-06 15:05:44 +02:00
Tobias Gesellchen c54ee79320 No need for that mock Spotify account to be version controlled 2026-04-06 15:05:44 +02:00
Tobias Gesellchen c4cf078d2a Add Spotify mock server and integration tests 2026-04-06 15:05:44 +02:00
Tobias Gesellchen 382567d67d Add .../api_versions.xml and .../musicprovider/{providerID}/is_eligible (#150) 2026-04-05 23:25:30 +02:00
Tobias Gesellchen de96b1f119 Add /streaming/account/{account}/presets/all (#149) 2026-04-05 23:10:53 +02:00
Tobias Gesellchen d22dc99c9e Add /streaming/account/{account}/devices (#148) 2026-04-05 10:16:22 +02:00
Tobias Gesellchen bd0e3d64a3 Add /streaming/account/{account}/sources (#147) 2026-04-05 01:09:02 +02:00
Tobias Gesellchen 379ac758f6 Add /bmx/tunein/v1/navigate and /bmx/tunein/v1/search (dummy) 2026-04-05 00:55:40 +02:00
Tobias Gesellchen 6d0b5f2c78 Add /bmx/registry/v1/servicesAvailability 2026-04-05 00:55:40 +02:00
Tobias Gesellchen f354c63bac Add /v1/report (#145)
https://github.com/gesellix/Bose-SoundTouch/issues/135
2026-04-04 23:23:04 +02:00
Tobias Gesellchen 50e45ab5f2 Add/improve e2e test cases (#144)
https://github.com/gesellix/Bose-SoundTouch/issues/135
2026-04-04 21:05:34 +02:00
Tobias Gesellchen c1e7d513b4 Add/improve e2e test cases 2026-04-04 18:53:59 +02:00
Tobias Gesellchen cc92430e69 Add /blacklist handler 2026-04-04 18:53:59 +02:00
Tobias Gesellchen 181cd550e3 Fix doc check 2026-04-04 18:53:59 +02:00
Tobias Gesellchen 7c92a785a4 Add/improve e2e tests (#142)
https://github.com/gesellix/Bose-SoundTouch/issues/135
2026-04-04 13:10:13 +02:00
Tobias Gesellchen b79a168084 Add/improve e2e test cases (#141)
https://github.com/gesellix/Bose-SoundTouch/issues/135
2026-04-04 12:22:58 +02:00
Tobias Gesellchen 21ce44fa2e Update the "bose-lab" runbook for app activity tracing (#140) 2026-04-03 23:50:28 +02:00
Tobias GesellchenandJunie 65f1a2565c feat: add spotify source registration and environment config for set_preset_5 integration test (#139)
Co-authored-by: Junie <junie@jetbrains.com>
2026-04-01 22:12:30 +02:00
aa7b2c28ab feat: improve Bose SoundTouch parity, Spotify integration, and data reliability (#138)
feat: improve Bose SoundTouch parity, Spotify integration, and data
reliability

- Update XML marshaling for ServicePreset and ServiceRecent to match
Bose parity requirements.
- Add support for adding music sources via
`/streaming/account/{account}/source`.
- Implement HandleBoseAccountToken for Spotify OAuth code exchange and
token persistence.
- Implement atomic file writes in the datastore to prevent data
corruption.
- Add startup logic to initialize default sources for existing devices.
- Expand test coverage with new parity regression and Spotify
integration tests.

---------

Co-authored-by: Junie <junie@jetbrains.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-04-01 21:55:52 +02:00
dependabot[bot] 8ef8d71121 ci(deps): bump actions/configure-pages in the actions-core group
Bumps the actions-core group with 1 update: [actions/configure-pages](https://github.com/actions/configure-pages).


Updates `actions/configure-pages` from 5 to 6
- [Release notes](https://github.com/actions/configure-pages/releases)
- [Commits](https://github.com/actions/configure-pages/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/configure-pages
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-core
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-01 19:31:27 +02:00
Tobias Gesellchen c5c88f32c3 Fix internal links 2026-03-30 00:52:00 +02:00
Tobias Gesellchen 0b8f561077 Ignore tests/ in doc link check 2026-03-30 00:52:00 +02:00
Tobias Gesellchen 71e3260823 Extend TuneIn support, add e2e tests 2026-03-30 00:52:00 +02:00
Tobias Gesellchenandlnx01 bc1b70b8a5 Potential fix for code scanning alert no. 88: Uncontrolled data used in path expression
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-03-29 19:14:48 +02:00
Tobias Gesellchen a8140ad4fd Fix AddDeviceToAccount 2026-03-29 19:14:48 +02:00
Tobias Gesellchen 766671f02b Cleanup, snapshot all routes 2026-03-29 19:14:48 +02:00
Tobias Gesellchen a06657f3f5 Add more e2e tests 2026-03-29 19:14:48 +02:00
Tobias Gesellchen 505a189ce5 Simplify route config 2026-03-29 19:14:48 +02:00
Tobias Gesellchen 509f613e34 Make test less dependent on the environment 2026-03-29 19:14:48 +02:00
Tobias Gesellchen 1478d97886 Cleanup .http client tests 2026-03-29 19:14:48 +02:00
Tobias Gesellchen a40fd8cdac bump 2026-03-29 19:14:48 +02:00
Tobias Gesellchen e0a84d5904 Split register and unregister device tests (#133) 2026-03-27 22:03:21 +01:00
dependabot[bot]andlnx01 5d080cf35f ci(deps): bump codecov/codecov-action from 5 to 6 in the security-actions group (#132)
Bumps the security-actions group with 1 update:
[codecov/codecov-action](https://github.com/codecov/codecov-action).

Updates `codecov/codecov-action` from 5 to 6
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/codecov/codecov-action/releases">codecov/codecov-action's
releases</a>.</em></p>
<blockquote>
<h2>v6.0.0</h2>
<h2>⚠️ This version introduces support for node24 which make cause
breaking changes for systems that do not currently support node24.
⚠️</h2>
<h2>What's Changed</h2>
<ul>
<li>Revert &quot;Revert &quot;build(deps): bump actions/github-script
from 7.0.1 to 8.0.0&quot;&quot; by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1929">codecov/codecov-action#1929</a></li>
<li>Th/6.0.0 by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1928">codecov/codecov-action#1928</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/codecov/codecov-action/compare/v5.5.4...v6.0.0">https://github.com/codecov/codecov-action/compare/v5.5.4...v6.0.0</a></p>
<h2>v5.5.4</h2>
<p>This is a mirror of <code>v5.5.2</code>. <code>v6</code> will be
released which requires <code>node24</code></p>
<h2>What's Changed</h2>
<ul>
<li>Revert &quot;build(deps): bump actions/github-script from 7.0.1 to
8.0.0&quot; by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1926">codecov/codecov-action#1926</a></li>
<li>chore(release): 5.5.4 by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1927">codecov/codecov-action#1927</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/codecov/codecov-action/compare/v5.5.3...v5.5.4">https://github.com/codecov/codecov-action/compare/v5.5.3...v5.5.4</a></p>
<h2>v5.5.3</h2>
<h2>What's Changed</h2>
<ul>
<li>build(deps): bump actions/github-script from 7.0.1 to 8.0.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1874">codecov/codecov-action#1874</a></li>
<li>chore(release): bump to 5.5.3 by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1922">codecov/codecov-action#1922</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/codecov/codecov-action/compare/v5.5.2...v5.5.3">https://github.com/codecov/codecov-action/compare/v5.5.2...v5.5.3</a></p>
<h2>v5.5.2</h2>
<h2>What's Changed</h2>
<ul>
<li>check gpg only when skip-validation = false by <a
href="https://github.com/maxweng-sentry"><code>@​maxweng-sentry</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1894">codecov/codecov-action#1894</a></li>
<li>chore: <code>disable_search</code> alignment by <a
href="https://github.com/freemanzMrojo"><code>@​freemanzMrojo</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1881">codecov/codecov-action#1881</a></li>
<li>chore(release): 5.5.2 by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1902">codecov/codecov-action#1902</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/maxweng-sentry"><code>@​maxweng-sentry</code></a>
made their first contribution in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1894">codecov/codecov-action#1894</a></li>
<li><a
href="https://github.com/freemanzMrojo"><code>@​freemanzMrojo</code></a>
made their first contribution in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1881">codecov/codecov-action#1881</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/codecov/codecov-action/compare/v5.5.1...v5.5.2">https://github.com/codecov/codecov-action/compare/v5.5.1...v5.5.2</a></p>
<h2>v5.5.1</h2>
<h2>What's Changed</h2>
<ul>
<li>build(deps): bump ossf/scorecard-action from 2.4.1 to 2.4.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1833">codecov/codecov-action#1833</a></li>
<li>build(deps): bump github/codeql-action from 3.28.18 to 3.29.9 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1861">codecov/codecov-action#1861</a></li>
<li>Document a <code>codecov-cli</code> version reference example by <a
href="https://github.com/webknjaz"><code>@​webknjaz</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1774">codecov/codecov-action#1774</a></li>
<li>docs: fix typo in README by <a
href="https://github.com/datalater"><code>@​datalater</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1866">codecov/codecov-action#1866</a></li>
<li>fix: update to use local app/ dir by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1872">codecov/codecov-action#1872</a></li>
<li>build(deps): bump github/codeql-action from 3.29.9 to 3.29.11 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1867">codecov/codecov-action#1867</a></li>
<li>build(deps): bump actions/checkout from 4.2.2 to 5.0.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1868">codecov/codecov-action#1868</a></li>
<li>fix: overwrite pr number on fork by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1871">codecov/codecov-action#1871</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md">codecov/codecov-action's
changelog</a>.</em></p>
<blockquote>
<h2>v5.5.2</h2>
<h3>What's Changed</h3>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/codecov/codecov-action/compare/v5.5.1..v5.5.2">https://github.com/codecov/codecov-action/compare/v5.5.1..v5.5.2</a></p>
<h2>v5.5.1</h2>
<h3>What's Changed</h3>
<ul>
<li>fix: overwrite pr number on fork by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1871">codecov/codecov-action#1871</a></li>
<li>build(deps): bump actions/checkout from 4.2.2 to 5.0.0 by
<code>@​app/dependabot</code> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1868">codecov/codecov-action#1868</a></li>
<li>build(deps): bump github/codeql-action from 3.29.9 to 3.29.11 by
<code>@​app/dependabot</code> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1867">codecov/codecov-action#1867</a></li>
<li>fix: update to use local app/ dir by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1872">codecov/codecov-action#1872</a></li>
<li>docs: fix typo in README by <a
href="https://github.com/datalater"><code>@​datalater</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1866">codecov/codecov-action#1866</a></li>
<li>Document a <code>codecov-cli</code> version reference example by <a
href="https://github.com/webknjaz"><code>@​webknjaz</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1774">codecov/codecov-action#1774</a></li>
<li>build(deps): bump github/codeql-action from 3.28.18 to 3.29.9 by
<code>@​app/dependabot</code> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1861">codecov/codecov-action#1861</a></li>
<li>build(deps): bump ossf/scorecard-action from 2.4.1 to 2.4.2 by
<code>@​app/dependabot</code> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1833">codecov/codecov-action#1833</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/codecov/codecov-action/compare/v5.5.0..v5.5.1">https://github.com/codecov/codecov-action/compare/v5.5.0..v5.5.1</a></p>
<h2>v5.5.0</h2>
<h3>What's Changed</h3>
<ul>
<li>feat: upgrade wrapper to 0.2.4 by <a
href="https://github.com/jviall"><code>@​jviall</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1864">codecov/codecov-action#1864</a></li>
<li>Pin actions/github-script by Git SHA by <a
href="https://github.com/martincostello"><code>@​martincostello</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1859">codecov/codecov-action#1859</a></li>
<li>fix: check reqs exist by <a
href="https://github.com/joseph-sentry"><code>@​joseph-sentry</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1835">codecov/codecov-action#1835</a></li>
<li>fix: Typo in README by <a
href="https://github.com/spalmurray"><code>@​spalmurray</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1838">codecov/codecov-action#1838</a></li>
<li>docs: Refine OIDC docs by <a
href="https://github.com/spalmurray"><code>@​spalmurray</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1837">codecov/codecov-action#1837</a></li>
<li>build(deps): bump github/codeql-action from 3.28.17 to 3.28.18 by
<code>@​app/dependabot</code> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1829">codecov/codecov-action#1829</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/codecov/codecov-action/compare/v5.4.3..v5.5.0">https://github.com/codecov/codecov-action/compare/v5.4.3..v5.5.0</a></p>
<h2>v5.4.3</h2>
<h3>What's Changed</h3>
<ul>
<li>build(deps): bump github/codeql-action from 3.28.13 to 3.28.17 by
<code>@​app/dependabot</code> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1822">codecov/codecov-action#1822</a></li>
<li>fix: OIDC on forks by <a
href="https://github.com/joseph-sentry"><code>@​joseph-sentry</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1823">codecov/codecov-action#1823</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/codecov/codecov-action/compare/v5.4.2..v5.4.3">https://github.com/codecov/codecov-action/compare/v5.4.2..v5.4.3</a></p>
<h2>v5.4.2</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/codecov/codecov-action/commit/57e3a136b779b570ffcdbf80b3bdc90e7fab3de2"><code>57e3a13</code></a>
Th/6.0.0 (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1928">#1928</a>)</li>
<li><a
href="https://github.com/codecov/codecov-action/commit/f67d33dda8a42b51c42a8318a1f66468119e898b"><code>f67d33d</code></a>
Revert &quot;Revert &quot;build(deps): bump actions/github-script from
7.0.1 to 8.0.0&quot;&quot;...</li>
<li>See full diff in <a
href="https://github.com/codecov/codecov-action/compare/v5...v6">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=codecov/codecov-action&package-manager=github_actions&previous-version=5&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 08:47:00 +01:00
dependabot[bot]andlnx01 bcd383bdff ci(deps): bump actions/deploy-pages from 4 to 5 in the actions-core group (#131)
Bumps the actions-core group with 1 update:
[actions/deploy-pages](https://github.com/actions/deploy-pages).

Updates `actions/deploy-pages` from 4 to 5
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/deploy-pages/releases">actions/deploy-pages's
releases</a>.</em></p>
<blockquote>
<h2>v5.0.0</h2>
<h1>Changelog</h1>
<ul>
<li>Update Node.js version to 24.x <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/404">#404</a>)</li>
<li>Add workflow file for publishing releases to immutable action
package <a
href="https://github.com/Jcambass"><code>@​Jcambass</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/374">#374</a>)</li>
<li>Bump braces from 3.0.2 to 3.0.3 in the npm_and_yarn group across 1
directory <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/360">#360</a>)</li>
<li>Make the rebuild dist workflow work nicer with Dependabot <a
href="https://github.com/yoannchaudet"><code>@​yoannchaudet</code></a>
(<a
href="https://redirect.github.com/actions/deploy-pages/issues/361">#361</a>)</li>
<li>Bump the non-breaking-changes group across 1 directory with 3
updates <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/358">#358</a>)</li>
<li>Delete repeated sentence <a
href="https://github.com/garethsb"><code>@​garethsb</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/359">#359</a>)</li>
<li>Update README.md <a
href="https://github.com/tsusdere"><code>@​tsusdere</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/348">#348</a>)</li>
<li>Bump the non-breaking-changes group with 4 updates <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/341">#341</a>)</li>
<li>Remove error message for file permissions <a
href="https://github.com/TooManyBees"><code>@​TooManyBees</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/340">#340</a>)</li>
</ul>
<hr />
<p>See details of <a
href="https://github.com/actions/deploy-pages/compare/v4.0.5...v4.0.6">all
code changes</a> since previous release.</p>
<p>⚠️ For use with products other than GitHub.com, such as GitHub
Enterprise Server, please consult the <a
href="https://github.com/actions/deploy-pages/#compatibility">compatibility
table</a>.</p>
<h2>v4.0.5</h2>
<h1>Changelog</h1>
<ul>
<li>On API error, the error message will surface the API request ID <a
href="https://github.com/TooManyBees"><code>@​TooManyBees</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/324">#324</a>)</li>
<li>Bump the non-breaking-changes group with 2 updates <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/318">#318</a>)</li>
<li>Bump the non-breaking-changes group with 1 update <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/316">#316</a>)</li>
<li>Bump the non-breaking-changes group with 3 updates <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/314">#314</a>)</li>
<li>Bump release-drafter/release-drafter from 5.25.0 to 6.0.0 <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/311">#311</a>)</li>
</ul>
<hr />
<p>See details of <a
href="https://github.com/actions/deploy-pages/compare/v4.0.4...v4.0.5">all
code changes</a> since previous release.</p>
<p>⚠️ For use with products other than GitHub.com, such as GitHub
Enterprise Server, please consult the <a
href="https://github.com/actions/deploy-pages/#compatibility">compatibility
table</a>.</p>
<h2>v4.0.4</h2>
<h1>Changelog</h1>
<ul>
<li>Update api-client.js <a
href="https://github.com/lmammino"><code>@​lmammino</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/295">#295</a>)</li>
<li>fix typo: compatibilty -&gt; compatibility <a
href="https://github.com/SimonSiefke"><code>@​SimonSiefke</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/298">#298</a>)</li>
<li>Bump <code>@​actions/artifact</code> from 2.0.1 to 2.1.1 <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/310">#310</a>)</li>
<li>Update Dependabot config to group non-breaking changes <a
href="https://github.com/JamesMGreene"><code>@​JamesMGreene</code></a>
(<a
href="https://redirect.github.com/actions/deploy-pages/issues/307">#307</a>)</li>
</ul>
<hr />
<p>See details of <a
href="https://github.com/actions/deploy-pages/compare/v4.0.3...v4.0.4">all
code changes</a> since previous release.</p>
<p>⚠️ For use with products other than GitHub.com, such as GitHub
Enterprise Server, please consult the <a
href="https://github.com/actions/deploy-pages/#compatibility">compatibility
table</a>.</p>
<h2>v4.0.3</h2>
<h1>Changelog</h1>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/deploy-pages/commit/cd2ce8fcbc39b97be8ca5fce6e763baed58fa128"><code>cd2ce8f</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/deploy-pages/issues/404">#404</a>
from salmanmkc/node24</li>
<li><a
href="https://github.com/actions/deploy-pages/commit/bbe2a950ee52d4f5cbe74e6d9d6a8803676e91d5"><code>bbe2a95</code></a>
Update Node.js version to 24.x</li>
<li><a
href="https://github.com/actions/deploy-pages/commit/854d7aa1b99e4509c4d1b53d69b7ba4eaf39215a"><code>854d7aa</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/deploy-pages/issues/374">#374</a>
from actions/Jcambass-patch-1</li>
<li><a
href="https://github.com/actions/deploy-pages/commit/306bb814f29679fd12f0e4b0014bc1f3a7e7f4bc"><code>306bb81</code></a>
Add workflow file for publishing releases to immutable action
package</li>
<li><a
href="https://github.com/actions/deploy-pages/commit/b74272834adc04f971da4b0b055c49fa8d7f90c9"><code>b742728</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/deploy-pages/issues/360">#360</a>
from actions/dependabot/npm_and_yarn/npm_and_yarn-513...</li>
<li><a
href="https://github.com/actions/deploy-pages/commit/72732942c639e67ea3f70165fd2e012dd6d95027"><code>7273294</code></a>
Bump braces in the npm_and_yarn group across 1 directory</li>
<li><a
href="https://github.com/actions/deploy-pages/commit/963791f01c40ef3eff219c255dbfb97a6f2c9f87"><code>963791f</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/deploy-pages/issues/361">#361</a>
from actions/dependabot-friendly</li>
<li><a
href="https://github.com/actions/deploy-pages/commit/51bb29d9d7bfe15d731c4957ce1887b5ae8c6727"><code>51bb29d</code></a>
Make the rebuild dist workflow safer for Dependabot</li>
<li><a
href="https://github.com/actions/deploy-pages/commit/89f3d10406f57ee86e6517a982b3fb0438bd6dc5"><code>89f3d10</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/deploy-pages/issues/358">#358</a>
from actions/dependabot/npm_and_yarn/non-breaking-cha...</li>
<li><a
href="https://github.com/actions/deploy-pages/commit/bce735589bbbfa569f1d2ac003277b590d743e4c"><code>bce7355</code></a>
Merge branch 'main' into
dependabot/npm_and_yarn/non-breaking-changes-99c12deb21</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/deploy-pages/compare/v4...v5">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/deploy-pages&package-manager=github_actions&previous-version=4&new-version=5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 08:46:51 +01:00
dependabot[bot]andlnx01 7a09a2ddc0 deps(deps): bump golang.org/x/image from 0.37.0 to 0.38.0 in the golang group (#130)
Bumps the golang group with 1 update:
[golang.org/x/image](https://github.com/golang/image).

Updates `golang.org/x/image` from 0.37.0 to 0.38.0
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/golang/image/commit/23ae9ed61c1d3343fb95015810f62dcbf444976e"><code>23ae9ed</code></a>
tiff: cap buffer growth to prevent OOM from malicious IFD offset</li>
<li><a
href="https://github.com/golang/image/commit/e589e60f29d0bbbf6400e250e024f93cbc4961ee"><code>e589e60</code></a>
webp: allow VP8L + VP8X(with alpha)</li>
<li>See full diff in <a
href="https://github.com/golang/image/compare/v0.37.0...v0.38.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=golang.org/x/image&package-manager=go_modules&previous-version=0.37.0&new-version=0.38.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 08:46:41 +01:00
Tobias Gesellchen b04b0bcc32 Add account registration/login (#129) 2026-03-27 08:37:50 +01:00
198 changed files with 11365 additions and 6389 deletions
-204
View File
@@ -1,204 +0,0 @@
---
name: adapt
description: Adapt designs to work across different screen sizes, devices, contexts, or platforms. Ensures consistent experience across varied environments.
user-invocable: true
args:
- name: target
description: The feature or component to adapt (optional)
required: false
- name: context
description: What to adapt for (mobile, tablet, desktop, print, email, etc.)
required: false
---
Adapt existing designs to work effectively across different contexts - different screen sizes, devices, platforms, or use cases.
## MANDATORY PREPARATION
Use the frontend-design skill — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run teach-impeccable first. Additionally gather: target platforms/devices and usage contexts.
---
## Assess Adaptation Challenge
Understand what needs adaptation and why:
1. **Identify the source context**:
- What was it designed for originally? (Desktop web? Mobile app?)
- What assumptions were made? (Large screen? Mouse input? Fast connection?)
- What works well in current context?
2. **Understand target context**:
- **Device**: Mobile, tablet, desktop, TV, watch, print?
- **Input method**: Touch, mouse, keyboard, voice, gamepad?
- **Screen constraints**: Size, resolution, orientation?
- **Connection**: Fast wifi, slow 3G, offline?
- **Usage context**: On-the-go vs desk, quick glance vs focused reading?
- **User expectations**: What do users expect on this platform?
3. **Identify adaptation challenges**:
- What won't fit? (Content, navigation, features)
- What won't work? (Hover states on touch, tiny touch targets)
- What's inappropriate? (Desktop patterns on mobile, mobile patterns on desktop)
**CRITICAL**: Adaptation is not just scaling - it's rethinking the experience for the new context.
## Plan Adaptation Strategy
Create context-appropriate strategy:
### Mobile Adaptation (Desktop → Mobile)
**Layout Strategy**:
- Single column instead of multi-column
- Vertical stacking instead of side-by-side
- Full-width components instead of fixed widths
- Bottom navigation instead of top/side navigation
**Interaction Strategy**:
- Touch targets 44x44px minimum (not hover-dependent)
- Swipe gestures where appropriate (lists, carousels)
- Bottom sheets instead of dropdowns
- Thumbs-first design (controls within thumb reach)
- Larger tap areas with more spacing
**Content Strategy**:
- Progressive disclosure (don't show everything at once)
- Prioritize primary content (secondary content in tabs/accordions)
- Shorter text (more concise)
- Larger text (16px minimum)
**Navigation Strategy**:
- Hamburger menu or bottom navigation
- Reduce navigation complexity
- Sticky headers for context
- Back button in navigation flow
### Tablet Adaptation (Hybrid Approach)
**Layout Strategy**:
- Two-column layouts (not single or three-column)
- Side panels for secondary content
- Master-detail views (list + detail)
- Adaptive based on orientation (portrait vs landscape)
**Interaction Strategy**:
- Support both touch and pointer
- Touch targets 44x44px but allow denser layouts than phone
- Side navigation drawers
- Multi-column forms where appropriate
### Desktop Adaptation (Mobile → Desktop)
**Layout Strategy**:
- Multi-column layouts (use horizontal space)
- Side navigation always visible
- Multiple information panels simultaneously
- Fixed widths with max-width constraints (don't stretch to 4K)
**Interaction Strategy**:
- Hover states for additional information
- Keyboard shortcuts
- Right-click context menus
- Drag and drop where helpful
- Multi-select with Shift/Cmd
**Content Strategy**:
- Show more information upfront (less progressive disclosure)
- Data tables with many columns
- Richer visualizations
- More detailed descriptions
### Print Adaptation (Screen → Print)
**Layout Strategy**:
- Page breaks at logical points
- Remove navigation, footer, interactive elements
- Black and white (or limited color)
- Proper margins for binding
**Content Strategy**:
- Expand shortened content (show full URLs, hidden sections)
- Add page numbers, headers, footers
- Include metadata (print date, page title)
- Convert charts to print-friendly versions
### Email Adaptation (Web → Email)
**Layout Strategy**:
- Narrow width (600px max)
- Single column only
- Inline CSS (no external stylesheets)
- Table-based layouts (for email client compatibility)
**Interaction Strategy**:
- Large, obvious CTAs (buttons not text links)
- No hover states (not reliable)
- Deep links to web app for complex interactions
## Implement Adaptations
Apply changes systematically:
### Responsive Breakpoints
Choose appropriate breakpoints:
- Mobile: 320px-767px
- Tablet: 768px-1023px
- Desktop: 1024px+
- Or content-driven breakpoints (where design breaks)
### Layout Adaptation Techniques
- **CSS Grid/Flexbox**: Reflow layouts automatically
- **Container Queries**: Adapt based on container, not viewport
- **`clamp()`**: Fluid sizing between min and max
- **Media queries**: Different styles for different contexts
- **Display properties**: Show/hide elements per context
### Touch Adaptation
- Increase touch target sizes (44x44px minimum)
- Add more spacing between interactive elements
- Remove hover-dependent interactions
- Add touch feedback (ripples, highlights)
- Consider thumb zones (easier to reach bottom than top)
### Content Adaptation
- Use `display: none` sparingly (still downloads)
- Progressive enhancement (core content first, enhancements on larger screens)
- Lazy loading for off-screen content
- Responsive images (`srcset`, `picture` element)
### Navigation Adaptation
- Transform complex nav to hamburger/drawer on mobile
- Bottom nav bar for mobile apps
- Persistent side navigation on desktop
- Breadcrumbs on smaller screens for context
**IMPORTANT**: Test on real devices, not just browser DevTools. Device emulation is helpful but not perfect.
**NEVER**:
- Hide core functionality on mobile (if it matters, make it work)
- Assume desktop = powerful device (consider accessibility, older machines)
- Use different information architecture across contexts (confusing)
- Break user expectations for platform (mobile users expect mobile patterns)
- Forget landscape orientation on mobile/tablet
- Use generic breakpoints blindly (use content-driven breakpoints)
- Ignore touch on desktop (many desktop devices have touch)
## Verify Adaptations
Test thoroughly across contexts:
- **Real devices**: Test on actual phones, tablets, desktops
- **Different orientations**: Portrait and landscape
- **Different browsers**: Safari, Chrome, Firefox, Edge
- **Different OS**: iOS, Android, Windows, macOS
- **Different input methods**: Touch, mouse, keyboard
- **Edge cases**: Very small screens (320px), very large screens (4K)
- **Slow connections**: Test on throttled network
Remember: You're a cross-platform design expert. Make experiences that feel native to each context while maintaining brand and functionality consistency. Adapt intentionally, test thoroughly.
-174
View File
@@ -1,174 +0,0 @@
---
name: animate
description: Review a feature and enhance it with purposeful animations, micro-interactions, and motion effects that improve usability and delight.
user-invocable: true
argument-hint: [TARGET=<value>]
---
Analyze a feature and strategically add animations and micro-interactions that enhance understanding, provide feedback, and create delight.
## MANDATORY PREPARATION
Use the frontend-design skill — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run teach-impeccable first. Additionally gather: performance constraints.
---
## Assess Animation Opportunities
Analyze where motion would improve the experience:
1. **Identify static areas**:
- **Missing feedback**: Actions without visual acknowledgment (button clicks, form submission, etc.)
- **Jarring transitions**: Instant state changes that feel abrupt (show/hide, page loads, route changes)
- **Unclear relationships**: Spatial or hierarchical relationships that aren't obvious
- **Lack of delight**: Functional but joyless interactions
- **Missed guidance**: Opportunities to direct attention or explain behavior
2. **Understand the context**:
- What's the personality? (Playful vs serious, energetic vs calm)
- What's the performance budget? (Mobile-first? Complex page?)
- Who's the audience? (Motion-sensitive users? Power users who want speed?)
- What matters most? (One hero animation vs many micro-interactions?)
If any of these are unclear from the codebase, ask the user directly to clarify what you cannot infer.
**CRITICAL**: Respect `prefers-reduced-motion`. Always provide non-animated alternatives for users who need them.
## Plan Animation Strategy
Create a purposeful animation plan:
- **Hero moment**: What's the ONE signature animation? (Page load? Hero section? Key interaction?)
- **Feedback layer**: Which interactions need acknowledgment?
- **Transition layer**: Which state changes need smoothing?
- **Delight layer**: Where can we surprise and delight?
**IMPORTANT**: One well-orchestrated experience beats scattered animations everywhere. Focus on high-impact moments.
## Implement Animations
Add motion systematically across these categories:
### Entrance Animations
- **Page load choreography**: Stagger element reveals (100-150ms delays), fade + slide combinations
- **Hero section**: Dramatic entrance for primary content (scale, parallax, or creative effects)
- **Content reveals**: Scroll-triggered animations using intersection observer
- **Modal/drawer entry**: Smooth slide + fade, backdrop fade, focus management
### Micro-interactions
- **Button feedback**:
- Hover: Subtle scale (1.02-1.05), color shift, shadow increase
- Click: Quick scale down then up (0.95 → 1), ripple effect
- Loading: Spinner or pulse state
- **Form interactions**:
- Input focus: Border color transition, slight scale or glow
- Validation: Shake on error, check mark on success, smooth color transitions
- **Toggle switches**: Smooth slide + color transition (200-300ms)
- **Checkboxes/radio**: Check mark animation, ripple effect
- **Like/favorite**: Scale + rotation, particle effects, color transition
### State Transitions
- **Show/hide**: Fade + slide (not instant), appropriate timing (200-300ms)
- **Expand/collapse**: Height transition with overflow handling, icon rotation
- **Loading states**: Skeleton screen fades, spinner animations, progress bars
- **Success/error**: Color transitions, icon animations, gentle scale pulse
- **Enable/disable**: Opacity transitions, cursor changes
### Navigation & Flow
- **Page transitions**: Crossfade between routes, shared element transitions
- **Tab switching**: Slide indicator, content fade/slide
- **Carousel/slider**: Smooth transforms, snap points, momentum
- **Scroll effects**: Parallax layers, sticky headers with state changes, scroll progress indicators
### Feedback & Guidance
- **Hover hints**: Tooltip fade-ins, cursor changes, element highlights
- **Drag & drop**: Lift effect (shadow + scale), drop zone highlights, smooth repositioning
- **Copy/paste**: Brief highlight flash on paste, "copied" confirmation
- **Focus flow**: Highlight path through form or workflow
### Delight Moments
- **Empty states**: Subtle floating animations on illustrations
- **Completed actions**: Confetti, check mark flourish, success celebrations
- **Easter eggs**: Hidden interactions for discovery
- **Contextual animation**: Weather effects, time-of-day themes, seasonal touches
## Technical Implementation
Use appropriate techniques for each animation:
### Timing & Easing
**Durations by purpose:**
- **100-150ms**: Instant feedback (button press, toggle)
- **200-300ms**: State changes (hover, menu open)
- **300-500ms**: Layout changes (accordion, modal)
- **500-800ms**: Entrance animations (page load)
**Easing curves (use these, not CSS defaults):**
```css
/* Recommended - natural deceleration */
--ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1); /* Smooth, refined */
--ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1); /* Slightly snappier */
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); /* Confident, decisive */
/* AVOID - feel dated and tacky */
/* bounce: cubic-bezier(0.34, 1.56, 0.64, 1); */
/* elastic: cubic-bezier(0.68, -0.6, 0.32, 1.6); */
```
**Exit animations are faster than entrances.** Use ~75% of enter duration.
### CSS Animations
```css
/* Prefer for simple, declarative animations */
- transitions for state changes
- @keyframes for complex sequences
- transform + opacity only (GPU-accelerated)
```
### JavaScript Animation
```javascript
/* Use for complex, interactive animations */
- Web Animations API for programmatic control
- Framer Motion for React
- GSAP for complex sequences
```
### Performance
- **GPU acceleration**: Use `transform` and `opacity`, avoid layout properties
- **will-change**: Add sparingly for known expensive animations
- **Reduce paint**: Minimize repaints, use `contain` where appropriate
- **Monitor FPS**: Ensure 60fps on target devices
### Accessibility
```css
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
```
**NEVER**:
- Use bounce or elastic easing curves—they feel dated and draw attention to the animation itself
- Animate layout properties (width, height, top, left)—use transform instead
- Use durations over 500ms for feedback—it feels laggy
- Animate without purpose—every animation needs a reason
- Ignore `prefers-reduced-motion`—this is an accessibility violation
- Animate everything—animation fatigue makes interfaces feel exhausting
- Block interaction during animations unless intentional
## Verify Quality
Test animations thoroughly:
- **Smooth at 60fps**: No jank on target devices
- **Feels natural**: Easing curves feel organic, not robotic
- **Appropriate timing**: Not too fast (jarring) or too slow (laggy)
- **Reduced motion works**: Animations disabled or simplified appropriately
- **Doesn't block**: Users can interact during/after animations
- **Adds value**: Makes interface clearer or more delightful
Remember: Motion should enhance understanding and provide feedback, not just add decoration. Animate with purpose, respect performance constraints, and always consider accessibility. Great animation is invisible - it just makes everything feel right.
-124
View File
@@ -1,124 +0,0 @@
---
name: arrange
description: Improve layout, spacing, and visual rhythm. Fixes monotonous grids, inconsistent spacing, and weak visual hierarchy to create intentional compositions.
user-invocable: true
argument-hint: [TARGET=<value>]
---
Assess and improve layout and spacing that feels monotonous, crowded, or structurally weak — turning generic arrangements into intentional, rhythmic compositions.
## MANDATORY PREPARATION
Use the frontend-design skill — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run teach-impeccable first.
---
## Assess Current Layout
Analyze what's weak about the current spatial design:
1. **Spacing**:
- Is spacing consistent or arbitrary? (Random padding/margin values)
- Is all spacing the same? (Equal padding everywhere = no rhythm)
- Are related elements grouped tightly, with generous space between groups?
2. **Visual hierarchy**:
- Apply the squint test: blur your (metaphorical) eyes — can you still identify the most important element, second most important, and clear groupings?
- Is hierarchy achieved effectively? (Space and weight alone can be enough — but is the current approach working?)
- Does whitespace guide the eye to what matters?
3. **Grid & structure**:
- Is there a clear underlying structure, or does the layout feel random?
- Are identical card grids used everywhere? (Icon + heading + text, repeated endlessly)
- Is everything centered? (Left-aligned with asymmetric layouts feels more designed, but not a hard and fast rule)
4. **Rhythm & variety**:
- Does the layout have visual rhythm? (Alternating tight/generous spacing)
- Is every section structured the same way? (Monotonous repetition)
- Are there intentional moments of surprise or emphasis?
5. **Density**:
- Is the layout too cramped? (Not enough breathing room)
- Is the layout too sparse? (Excessive whitespace without purpose)
- Does density match the content type? (Data-dense UIs need tighter spacing; marketing pages need more air)
**CRITICAL**: Layout problems are often the root cause of interfaces feeling "off" even when colors and fonts are fine. Space is a design material — use it with intention.
## Plan Layout Improvements
Consult the [spatial design reference](reference/spatial-design.md) from the frontend-design skill for detailed guidance on grids, rhythm, and container queries.
Create a systematic plan:
- **Spacing system**: Use a consistent scale — whether that's a framework's built-in scale (e.g., Tailwind), rem-based tokens, or a custom system. The specific values matter less than consistency.
- **Hierarchy strategy**: How will space communicate importance?
- **Layout approach**: What structure fits the content? Flex for 1D, Grid for 2D, named areas for complex page layouts.
- **Rhythm**: Where should spacing be tight vs generous?
## Improve Layout Systematically
### Establish a Spacing System
- Use a consistent spacing scale — framework scales (Tailwind, etc.), rem-based tokens, or a custom scale all work. What matters is that values come from a defined set, not arbitrary numbers.
- Name tokens semantically if using custom properties: `--space-xs` through `--space-xl`, not `--spacing-8`
- Use `gap` for sibling spacing instead of margins — eliminates margin collapse hacks
- Apply `clamp()` for fluid spacing that breathes on larger screens
### Create Visual Rhythm
- **Tight grouping** for related elements (8-12px between siblings)
- **Generous separation** between distinct sections (48-96px)
- **Varied spacing** within sections — not every row needs the same gap
- **Asymmetric compositions** — break the predictable centered-content pattern when it makes sense
### Choose the Right Layout Tool
- **Use Flexbox for 1D layouts**: Rows of items, nav bars, button groups, card contents, most component internals. Flex is simpler and more appropriate for the majority of layout tasks.
- **Use Grid for 2D layouts**: Page-level structure, dashboards, data-dense interfaces, anything where rows AND columns need coordinated control.
- **Don't default to Grid** when Flexbox with `flex-wrap` would be simpler and more flexible.
- Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints.
- Use named grid areas (`grid-template-areas`) for complex page layouts — redefine at breakpoints.
### Break Card Grid Monotony
- Don't default to card grids for everything — spacing and alignment create visual grouping naturally
- Use cards only when content is truly distinct and actionable — never nest cards inside cards
- Vary card sizes, span columns, or mix cards with non-card content to break repetition
### Strengthen Visual Hierarchy
- Use the fewest dimensions needed for clear hierarchy. Space alone can be enough — generous whitespace around an element draws the eye. Some of the most sophisticated designs achieve rhythm with just space and weight. Add color or size contrast only when simpler means aren't sufficient.
- Be aware of reading flow — in LTR languages, the eye naturally scans top-left to bottom-right, but primary action placement depends on context (e.g., bottom-right in dialogs, top in navigation).
- Create clear content groupings through proximity and separation.
### Manage Depth & Elevation
- Create a semantic z-index scale (dropdown → sticky → modal-backdrop → modal → toast → tooltip)
- Build a consistent shadow scale (sm → md → lg → xl) — shadows should be subtle
- Use elevation to reinforce hierarchy, not as decoration
### Optical Adjustments
- If an icon looks visually off-center despite being geometrically centered, nudge it — but only if you're confident it actually looks wrong. Don't adjust speculatively.
**NEVER**:
- Use arbitrary spacing values outside your scale
- Make all spacing equal — variety creates hierarchy
- Wrap everything in cards — not everything needs a container
- Nest cards inside cards — use spacing and dividers for hierarchy within
- Use identical card grids everywhere (icon + heading + text, repeated)
- Center everything — left-aligned with asymmetry feels more designed
- Default to the hero metric layout (big number, small label, stats, gradient) as a template. If showing real user data, a prominent metric can work — but it should display actual data, not decorative numbers.
- Default to CSS Grid when Flexbox would be simpler — use the simplest tool for the job
- Use arbitrary z-index values (999, 9999) — build a semantic scale
## Verify Layout Improvements
- **Squint test**: Can you identify primary, secondary, and groupings with blurred vision?
- **Rhythm**: Does the page have a satisfying beat of tight and generous spacing?
- **Hierarchy**: Is the most important content obvious within 2 seconds?
- **Breathing room**: Does the layout feel comfortable, not cramped or wasteful?
- **Consistency**: Is the spacing system applied uniformly?
- **Responsiveness**: Does the layout adapt gracefully across screen sizes?
Remember: Space is the most underused design tool. A layout with the right rhythm and hierarchy can make even simple content feel polished and intentional.
-125
View File
@@ -1,125 +0,0 @@
---
name: audit
description: Perform comprehensive audit of interface quality across accessibility, performance, theming, and responsive design. Generates detailed report of issues with severity ratings and recommendations.
user-invocable: true
argument-hint: [AREA=<value>]
---
Run systematic quality checks and generate a comprehensive audit report with prioritized issues and actionable recommendations. Don't fix issues - document them for other commands to address.
**First**: Use the frontend-design skill for design principles and anti-patterns.
## Diagnostic Scan
Run comprehensive checks across multiple dimensions:
1. **Accessibility (A11y)** - Check for:
- **Contrast issues**: Text contrast ratios < 4.5:1 (or 7:1 for AAA)
- **Missing ARIA**: Interactive elements without proper roles, labels, or states
- **Keyboard navigation**: Missing focus indicators, illogical tab order, keyboard traps
- **Semantic HTML**: Improper heading hierarchy, missing landmarks, divs instead of buttons
- **Alt text**: Missing or poor image descriptions
- **Form issues**: Inputs without labels, poor error messaging, missing required indicators
2. **Performance** - Check for:
- **Layout thrashing**: Reading/writing layout properties in loops
- **Expensive animations**: Animating layout properties (width, height, top, left) instead of transform/opacity
- **Missing optimization**: Images without lazy loading, unoptimized assets, missing will-change
- **Bundle size**: Unnecessary imports, unused dependencies
- **Render performance**: Unnecessary re-renders, missing memoization
3. **Theming** - Check for:
- **Hard-coded colors**: Colors not using design tokens
- **Broken dark mode**: Missing dark mode variants, poor contrast in dark theme
- **Inconsistent tokens**: Using wrong tokens, mixing token types
- **Theme switching issues**: Values that don't update on theme change
4. **Responsive Design** - Check for:
- **Fixed widths**: Hard-coded widths that break on mobile
- **Touch targets**: Interactive elements < 44x44px
- **Horizontal scroll**: Content overflow on narrow viewports
- **Text scaling**: Layouts that break when text size increases
- **Missing breakpoints**: No mobile/tablet variants
5. **Anti-Patterns (CRITICAL)** - Check against ALL the **DON'T** guidelines in the frontend-design skill. Look for AI slop tells (AI color palette, gradient text, glassmorphism, hero metrics, card grids, generic fonts) and general design anti-patterns (gray on color, nested cards, bounce easing, redundant copy).
**CRITICAL**: This is an audit, not a fix. Document issues thoroughly with clear explanations of impact. Use other commands (normalize, optimize, harden, etc.) to fix issues after audit.
## Generate Comprehensive Report
Create a detailed audit report with the following structure:
### Anti-Patterns Verdict
**Start here.** Pass/fail: Does this look AI-generated? List specific tells from the skill's Anti-Patterns section. Be brutally honest.
### Executive Summary
- Total issues found (count by severity)
- Most critical issues (top 3-5)
- Overall quality score (if applicable)
- Recommended next steps
### Detailed Findings by Severity
For each issue, document:
- **Location**: Where the issue occurs (component, file, line)
- **Severity**: Critical / High / Medium / Low
- **Category**: Accessibility / Performance / Theming / Responsive
- **Description**: What the issue is
- **Impact**: How it affects users
- **WCAG/Standard**: Which standard it violates (if applicable)
- **Recommendation**: How to fix it
- **Suggested command**: Which command to use (prefer: /animate, /quieter, /optimize, /adapt, /clarify, /distill, /delight, /onboard, /normalize, /audit, /harden, /polish, /extract, /bolder, /arrange, /typeset, /critique, /colorize, /overdrive — or other installed skills you're sure exist)
#### Critical Issues
[Issues that block core functionality or violate WCAG A]
#### High-Severity Issues
[Significant usability/accessibility impact, WCAG AA violations]
#### Medium-Severity Issues
[Quality issues, WCAG AAA violations, performance concerns]
#### Low-Severity Issues
[Minor inconsistencies, optimization opportunities]
### Patterns & Systemic Issues
Identify recurring problems:
- "Hard-coded colors appear in 15+ components, should use design tokens"
- "Touch targets consistently too small (<44px) throughout mobile experience"
- "Missing focus indicators on all custom interactive components"
### Positive Findings
Note what's working well:
- Good practices to maintain
- Exemplary implementations to replicate elsewhere
### Recommendations by Priority
Create actionable plan:
1. **Immediate**: Critical blockers to fix first
2. **Short-term**: High-severity issues (this sprint)
3. **Medium-term**: Quality improvements (next sprint)
4. **Long-term**: Nice-to-haves and optimizations
### Suggested Commands for Fixes
Map issues to available commands. Prefer these: /animate, /quieter, /optimize, /adapt, /clarify, /distill, /delight, /onboard, /normalize, /audit, /harden, /polish, /extract, /bolder, /arrange, /typeset, /critique, /colorize, /overdrive. You may also suggest other installed skills you're sure exist, but never invent commands.
Examples:
- "Use `/normalize` to align with design system (addresses N theming issues)"
- "Use `/optimize` to improve performance (addresses N performance issues)"
- "Use `/harden` to improve resilience (addresses N edge cases)"
**IMPORTANT**: Be thorough but actionable. Too many low-priority issues creates noise. Focus on what actually matters.
**NEVER**:
- Report issues without explaining impact (why does this matter?)
- Mix severity levels inconsistently
- Skip positive findings (celebrate what works)
- Provide generic recommendations (be specific and actionable)
- Forget to prioritize (everything can't be critical)
- Report false positives without verification
Remember: You're a quality auditor with exceptional attention to detail. Document systematically, prioritize ruthlessly, and provide clear paths to improvement. A good audit makes fixing easy.
-116
View File
@@ -1,116 +0,0 @@
---
name: bolder
description: Amplify safe or boring designs to make them more visually interesting and stimulating. Increases impact while maintaining usability.
user-invocable: true
argument-hint: [TARGET=<value>]
---
Increase visual impact and personality in designs that are too safe, generic, or visually underwhelming, creating more engaging and memorable experiences.
## MANDATORY PREPARATION
Use the frontend-design skill — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run teach-impeccable first.
---
## Assess Current State
Analyze what makes the design feel too safe or boring:
1. **Identify weakness sources**:
- **Generic choices**: System fonts, basic colors, standard layouts
- **Timid scale**: Everything is medium-sized with no drama
- **Low contrast**: Everything has similar visual weight
- **Static**: No motion, no energy, no life
- **Predictable**: Standard patterns with no surprises
- **Flat hierarchy**: Nothing stands out or commands attention
2. **Understand the context**:
- What's the brand personality? (How far can we push?)
- What's the purpose? (Marketing can be bolder than financial dashboards)
- Who's the audience? (What will resonate?)
- What are the constraints? (Brand guidelines, accessibility, performance)
If any of these are unclear from the codebase, ask the user directly to clarify what you cannot infer.
**CRITICAL**: "Bolder" doesn't mean chaotic or garish. It means distinctive, memorable, and confident. Think intentional drama, not random chaos.
**WARNING - AI SLOP TRAP**: When making things "bolder," AI defaults to the same tired tricks: cyan/purple gradients, glassmorphism, neon accents on dark backgrounds, gradient text on metrics. These are the OPPOSITE of bold—they're generic. Review ALL the DON'T guidelines in the frontend-design skill before proceeding. Bold means distinctive, not "more effects."
## Plan Amplification
Create a strategy to increase impact while maintaining coherence:
- **Focal point**: What should be the hero moment? (Pick ONE, make it amazing)
- **Personality direction**: Maximalist chaos? Elegant drama? Playful energy? Dark moody? Choose a lane.
- **Risk budget**: How experimental can we be? Push boundaries within constraints.
- **Hierarchy amplification**: Make big things BIGGER, small things smaller (increase contrast)
**IMPORTANT**: Bold design must still be usable. Impact without function is just decoration.
## Amplify the Design
Systematically increase impact across these dimensions:
### Typography Amplification
- **Replace generic fonts**: Swap system fonts for distinctive choices (see frontend-design skill for inspiration)
- **Extreme scale**: Create dramatic size jumps (3x-5x differences, not 1.5x)
- **Weight contrast**: Pair 900 weights with 200 weights, not 600 with 400
- **Unexpected choices**: Variable fonts, display fonts for headlines, condensed/extended widths, monospace as intentional accent (not as lazy "dev tool" default)
### Color Intensification
- **Increase saturation**: Shift to more vibrant, energetic colors (but not neon)
- **Bold palette**: Introduce unexpected color combinations—avoid the purple-blue gradient AI slop
- **Dominant color strategy**: Let one bold color own 60% of the design
- **Sharp accents**: High-contrast accent colors that pop
- **Tinted neutrals**: Replace pure grays with tinted grays that harmonize with your palette
- **Rich gradients**: Intentional multi-stop gradients (not generic purple-to-blue)
### Spatial Drama
- **Extreme scale jumps**: Make important elements 3-5x larger than surroundings
- **Break the grid**: Let hero elements escape containers and cross boundaries
- **Asymmetric layouts**: Replace centered, balanced layouts with tension-filled asymmetry
- **Generous space**: Use white space dramatically (100-200px gaps, not 20-40px)
- **Overlap**: Layer elements intentionally for depth
### Visual Effects
- **Dramatic shadows**: Large, soft shadows for elevation (but not generic drop shadows on rounded rectangles)
- **Background treatments**: Mesh patterns, noise textures, geometric patterns, intentional gradients (not purple-to-blue)
- **Texture & depth**: Grain, halftone, duotone, layered elements—NOT glassmorphism (it's overused AI slop)
- **Borders & frames**: Thick borders, decorative frames, custom shapes (not rounded rectangles with colored border on one side)
- **Custom elements**: Illustrative elements, custom icons, decorative details that reinforce brand
### Motion & Animation
- **Entrance choreography**: Staggered, dramatic page load animations with 50-100ms delays
- **Scroll effects**: Parallax, reveal animations, scroll-triggered sequences
- **Micro-interactions**: Satisfying hover effects, click feedback, state changes
- **Transitions**: Smooth, noticeable transitions using ease-out-quart/quint/expo (not bounce or elastic—they cheapen the effect)
### Composition Boldness
- **Hero moments**: Create clear focal points with dramatic treatment
- **Diagonal flows**: Escape horizontal/vertical rigidity with diagonal arrangements
- **Full-bleed elements**: Use full viewport width/height for impact
- **Unexpected proportions**: Golden ratio? Throw it out. Try 70/30, 80/20 splits
**NEVER**:
- Add effects randomly without purpose (chaos ≠ bold)
- Sacrifice readability for aesthetics (body text must be readable)
- Make everything bold (then nothing is bold - need contrast)
- Ignore accessibility (bold design must still meet WCAG standards)
- Overwhelm with motion (animation fatigue is real)
- Copy trendy aesthetics blindly (bold means distinctive, not derivative)
## Verify Quality
Ensure amplification maintains usability and coherence:
- **NOT AI slop**: Does this look like every other AI-generated "bold" design? If yes, start over.
- **Still functional**: Can users accomplish tasks without distraction?
- **Coherent**: Does everything feel intentional and unified?
- **Memorable**: Will users remember this experience?
- **Performant**: Do all these effects run smoothly?
- **Accessible**: Does it still meet accessibility standards?
**The test**: If you showed this to someone and said "AI made this bolder," would they believe you immediately? If yes, you've failed. Bold means distinctive, not "more AI effects."
Remember: Bold design is confident design. It takes risks, makes statements, and creates memorable experiences. But bold without strategy is just loud. Be intentional, be dramatic, be unforgettable.
-182
View File
@@ -1,182 +0,0 @@
---
name: clarify
description: Improve unclear UX copy, error messages, microcopy, labels, and instructions. Makes interfaces easier to understand and use.
user-invocable: true
argument-hint: [TARGET=<value>]
---
Identify and improve unclear, confusing, or poorly written interface text to make the product easier to understand and use.
## MANDATORY PREPARATION
Use the frontend-design skill — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run teach-impeccable first. Additionally gather: audience technical level and users' mental state in context.
---
## Assess Current Copy
Identify what makes the text unclear or ineffective:
1. **Find clarity problems**:
- **Jargon**: Technical terms users won't understand
- **Ambiguity**: Multiple interpretations possible
- **Passive voice**: "Your file has been uploaded" vs "We uploaded your file"
- **Length**: Too wordy or too terse
- **Assumptions**: Assuming user knowledge they don't have
- **Missing context**: Users don't know what to do or why
- **Tone mismatch**: Too formal, too casual, or inappropriate for situation
2. **Understand the context**:
- Who's the audience? (Technical? General? First-time users?)
- What's the user's mental state? (Stressed during error? Confident during success?)
- What's the action? (What do we want users to do?)
- What's the constraint? (Character limits? Space limitations?)
**CRITICAL**: Clear copy helps users succeed. Unclear copy creates frustration, errors, and support tickets.
## Plan Copy Improvements
Create a strategy for clearer communication:
- **Primary message**: What's the ONE thing users need to know?
- **Action needed**: What should users do next (if anything)?
- **Tone**: How should this feel? (Helpful? Apologetic? Encouraging?)
- **Constraints**: Length limits, brand voice, localization considerations
**IMPORTANT**: Good UX writing is invisible. Users should understand immediately without noticing the words.
## Improve Copy Systematically
Refine text across these common areas:
### Error Messages
**Bad**: "Error 403: Forbidden"
**Good**: "You don't have permission to view this page. Contact your admin for access."
**Bad**: "Invalid input"
**Good**: "Email addresses need an @ symbol. Try: name@example.com"
**Principles**:
- Explain what went wrong in plain language
- Suggest how to fix it
- Don't blame the user
- Include examples when helpful
- Link to help/support if applicable
### Form Labels & Instructions
**Bad**: "DOB (MM/DD/YYYY)"
**Good**: "Date of birth" (with placeholder showing format)
**Bad**: "Enter value here"
**Good**: "Your email address" or "Company name"
**Principles**:
- Use clear, specific labels (not generic placeholders)
- Show format expectations with examples
- Explain why you're asking (when not obvious)
- Put instructions before the field, not after
- Keep required field indicators clear
### Button & CTA Text
**Bad**: "Click here" | "Submit" | "OK"
**Good**: "Create account" | "Save changes" | "Got it, thanks"
**Principles**:
- Describe the action specifically
- Use active voice (verb + noun)
- Match user's mental model
- Be specific ("Save" is better than "OK")
### Help Text & Tooltips
**Bad**: "This is the username field"
**Good**: "Choose a username. You can change this later in Settings."
**Principles**:
- Add value (don't just repeat the label)
- Answer the implicit question ("What is this?" or "Why do you need this?")
- Keep it brief but complete
- Link to detailed docs if needed
### Empty States
**Bad**: "No items"
**Good**: "No projects yet. Create your first project to get started."
**Principles**:
- Explain why it's empty (if not obvious)
- Show next action clearly
- Make it welcoming, not dead-end
### Success Messages
**Bad**: "Success"
**Good**: "Settings saved! Your changes will take effect immediately."
**Principles**:
- Confirm what happened
- Explain what happens next (if relevant)
- Be brief but complete
- Match the user's emotional moment (celebrate big wins)
### Loading States
**Bad**: "Loading..." (for 30+ seconds)
**Good**: "Analyzing your data... this usually takes 30-60 seconds"
**Principles**:
- Set expectations (how long?)
- Explain what's happening (when it's not obvious)
- Show progress when possible
- Offer escape hatch if appropriate ("Cancel")
### Confirmation Dialogs
**Bad**: "Are you sure?"
**Good**: "Delete 'Project Alpha'? This can't be undone."
**Principles**:
- State the specific action
- Explain consequences (especially for destructive actions)
- Use clear button labels ("Delete project" not "Yes")
- Don't overuse confirmations (only for risky actions)
### Navigation & Wayfinding
**Bad**: Generic labels like "Items" | "Things" | "Stuff"
**Good**: Specific labels like "Your projects" | "Team members" | "Settings"
**Principles**:
- Be specific and descriptive
- Use language users understand (not internal jargon)
- Make hierarchy clear
- Consider information scent (breadcrumbs, current location)
## Apply Clarity Principles
Every piece of copy should follow these rules:
1. **Be specific**: "Enter email" not "Enter value"
2. **Be concise**: Cut unnecessary words (but don't sacrifice clarity)
3. **Be active**: "Save changes" not "Changes will be saved"
4. **Be human**: "Oops, something went wrong" not "System error encountered"
5. **Be helpful**: Tell users what to do, not just what happened
6. **Be consistent**: Use same terms throughout (don't vary for variety)
**NEVER**:
- Use jargon without explanation
- Blame users ("You made an error" → "This field is required")
- Be vague ("Something went wrong" without explanation)
- Use passive voice unnecessarily
- Write overly long explanations (be concise)
- Use humor for errors (be empathetic instead)
- Assume technical knowledge
- Vary terminology (pick one term and stick with it)
- Repeat information (headers restating intros, redundant explanations)
- Use placeholders as the only labels (they disappear when users type)
## Verify Improvements
Test that copy improvements work:
- **Comprehension**: Can users understand without context?
- **Actionability**: Do users know what to do next?
- **Brevity**: Is it as short as possible while remaining clear?
- **Consistency**: Does it match terminology elsewhere?
- **Tone**: Is it appropriate for the situation?
Remember: You're a clarity expert with excellent communication skills. Write like you're explaining to a smart friend who's unfamiliar with the product. Be clear, be helpful, be human.
-142
View File
@@ -1,142 +0,0 @@
---
name: colorize
description: Add strategic color to features that are too monochromatic or lack visual interest. Makes interfaces more engaging and expressive.
user-invocable: true
argument-hint: [TARGET=<value>]
---
Strategically introduce color to designs that are too monochromatic, gray, or lacking in visual warmth and personality.
## MANDATORY PREPARATION
Use the frontend-design skill — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run teach-impeccable first. Additionally gather: existing brand colors.
---
## Assess Color Opportunity
Analyze the current state and identify opportunities:
1. **Understand current state**:
- **Color absence**: Pure grayscale? Limited neutrals? One timid accent?
- **Missed opportunities**: Where could color add meaning, hierarchy, or delight?
- **Context**: What's appropriate for this domain and audience?
- **Brand**: Are there existing brand colors we should use?
2. **Identify where color adds value**:
- **Semantic meaning**: Success (green), error (red), warning (yellow/orange), info (blue)
- **Hierarchy**: Drawing attention to important elements
- **Categorization**: Different sections, types, or states
- **Emotional tone**: Warmth, energy, trust, creativity
- **Wayfinding**: Helping users navigate and understand structure
- **Delight**: Moments of visual interest and personality
If any of these are unclear from the codebase, ask the user directly to clarify what you cannot infer.
**CRITICAL**: More color ≠ better. Strategic color beats rainbow vomit every time. Every color should have a purpose.
## Plan Color Strategy
Create a purposeful color introduction plan:
- **Color palette**: What colors match the brand/context? (Choose 2-4 colors max beyond neutrals)
- **Dominant color**: Which color owns 60% of colored elements?
- **Accent colors**: Which colors provide contrast and highlights? (30% and 10%)
- **Application strategy**: Where does each color appear and why?
**IMPORTANT**: Color should enhance hierarchy and meaning, not create chaos. Less is more when it matters more.
## Introduce Color Strategically
Add color systematically across these dimensions:
### Semantic Color
- **State indicators**:
- Success: Green tones (emerald, forest, mint)
- Error: Red/pink tones (rose, crimson, coral)
- Warning: Orange/amber tones
- Info: Blue tones (sky, ocean, indigo)
- Neutral: Gray/slate for inactive states
- **Status badges**: Colored backgrounds or borders for states (active, pending, completed, etc.)
- **Progress indicators**: Colored bars, rings, or charts showing completion or health
### Accent Color Application
- **Primary actions**: Color the most important buttons/CTAs
- **Links**: Add color to clickable text (maintain accessibility)
- **Icons**: Colorize key icons for recognition and personality
- **Headers/titles**: Add color to section headers or key labels
- **Hover states**: Introduce color on interaction
### Background & Surfaces
- **Tinted backgrounds**: Replace pure gray (`#f5f5f5`) with warm neutrals (`oklch(97% 0.01 60)`) or cool tints (`oklch(97% 0.01 250)`)
- **Colored sections**: Use subtle background colors to separate areas
- **Gradient backgrounds**: Add depth with subtle, intentional gradients (not generic purple-blue)
- **Cards & surfaces**: Tint cards or surfaces slightly for warmth
**Use OKLCH for color**: It's perceptually uniform, meaning equal steps in lightness *look* equal. Great for generating harmonious scales.
### Data Visualization
- **Charts & graphs**: Use color to encode categories or values
- **Heatmaps**: Color intensity shows density or importance
- **Comparison**: Color coding for different datasets or timeframes
### Borders & Accents
- **Accent borders**: Add colored left/top borders to cards or sections
- **Underlines**: Color underlines for emphasis or active states
- **Dividers**: Subtle colored dividers instead of gray lines
- **Focus rings**: Colored focus indicators matching brand
### Typography Color
- **Colored headings**: Use brand colors for section headings (maintain contrast)
- **Highlight text**: Color for emphasis or categories
- **Labels & tags**: Small colored labels for metadata or categories
### Decorative Elements
- **Illustrations**: Add colored illustrations or icons
- **Shapes**: Geometric shapes in brand colors as background elements
- **Gradients**: Colorful gradient overlays or mesh backgrounds
- **Blobs/organic shapes**: Soft colored shapes for visual interest
## Balance & Refinement
Ensure color addition improves rather than overwhelms:
### Maintain Hierarchy
- **Dominant color** (60%): Primary brand color or most used accent
- **Secondary color** (30%): Supporting color for variety
- **Accent color** (10%): High contrast for key moments
- **Neutrals** (remaining): Gray/black/white for structure
### Accessibility
- **Contrast ratios**: Ensure WCAG compliance (4.5:1 for text, 3:1 for UI components)
- **Don't rely on color alone**: Use icons, labels, or patterns alongside color
- **Test for color blindness**: Verify red/green combinations work for all users
### Cohesion
- **Consistent palette**: Use colors from defined palette, not arbitrary choices
- **Systematic application**: Same color meanings throughout (green always = success)
- **Temperature consistency**: Warm palette stays warm, cool stays cool
**NEVER**:
- Use every color in the rainbow (choose 2-4 colors beyond neutrals)
- Apply color randomly without semantic meaning
- Put gray text on colored backgrounds—it looks washed out; use a darker shade of the background color or transparency instead
- Use pure gray for neutrals—add subtle color tint (warm or cool) for sophistication
- Use pure black (`#000`) or pure white (`#fff`) for large areas
- Violate WCAG contrast requirements
- Use color as the only indicator (accessibility issue)
- Make everything colorful (defeats the purpose)
- Default to purple-blue gradients (AI slop aesthetic)
## Verify Color Addition
Test that colorization improves the experience:
- **Better hierarchy**: Does color guide attention appropriately?
- **Clearer meaning**: Does color help users understand states/categories?
- **More engaging**: Does the interface feel warmer and more inviting?
- **Still accessible**: Do all color combinations meet WCAG standards?
- **Not overwhelming**: Is color balanced and purposeful?
Remember: Color is emotional and powerful. Use it to create warmth, guide attention, communicate meaning, and express personality. But restraint and strategy matter more than saturation and variety. Be colorful, but be intentional.
-119
View File
@@ -1,119 +0,0 @@
---
name: critique
description: Evaluate design effectiveness from a UX perspective. Assesses visual hierarchy, information architecture, emotional resonance, and overall design quality with actionable feedback.
user-invocable: true
argument-hint: [AREA=<value>]
---
## MANDATORY PREPARATION
Use the frontend-design skill — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run teach-impeccable first. Additionally gather: what the interface is trying to accomplish.
---
Conduct a holistic design critique, evaluating whether the interface actually works—not just technically, but as a designed experience. Think like a design director giving feedback.
## Design Critique
Evaluate the interface across these dimensions:
### 1. AI Slop Detection (CRITICAL)
**This is the most important check.** Does this look like every other AI-generated interface from 2024-2025?
Review the design against ALL the **DON'T** guidelines in the frontend-design skill—they are the fingerprints of AI-generated work. Check for the AI color palette, gradient text, dark mode with glowing accents, glassmorphism, hero metric layouts, identical card grids, generic fonts, and all other tells.
**The test**: If you showed this to someone and said "AI made this," would they believe you immediately? If yes, that's the problem.
### 2. Visual Hierarchy
- Does the eye flow to the most important element first?
- Is there a clear primary action? Can you spot it in 2 seconds?
- Do size, color, and position communicate importance correctly?
- Is there visual competition between elements that should have different weights?
### 3. Information Architecture
- Is the structure intuitive? Would a new user understand the organization?
- Is related content grouped logically?
- Are there too many choices at once? (cognitive overload)
- Is the navigation clear and predictable?
### 4. Emotional Resonance
- What emotion does this interface evoke? Is that intentional?
- Does it match the brand personality?
- Does it feel trustworthy, approachable, premium, playful—whatever it should feel?
- Would the target user feel "this is for me"?
### 5. Discoverability & Affordance
- Are interactive elements obviously interactive?
- Would a user know what to do without instructions?
- Are hover/focus states providing useful feedback?
- Are there hidden features that should be more visible?
### 6. Composition & Balance
- Does the layout feel balanced or uncomfortably weighted?
- Is whitespace used intentionally or just leftover?
- Is there visual rhythm in spacing and repetition?
- Does asymmetry feel designed or accidental?
### 7. Typography as Communication
- Does the type hierarchy clearly signal what to read first, second, third?
- Is body text comfortable to read? (line length, spacing, size)
- Do font choices reinforce the brand/tone?
- Is there enough contrast between heading levels?
### 8. Color with Purpose
- Is color used to communicate, not just decorate?
- Does the palette feel cohesive?
- Are accent colors drawing attention to the right things?
- Does it work for colorblind users? (not just technically—does meaning still come through?)
### 9. States & Edge Cases
- Empty states: Do they guide users toward action, or just say "nothing here"?
- Loading states: Do they reduce perceived wait time?
- Error states: Are they helpful and non-blaming?
- Success states: Do they confirm and guide next steps?
### 10. Microcopy & Voice
- Is the writing clear and concise?
- Does it sound like a human (the right human for this brand)?
- Are labels and buttons unambiguous?
- Does error copy help users fix the problem?
## Generate Critique Report
Structure your feedback as a design director would:
### Anti-Patterns Verdict
**Start here.** Pass/fail: Does this look AI-generated? List specific tells from the skill's Anti-Patterns section. Be brutally honest.
### Overall Impression
A brief gut reaction—what works, what doesn't, and the single biggest opportunity.
### What's Working
Highlight 2-3 things done well. Be specific about why they work.
### Priority Issues
The 3-5 most impactful design problems, ordered by importance:
For each issue:
- **What**: Name the problem clearly
- **Why it matters**: How this hurts users or undermines goals
- **Fix**: What to do about it (be concrete)
- **Command**: Which command to use (prefer: /animate, /quieter, /optimize, /adapt, /clarify, /distill, /delight, /onboard, /normalize, /audit, /harden, /polish, /extract, /bolder, /arrange, /typeset, /critique, /colorize, /overdrive — or other installed skills you're sure exist)
### Minor Observations
Quick notes on smaller issues worth addressing.
### Questions to Consider
Provocative questions that might unlock better solutions:
- "What if the primary action were more prominent?"
- "Does this need to feel this complex?"
- "What would a confident version of this look like?"
**Remember**:
- Be direct—vague feedback wastes everyone's time
- Be specific—"the submit button" not "some elements"
- Say what's wrong AND why it matters to users
- Give concrete suggestions, not just "consider exploring..."
- Prioritize ruthlessly—if everything is important, nothing is
- Don't soften criticism—developers need honest feedback to ship great design
-303
View File
@@ -1,303 +0,0 @@
---
name: delight
description: Add moments of joy, personality, and unexpected touches that make interfaces memorable and enjoyable to use. Elevates functional to delightful.
user-invocable: true
argument-hint: [TARGET=<value>]
---
Identify opportunities to add moments of joy, personality, and unexpected polish that transform functional interfaces into delightful experiences.
## MANDATORY PREPARATION
Use the frontend-design skill — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run teach-impeccable first. Additionally gather: what's appropriate for the domain (playful vs professional vs quirky vs elegant).
---
## Assess Delight Opportunities
Identify where delight would enhance (not distract from) the experience:
1. **Find natural delight moments**:
- **Success states**: Completed actions (save, send, publish)
- **Empty states**: First-time experiences, onboarding
- **Loading states**: Waiting periods that could be entertaining
- **Achievements**: Milestones, streaks, completions
- **Interactions**: Hover states, clicks, drags
- **Errors**: Softening frustrating moments
- **Easter eggs**: Hidden discoveries for curious users
2. **Understand the context**:
- What's the brand personality? (Playful? Professional? Quirky? Elegant?)
- Who's the audience? (Tech-savvy? Creative? Corporate?)
- What's the emotional context? (Accomplishment? Exploration? Frustration?)
- What's appropriate? (Banking app ≠ gaming app)
3. **Define delight strategy**:
- **Subtle sophistication**: Refined micro-interactions (luxury brands)
- **Playful personality**: Whimsical illustrations and copy (consumer apps)
- **Helpful surprises**: Anticipating needs before users ask (productivity tools)
- **Sensory richness**: Satisfying sounds, smooth animations (creative tools)
If any of these are unclear from the codebase, ask the user directly to clarify what you cannot infer.
**CRITICAL**: Delight should enhance usability, never obscure it. If users notice the delight more than accomplishing their goal, you've gone too far.
## Delight Principles
Follow these guidelines:
### Delight Amplifies, Never Blocks
- Delight moments should be quick (< 1 second)
- Never delay core functionality for delight
- Make delight skippable or subtle
- Respect user's time and task focus
### Surprise and Discovery
- Hide delightful details for users to discover
- Reward exploration and curiosity
- Don't announce every delight moment
- Let users share discoveries with others
### Appropriate to Context
- Match delight to emotional moment (celebrate success, empathize with errors)
- Respect the user's state (don't be playful during critical errors)
- Match brand personality and audience expectations
- Cultural sensitivity (what's delightful varies by culture)
### Compound Over Time
- Delight should remain fresh with repeated use
- Vary responses (not same animation every time)
- Reveal deeper layers with continued use
- Build anticipation through patterns
## Delight Techniques
Add personality and joy through these methods:
### Micro-interactions & Animation
**Button delight**:
```css
/* Satisfying button press */
.button {
transition: transform 0.1s, box-shadow 0.1s;
}
.button:active {
transform: translateY(2px);
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
/* Ripple effect on click */
/* Smooth lift on hover */
.button:hover {
transform: translateY(-2px);
transition: transform 0.2s cubic-bezier(0.25, 1, 0.5, 1); /* ease-out-quart */
}
```
**Loading delight**:
- Playful loading animations (not just spinners)
- Personality in loading messages (write product-specific ones, not generic AI filler)
- Progress indication with encouraging messages
- Skeleton screens with subtle animations
**Success animations**:
- Checkmark draw animation
- Confetti burst for major achievements
- Gentle scale + fade for confirmation
- Satisfying sound effects (subtle)
**Hover surprises**:
- Icons that animate on hover
- Color shifts or glow effects
- Tooltip reveals with personality
- Cursor changes (custom cursors for branded experiences)
### Personality in Copy
**Playful error messages**:
```
"Error 404"
"This page is playing hide and seek. (And winning)"
"Connection failed"
"Looks like the internet took a coffee break. Want to retry?"
```
**Encouraging empty states**:
```
"No projects"
"Your canvas awaits. Create something amazing."
"No messages"
"Inbox zero! You're crushing it today."
```
**Playful labels & tooltips**:
```
"Delete"
"Send to void" (for playful brand)
"Help"
"Rescue me" (tooltip)
```
**IMPORTANT**: Match copy personality to brand. Banks shouldn't be wacky, but they can be warm.
### Illustrations & Visual Personality
**Custom illustrations**:
- Empty state illustrations (not stock icons)
- Error state illustrations (friendly monsters, quirky characters)
- Loading state illustrations (animated characters)
- Success state illustrations (celebrations)
**Icon personality**:
- Custom icon set matching brand personality
- Animated icons (subtle motion on hover/click)
- Illustrative icons (more detailed than generic)
- Consistent style across all icons
**Background effects**:
- Subtle particle effects
- Gradient mesh backgrounds
- Geometric patterns
- Parallax depth
- Time-of-day themes (morning vs night)
### Satisfying Interactions
**Drag and drop delight**:
- Lift effect on drag (shadow, scale)
- Snap animation when dropped
- Satisfying placement sound
- Undo toast ("Dropped in wrong place? [Undo]")
**Toggle switches**:
- Smooth slide with spring physics
- Color transition
- Haptic feedback on mobile
- Optional sound effect
**Progress & achievements**:
- Streak counters with celebratory milestones
- Progress bars that "celebrate" at 100%
- Badge unlocks with animation
- Playful stats ("You're on fire! 5 days in a row")
**Form interactions**:
- Input fields that animate on focus
- Checkboxes with a satisfying scale pulse when checked
- Success state that celebrates valid input
- Auto-grow textareas
### Sound Design
**Subtle audio cues** (when appropriate):
- Notification sounds (distinctive but not annoying)
- Success sounds (satisfying "ding")
- Error sounds (empathetic, not harsh)
- Typing sounds for chat/messaging
- Ambient background audio (very subtle)
**IMPORTANT**:
- Respect system sound settings
- Provide mute option
- Keep volumes quiet (subtle cues, not alarms)
- Don't play on every interaction (sound fatigue is real)
### Easter Eggs & Hidden Delights
**Discovery rewards**:
- Konami code unlocks special theme
- Hidden keyboard shortcuts (Cmd+K for special features)
- Hover reveals on logos or illustrations
- Alt text jokes on images (for screen reader users too!)
- Console messages for developers ("Like what you see? We're hiring!")
**Seasonal touches**:
- Holiday themes (subtle, tasteful)
- Seasonal color shifts
- Weather-based variations
- Time-based changes (dark at night, light during day)
**Contextual personality**:
- Different messages based on time of day
- Responses to specific user actions
- Randomized variations (not same every time)
- Progressive reveals with continued use
### Loading & Waiting States
**Make waiting engaging**:
- Interesting loading messages that rotate
- Progress bars with personality
- Mini-games during long loads
- Fun facts or tips while waiting
- Countdown with encouraging messages
```
Loading messages — write ones specific to your product, not generic AI filler:
- "Crunching your latest numbers..."
- "Syncing with your team's changes..."
- "Preparing your dashboard..."
- "Checking for updates since yesterday..."
```
**WARNING**: Avoid cliched loading messages like "Herding pixels", "Teaching robots to dance", "Consulting the magic 8-ball", "Counting backwards from infinity". These are AI-slop copy — instantly recognizable as machine-generated. Write messages that are specific to what your product actually does.
### Celebration Moments
**Success celebrations**:
- Confetti for major milestones
- Animated checkmarks for completions
- Progress bar celebrations at 100%
- "Achievement unlocked" style notifications
- Personalized messages ("You published your 10th article!")
**Milestone recognition**:
- First-time actions get special treatment
- Streak tracking and celebration
- Progress toward goals
- Anniversary celebrations
## Implementation Patterns
**Animation libraries**:
- Framer Motion (React)
- GSAP (universal)
- Lottie (After Effects animations)
- Canvas confetti (party effects)
**Sound libraries**:
- Howler.js (audio management)
- Use-sound (React hook)
**Physics libraries**:
- React Spring (spring physics)
- Popmotion (animation primitives)
**IMPORTANT**: File size matters. Compress images, optimize animations, lazy load delight features.
**NEVER**:
- Delay core functionality for delight
- Force users through delightful moments (make skippable)
- Use delight to hide poor UX
- Overdo it (less is more)
- Ignore accessibility (animate responsibly, provide alternatives)
- Make every interaction delightful (special moments should be special)
- Sacrifice performance for delight
- Be inappropriate for context (read the room)
## Verify Delight Quality
Test that delight actually delights:
- **User reactions**: Do users smile? Share screenshots?
- **Doesn't annoy**: Still pleasant after 100th time?
- **Doesn't block**: Can users opt out or skip?
- **Performant**: No jank, no slowdown
- **Appropriate**: Matches brand and context
- **Accessible**: Works with reduced motion, screen readers
Remember: Delight is the difference between a tool and an experience. Add personality, surprise users positively, and create moments worth sharing. But always respect usability - delight should enhance, never obstruct.
-121
View File
@@ -1,121 +0,0 @@
---
name: distill
description: Strip designs to their essence by removing unnecessary complexity. Great design is simple, powerful, and clean.
user-invocable: true
argument-hint: [TARGET=<value>]
---
Remove unnecessary complexity from designs, revealing the essential elements and creating clarity through ruthless simplification.
## MANDATORY PREPARATION
Use the frontend-design skill — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run teach-impeccable first.
---
## Assess Current State
Analyze what makes the design feel complex or cluttered:
1. **Identify complexity sources**:
- **Too many elements**: Competing buttons, redundant information, visual clutter
- **Excessive variation**: Too many colors, fonts, sizes, styles without purpose
- **Information overload**: Everything visible at once, no progressive disclosure
- **Visual noise**: Unnecessary borders, shadows, backgrounds, decorations
- **Confusing hierarchy**: Unclear what matters most
- **Feature creep**: Too many options, actions, or paths forward
2. **Find the essence**:
- What's the primary user goal? (There should be ONE)
- What's actually necessary vs nice-to-have?
- What can be removed, hidden, or combined?
- What's the 20% that delivers 80% of value?
If any of these are unclear from the codebase, ask the user directly to clarify what you cannot infer.
**CRITICAL**: Simplicity is not about removing features - it's about removing obstacles between users and their goals. Every element should justify its existence.
## Plan Simplification
Create a ruthless editing strategy:
- **Core purpose**: What's the ONE thing this should accomplish?
- **Essential elements**: What's truly necessary to achieve that purpose?
- **Progressive disclosure**: What can be hidden until needed?
- **Consolidation opportunities**: What can be combined or integrated?
**IMPORTANT**: Simplification is hard. It requires saying no to good ideas to make room for great execution. Be ruthless.
## Simplify the Design
Systematically remove complexity across these dimensions:
### Information Architecture
- **Reduce scope**: Remove secondary actions, optional features, redundant information
- **Progressive disclosure**: Hide complexity behind clear entry points (accordions, modals, step-through flows)
- **Combine related actions**: Merge similar buttons, consolidate forms, group related content
- **Clear hierarchy**: ONE primary action, few secondary actions, everything else tertiary or hidden
- **Remove redundancy**: If it's said elsewhere, don't repeat it here
### Visual Simplification
- **Reduce color palette**: Use 1-2 colors plus neutrals, not 5-7 colors
- **Limit typography**: One font family, 3-4 sizes maximum, 2-3 weights
- **Remove decorations**: Eliminate borders, shadows, backgrounds that don't serve hierarchy or function
- **Flatten structure**: Reduce nesting, remove unnecessary containers—never nest cards inside cards
- **Remove unnecessary cards**: Cards aren't needed for basic layout; use spacing and alignment instead
- **Consistent spacing**: Use one spacing scale, remove arbitrary gaps
### Layout Simplification
- **Linear flow**: Replace complex grids with simple vertical flow where possible
- **Remove sidebars**: Move secondary content inline or hide it
- **Full-width**: Use available space generously instead of complex multi-column layouts
- **Consistent alignment**: Pick left or center, stick with it
- **Generous white space**: Let content breathe, don't pack everything tight
### Interaction Simplification
- **Reduce choices**: Fewer buttons, fewer options, clearer path forward (paradox of choice is real)
- **Smart defaults**: Make common choices automatic, only ask when necessary
- **Inline actions**: Replace modal flows with inline editing where possible
- **Remove steps**: Can signup be one step instead of three? Can checkout be simplified?
- **Clear CTAs**: ONE obvious next step, not five competing actions
### Content Simplification
- **Shorter copy**: Cut every sentence in half, then do it again
- **Active voice**: "Save changes" not "Changes will be saved"
- **Remove jargon**: Plain language always wins
- **Scannable structure**: Short paragraphs, bullet points, clear headings
- **Essential information only**: Remove marketing fluff, legalese, hedging
- **Remove redundant copy**: No headers restating intros, no repeated explanations, say it once
### Code Simplification
- **Remove unused code**: Dead CSS, unused components, orphaned files
- **Flatten component trees**: Reduce nesting depth
- **Consolidate styles**: Merge similar styles, use utilities consistently
- **Reduce variants**: Does that component need 12 variations, or can 3 cover 90% of cases?
**NEVER**:
- Remove necessary functionality (simplicity ≠ feature-less)
- Sacrifice accessibility for simplicity (clear labels and ARIA still required)
- Make things so simple they're unclear (mystery ≠ minimalism)
- Remove information users need to make decisions
- Eliminate hierarchy completely (some things should stand out)
- Oversimplify complex domains (match complexity to actual task complexity)
## Verify Simplification
Ensure simplification improves usability:
- **Faster task completion**: Can users accomplish goals more quickly?
- **Reduced cognitive load**: Is it easier to understand what to do?
- **Still complete**: Are all necessary features still accessible?
- **Clearer hierarchy**: Is it obvious what matters most?
- **Better performance**: Does simpler design load faster?
## Document Removed Complexity
If you removed features or options:
- Document why they were removed
- Consider if they need alternative access points
- Note any user feedback to monitor
Remember: You have great taste and judgment. Simplification is an act of confidence - knowing what to keep and courage to remove the rest. As Antoine de Saint-Exupéry said: "Perfection is achieved not when there is nothing more to add, but when there is nothing left to take away."
-91
View File
@@ -1,91 +0,0 @@
---
name: extract
description: Extract and consolidate reusable components, design tokens, and patterns into your design system. Identifies opportunities for systematic reuse and enriches your component library.
user-invocable: true
argument-hint: [TARGET=<value>]
---
Identify reusable patterns, components, and design tokens, then extract and consolidate them into the design system for systematic reuse.
## Discover
Analyze the target area to identify extraction opportunities:
1. **Find the design system**: Locate your design system, component library, or shared UI directory (grep for "design system", "ui", "components", etc.). Understand its structure:
- Component organization and naming conventions
- Design token structure (if any)
- Documentation patterns
- Import/export conventions
**CRITICAL**: If no design system exists, ask before creating one. Understand the preferred location and structure first.
2. **Identify patterns**: Look for:
- **Repeated components**: Similar UI patterns used multiple times (buttons, cards, inputs, etc.)
- **Hard-coded values**: Colors, spacing, typography, shadows that should be tokens
- **Inconsistent variations**: Multiple implementations of the same concept (3 different button styles)
- **Reusable patterns**: Layout patterns, composition patterns, interaction patterns worth systematizing
3. **Assess value**: Not everything should be extracted. Consider:
- Is this used 3+ times, or likely to be reused?
- Would systematizing this improve consistency?
- Is this a general pattern or context-specific?
- What's the maintenance cost vs benefit?
## Plan Extraction
Create a systematic extraction plan:
- **Components to extract**: Which UI elements become reusable components?
- **Tokens to create**: Which hard-coded values become design tokens?
- **Variants to support**: What variations does each component need?
- **Naming conventions**: Component names, token names, prop names that match existing patterns
- **Migration path**: How to refactor existing uses to consume the new shared versions
**IMPORTANT**: Design systems grow incrementally. Extract what's clearly reusable now, not everything that might someday be reusable.
## Extract & Enrich
Build improved, reusable versions:
- **Components**: Create well-designed components with:
- Clear props API with sensible defaults
- Proper variants for different use cases
- Accessibility built in (ARIA, keyboard navigation, focus management)
- Documentation and usage examples
- **Design tokens**: Create tokens with:
- Clear naming (primitive vs semantic)
- Proper hierarchy and organization
- Documentation of when to use each token
- **Patterns**: Document patterns with:
- When to use this pattern
- Code examples
- Variations and combinations
**NEVER**:
- Extract one-off, context-specific implementations without generalization
- Create components so generic they're useless
- Extract without considering existing design system conventions
- Skip proper TypeScript types or prop documentation
- Create tokens for every single value (tokens should have semantic meaning)
## Migrate
Replace existing uses with the new shared versions:
- **Find all instances**: Search for the patterns you've extracted
- **Replace systematically**: Update each use to consume the shared version
- **Test thoroughly**: Ensure visual and functional parity
- **Delete dead code**: Remove the old implementations
## Document
Update design system documentation:
- Add new components to the component library
- Document token usage and values
- Add examples and guidelines
- Update any Storybook or component catalog
Remember: A good design system is a living system. Extract patterns as they emerge, enrich them thoughtfully, and maintain them consistently.
-146
View File
@@ -1,146 +0,0 @@
---
name: frontend-design
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications. Generates creative, polished code that avoids generic AI aesthetics.
---
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
## Context Gathering Protocol
Design skills produce generic output without project context. You MUST have confirmed design context before doing any design work.
**Required context** — every design skill needs at minimum:
- **Target audience**: Who uses this product and in what context?
- **Use cases**: What jobs are they trying to get done?
- **Brand personality/tone**: How should the interface feel?
Individual skills may require additional context — check the skill's preparation section for specifics.
**CRITICAL**: You cannot infer this context by reading the codebase. Code tells you what was built, not who it's for or what it should feel like. Only the creator can provide this context.
**Gathering order:**
1. **Check current instructions (instant)**: If your loaded instructions already contain a **Design Context** section, proceed immediately.
2. **Check .impeccable.md (fast)**: If not in instructions, read `.impeccable.md` from the project root. If it exists and contains the required context, proceed.
3. **Run teach-impeccable (REQUIRED)**: If neither source has context, you MUST run the teach-impeccable skill NOW before doing anything else. Do NOT skip this step. Do NOT attempt to infer context from the codebase instead.
---
## Design Direction
Commit to a BOLD aesthetic direction:
- **Purpose**: What problem does this interface solve? Who uses it?
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
- **Constraints**: Technical requirements (framework, performance, accessibility).
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work—the key is intentionality, not intensity.
Then implement working code that is:
- Production-grade and functional
- Visually striking and memorable
- Cohesive with a clear aesthetic point-of-view
- Meticulously refined in every detail
## Frontend Aesthetics Guidelines
### Typography
*Consult [typography reference](reference/typography.md) for scales, pairing, and loading strategies.*
Choose fonts that are beautiful, unique, and interesting. Pair a distinctive display font with a refined body font.
**DO**: Use a modular type scale with fluid sizing (clamp)
**DO**: Vary font weights and sizes to create clear visual hierarchy
**DON'T**: Use overused fonts—Inter, Roboto, Arial, Open Sans, system defaults
**DON'T**: Use monospace typography as lazy shorthand for "technical/developer" vibes
**DON'T**: Put large icons with rounded corners above every heading—they rarely add value and make sites look templated
### Color & Theme
*Consult [color reference](reference/color-and-contrast.md) for OKLCH, palettes, and dark mode.*
Commit to a cohesive palette. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
**DO**: Use modern CSS color functions (oklch, color-mix, light-dark) for perceptually uniform, maintainable palettes
**DO**: Tint your neutrals toward your brand hue—even a subtle hint creates subconscious cohesion
**DON'T**: Use gray text on colored backgrounds—it looks washed out; use a shade of the background color instead
**DON'T**: Use pure black (#000) or pure white (#fff)—always tint; pure black/white never appears in nature
**DON'T**: Use the AI color palette: cyan-on-dark, purple-to-blue gradients, neon accents on dark backgrounds
**DON'T**: Use gradient text for "impact"—especially on metrics or headings; it's decorative rather than meaningful
**DON'T**: Default to dark mode with glowing accents—it looks "cool" without requiring actual design decisions
### Layout & Space
*Consult [spatial reference](reference/spatial-design.md) for grids, rhythm, and container queries.*
Create visual rhythm through varied spacing—not the same padding everywhere. Embrace asymmetry and unexpected compositions. Break the grid intentionally for emphasis.
**DO**: Create visual rhythm through varied spacing—tight groupings, generous separations
**DO**: Use fluid spacing with clamp() that breathes on larger screens
**DO**: Use asymmetry and unexpected compositions; break the grid intentionally for emphasis
**DON'T**: Wrap everything in cards—not everything needs a container
**DON'T**: Nest cards inside cards—visual noise, flatten the hierarchy
**DON'T**: Use identical card grids—same-sized cards with icon + heading + text, repeated endlessly
**DON'T**: Use the hero metric layout template—big number, small label, supporting stats, gradient accent
**DON'T**: Center everything—left-aligned text with asymmetric layouts feels more designed
**DON'T**: Use the same spacing everywhere—without rhythm, layouts feel monotonous
### Visual Details
**DO**: Use intentional, purposeful decorative elements that reinforce brand
**DON'T**: Use glassmorphism everywhere—blur effects, glass cards, glow borders used decoratively rather than purposefully
**DON'T**: Use rounded elements with thick colored border on one side—a lazy accent that almost never looks intentional
**DON'T**: Use sparklines as decoration—tiny charts that look sophisticated but convey nothing meaningful
**DON'T**: Use rounded rectangles with generic drop shadows—safe, forgettable, could be any AI output
**DON'T**: Use modals unless there's truly no better alternative—modals are lazy
### Motion
*Consult [motion reference](reference/motion-design.md) for timing, easing, and reduced motion.*
Focus on high-impact moments: one well-orchestrated page load with staggered reveals creates more delight than scattered micro-interactions.
**DO**: Use motion to convey state changes—entrances, exits, feedback
**DO**: Use exponential easing (ease-out-quart/quint/expo) for natural deceleration
**DO**: For height animations, use grid-template-rows transitions instead of animating height directly
**DON'T**: Animate layout properties (width, height, padding, margin)—use transform and opacity only
**DON'T**: Use bounce or elastic easing—they feel dated and tacky; real objects decelerate smoothly
### Interaction
*Consult [interaction reference](reference/interaction-design.md) for forms, focus, and loading patterns.*
Make interactions feel fast. Use optimistic UI—update immediately, sync later.
**DO**: Use progressive disclosure—start simple, reveal sophistication through interaction (basic options first, advanced behind expandable sections; hover states that reveal secondary actions)
**DO**: Design empty states that teach the interface, not just say "nothing here"
**DO**: Make every interactive surface feel intentional and responsive
**DON'T**: Repeat the same information—redundant headers, intros that restate the heading
**DON'T**: Make every button primary—use ghost buttons, text links, secondary styles; hierarchy matters
### Responsive
*Consult [responsive reference](reference/responsive-design.md) for mobile-first, fluid design, and container queries.*
**DO**: Use container queries (@container) for component-level responsiveness
**DO**: Adapt the interface for different contexts—don't just shrink it
**DON'T**: Hide critical functionality on mobile—adapt the interface, don't amputate it
### UX Writing
*Consult [ux-writing reference](reference/ux-writing.md) for labels, errors, and empty states.*
**DO**: Make every word earn its place
**DON'T**: Repeat information users can already see
---
## The AI Slop Test
**Critical quality check**: If you showed this interface to someone and said "AI made this," would they believe you immediately? If yes, that's the problem.
A distinctive interface should make someone ask "how was this made?" not "which AI made this?"
Review the DON'T guidelines above—they are the fingerprints of AI-generated work from 2024-2025.
---
## Implementation Principles
Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details.
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices across generations.
Remember: the model is capable of extraordinary creative work. Don't hold back—show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
@@ -1,132 +0,0 @@
# Color & Contrast
## Color Spaces: Use OKLCH
**Stop using HSL.** Use OKLCH (or LCH) instead. It's perceptually uniform, meaning equal steps in lightness *look* equal—unlike HSL where 50% lightness in yellow looks bright while 50% in blue looks dark.
```css
/* OKLCH: lightness (0-100%), chroma (0-0.4+), hue (0-360) */
--color-primary: oklch(60% 0.15 250); /* Blue */
--color-primary-light: oklch(85% 0.08 250); /* Same hue, lighter */
--color-primary-dark: oklch(35% 0.12 250); /* Same hue, darker */
```
**Key insight**: As you move toward white or black, reduce chroma (saturation). High chroma at extreme lightness looks garish. A light blue at 85% lightness needs ~0.08 chroma, not the 0.15 of your base color.
## Building Functional Palettes
### The Tinted Neutral Trap
**Pure gray is dead.** Add a subtle hint of your brand hue to all neutrals:
```css
/* Dead grays */
--gray-100: oklch(95% 0 0); /* No personality */
--gray-900: oklch(15% 0 0);
/* Warm-tinted grays (add brand warmth) */
--gray-100: oklch(95% 0.01 60); /* Hint of warmth */
--gray-900: oklch(15% 0.01 60);
/* Cool-tinted grays (tech, professional) */
--gray-100: oklch(95% 0.01 250); /* Hint of blue */
--gray-900: oklch(15% 0.01 250);
```
The chroma is tiny (0.01) but perceptible. It creates subconscious cohesion between your brand color and your UI.
### Palette Structure
A complete system needs:
| Role | Purpose | Example |
|------|---------|---------|
| **Primary** | Brand, CTAs, key actions | 1 color, 3-5 shades |
| **Neutral** | Text, backgrounds, borders | 9-11 shade scale |
| **Semantic** | Success, error, warning, info | 4 colors, 2-3 shades each |
| **Surface** | Cards, modals, overlays | 2-3 elevation levels |
**Skip secondary/tertiary unless you need them.** Most apps work fine with one accent color. Adding more creates decision fatigue and visual noise.
### The 60-30-10 Rule (Applied Correctly)
This rule is about **visual weight**, not pixel count:
- **60%**: Neutral backgrounds, white space, base surfaces
- **30%**: Secondary colors—text, borders, inactive states
- **10%**: Accent—CTAs, highlights, focus states
The common mistake: using the accent color everywhere because it's "the brand color." Accent colors work *because* they're rare. Overuse kills their power.
## Contrast & Accessibility
### WCAG Requirements
| Content Type | AA Minimum | AAA Target |
|--------------|------------|------------|
| Body text | 4.5:1 | 7:1 |
| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 |
| UI components, icons | 3:1 | 4.5:1 |
| Non-essential decorations | None | None |
**The gotcha**: Placeholder text still needs 4.5:1. That light gray placeholder you see everywhere? Usually fails WCAG.
### Dangerous Color Combinations
These commonly fail contrast or cause readability issues:
- Light gray text on white (the #1 accessibility fail)
- **Gray text on any colored background**—gray looks washed out and dead on color. Use a darker shade of the background color, or transparency
- Red text on green background (or vice versa)—8% of men can't distinguish these
- Blue text on red background (vibrates visually)
- Yellow text on white (almost always fails)
- Thin light text on images (unpredictable contrast)
### Never Use Pure Gray or Pure Black
Pure gray (`oklch(50% 0 0)`) and pure black (`#000`) don't exist in nature—real shadows and surfaces always have a color cast. Even a chroma of 0.005-0.01 is enough to feel natural without being obviously tinted. (See tinted neutrals example above.)
### Testing
Don't trust your eyes. Use tools:
- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
- Browser DevTools → Rendering → Emulate vision deficiencies
- [Polypane](https://polypane.app/) for real-time testing
## Theming: Light & Dark Mode
### Dark Mode Is Not Inverted Light Mode
You can't just swap colors. Dark mode requires different design decisions:
| Light Mode | Dark Mode |
|------------|-----------|
| Shadows for depth | Lighter surfaces for depth (no shadows) |
| Dark text on light | Light text on dark (reduce font weight) |
| Vibrant accents | Desaturate accents slightly |
| White backgrounds | Never pure black—use dark gray (oklch 12-18%) |
```css
/* Dark mode depth via surface color, not shadow */
:root[data-theme="dark"] {
--surface-1: oklch(15% 0.01 250);
--surface-2: oklch(20% 0.01 250); /* "Higher" = lighter */
--surface-3: oklch(25% 0.01 250);
/* Reduce text weight slightly */
--body-weight: 350; /* Instead of 400 */
}
```
### Token Hierarchy
Use two layers: primitive tokens (`--blue-500`) and semantic tokens (`--color-primary: var(--blue-500)`). For dark mode, only redefine the semantic layer—primitives stay the same.
## Alpha Is A Design Smell
Heavy use of transparency (rgba, hsla) usually means an incomplete palette. Alpha creates unpredictable contrast, performance overhead, and inconsistency. Define explicit overlay colors for each context instead. Exception: focus rings and interactive states where see-through is needed.
---
**Avoid**: Relying on color alone to convey information. Creating palettes without clear roles for each color. Using pure black (#000) for large areas. Skipping color blindness testing (8% of men affected).
@@ -1,123 +0,0 @@
# Interaction Design
## The Eight Interactive States
Every interactive element needs these states designed:
| State | When | Visual Treatment |
|-------|------|------------------|
| **Default** | At rest | Base styling |
| **Hover** | Pointer over (not touch) | Subtle lift, color shift |
| **Focus** | Keyboard/programmatic focus | Visible ring (see below) |
| **Active** | Being pressed | Pressed in, darker |
| **Disabled** | Not interactive | Reduced opacity, no pointer |
| **Loading** | Processing | Spinner, skeleton |
| **Error** | Invalid state | Red border, icon, message |
| **Success** | Completed | Green check, confirmation |
**The common miss**: Designing hover without focus, or vice versa. They're different. Keyboard users never see hover states.
## Focus Rings: Do Them Right
**Never `outline: none` without replacement.** It's an accessibility violation. Instead, use `:focus-visible` to show focus only for keyboard users:
```css
/* Hide focus ring for mouse/touch */
button:focus {
outline: none;
}
/* Show focus ring for keyboard */
button:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
```
**Focus ring design**:
- High contrast (3:1 minimum against adjacent colors)
- 2-3px thick
- Offset from element (not inside it)
- Consistent across all interactive elements
## Form Design: The Non-Obvious
**Placeholders aren't labels**—they disappear on input. Always use visible `<label>` elements. **Validate on blur**, not on every keystroke (exception: password strength). Place errors **below** fields with `aria-describedby` connecting them.
## Loading States
**Optimistic updates**: Show success immediately, rollback on failure. Use for low-stakes actions (likes, follows), not payments or destructive actions. **Skeleton screens > spinners**—they preview content shape and feel faster than generic spinners.
## Modals: The Inert Approach
Focus trapping in modals used to require complex JavaScript. Now use the `inert` attribute:
```html
<!-- When modal is open -->
<main inert>
<!-- Content behind modal can't be focused or clicked -->
</main>
<dialog open>
<h2>Modal Title</h2>
<!-- Focus stays inside modal -->
</dialog>
```
Or use the native `<dialog>` element:
```javascript
const dialog = document.querySelector('dialog');
dialog.showModal(); // Opens with focus trap, closes on Escape
```
## The Popover API
For tooltips, dropdowns, and non-modal overlays, use native popovers:
```html
<button popovertarget="menu">Open menu</button>
<div id="menu" popover>
<button>Option 1</button>
<button>Option 2</button>
</div>
```
**Benefits**: Light-dismiss (click outside closes), proper stacking, no z-index wars, accessible by default.
## Destructive Actions: Undo > Confirm
**Undo is better than confirmation dialogs**—users click through confirmations mindlessly. Remove from UI immediately, show undo toast, actually delete after toast expires. Use confirmation only for truly irreversible actions (account deletion), high-cost actions, or batch operations.
## Keyboard Navigation Patterns
### Roving Tabindex
For component groups (tabs, menu items, radio groups), one item is tabbable; arrow keys move within:
```html
<div role="tablist">
<button role="tab" tabindex="0">Tab 1</button>
<button role="tab" tabindex="-1">Tab 2</button>
<button role="tab" tabindex="-1">Tab 3</button>
</div>
```
Arrow keys move `tabindex="0"` between items. Tab moves to the next component entirely.
### Skip Links
Provide skip links (`<a href="#main-content">Skip to main content</a>`) for keyboard users to jump past navigation. Hide off-screen, show on focus.
## Gesture Discoverability
Swipe-to-delete and similar gestures are invisible. Hint at their existence:
- **Partially reveal**: Show delete button peeking from edge
- **Onboarding**: Coach marks on first use
- **Alternative**: Always provide a visible fallback (menu with "Delete")
Don't rely on gestures as the only way to perform actions.
---
**Avoid**: Removing focus indicators without alternatives. Using placeholder text as labels. Touch targets <44x44px. Generic error messages. Custom controls without ARIA/keyboard support.
@@ -1,99 +0,0 @@
# Motion Design
## Duration: The 100/300/500 Rule
Timing matters more than easing. These durations feel right for most UI:
| Duration | Use Case | Examples |
|----------|----------|----------|
| **100-150ms** | Instant feedback | Button press, toggle, color change |
| **200-300ms** | State changes | Menu open, tooltip, hover states |
| **300-500ms** | Layout changes | Accordion, modal, drawer |
| **500-800ms** | Entrance animations | Page load, hero reveals |
**Exit animations are faster than entrances**—use ~75% of enter duration.
## Easing: Pick the Right Curve
**Don't use `ease`.** It's a compromise that's rarely optimal. Instead:
| Curve | Use For | CSS |
|-------|---------|-----|
| **ease-out** | Elements entering | `cubic-bezier(0.16, 1, 0.3, 1)` |
| **ease-in** | Elements leaving | `cubic-bezier(0.7, 0, 0.84, 0)` |
| **ease-in-out** | State toggles (there → back) | `cubic-bezier(0.65, 0, 0.35, 1)` |
**For micro-interactions, use exponential curves**—they feel natural because they mimic real physics (friction, deceleration):
```css
/* Quart out - smooth, refined (recommended default) */
--ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1);
/* Quint out - slightly more dramatic */
--ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1);
/* Expo out - snappy, confident */
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
```
**Avoid bounce and elastic curves.** They were trendy in 2015 but now feel tacky and amateurish. Real objects don't bounce when they stop—they decelerate smoothly. Overshoot effects draw attention to the animation itself rather than the content.
## The Only Two Properties You Should Animate
**transform** and **opacity** only—everything else causes layout recalculation. For height animations (accordions), use `grid-template-rows: 0fr → 1fr` instead of animating `height` directly.
## Staggered Animations
Use CSS custom properties for cleaner stagger: `animation-delay: calc(var(--i, 0) * 50ms)` with `style="--i: 0"` on each item. **Cap total stagger time**—10 items at 50ms = 500ms total. For many items, reduce per-item delay or cap staggered count.
## Reduced Motion
This is not optional. Vestibular disorders affect ~35% of adults over 40.
```css
/* Define animations normally */
.card {
animation: slide-up 500ms ease-out;
}
/* Provide alternative for reduced motion */
@media (prefers-reduced-motion: reduce) {
.card {
animation: fade-in 200ms ease-out; /* Crossfade instead of motion */
}
}
/* Or disable entirely */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
```
**What to preserve**: Functional animations like progress bars, loading spinners (slowed down), and focus indicators should still work—just without spatial movement.
## Perceived Performance
**Nobody cares how fast your site is—just how fast it feels.** Perception can be as effective as actual performance.
**The 80ms threshold**: Our brains buffer sensory input for ~80ms to synchronize perception. Anything under 80ms feels instant and simultaneous. This is your target for micro-interactions.
**Active vs passive time**: Passive waiting (staring at a spinner) feels longer than active engagement. Strategies to shift the balance:
- **Preemptive start**: Begin transitions immediately while loading (iOS app zoom, skeleton UI). Users perceive work happening.
- **Early completion**: Show content progressively—don't wait for everything. Video buffering, progressive images, streaming HTML.
- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Instagram likes work offline—the UI updates instantly, syncs later. Use for low-stakes actions; avoid for payments or destructive operations.
**Easing affects perceived duration**: Ease-in (accelerating toward completion) makes tasks feel shorter because the peak-end effect weights final moments heavily. Ease-out feels satisfying for entrances, but ease-in toward a task's end compresses perceived time.
**Caution**: Too-fast responses can decrease perceived value. Users may distrust instant results for complex operations (search, analysis). Sometimes a brief delay signals "real work" is happening.
## Performance
Don't use `will-change` preemptively—only when animation is imminent (`:hover`, `.animating`). For scroll-triggered animations, use Intersection Observer instead of scroll events; unobserve after animating once. Create motion tokens for consistency (durations, easings, common transitions).
---
**Avoid**: Animating everything (animation fatigue is real). Using >500ms for UI feedback. Ignoring `prefers-reduced-motion`. Using animation to hide slow loading.
@@ -1,114 +0,0 @@
# Responsive Design
## Mobile-First: Write It Right
Start with base styles for mobile, use `min-width` queries to layer complexity. Desktop-first (`max-width`) means mobile loads unnecessary styles first.
## Breakpoints: Content-Driven
Don't chase device sizes—let content tell you where to break. Start narrow, stretch until design breaks, add breakpoint there. Three breakpoints usually suffice (640, 768, 1024px). Use `clamp()` for fluid values without breakpoints.
## Detect Input Method, Not Just Screen Size
**Screen size doesn't tell you input method.** A laptop with touchscreen, a tablet with keyboard—use pointer and hover queries:
```css
/* Fine pointer (mouse, trackpad) */
@media (pointer: fine) {
.button { padding: 8px 16px; }
}
/* Coarse pointer (touch, stylus) */
@media (pointer: coarse) {
.button { padding: 12px 20px; } /* Larger touch target */
}
/* Device supports hover */
@media (hover: hover) {
.card:hover { transform: translateY(-2px); }
}
/* Device doesn't support hover (touch) */
@media (hover: none) {
.card { /* No hover state - use active instead */ }
}
```
**Critical**: Don't rely on hover for functionality. Touch users can't hover.
## Safe Areas: Handle the Notch
Modern phones have notches, rounded corners, and home indicators. Use `env()`:
```css
body {
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}
/* With fallback */
.footer {
padding-bottom: max(1rem, env(safe-area-inset-bottom));
}
```
**Enable viewport-fit** in your meta tag:
```html
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
```
## Responsive Images: Get It Right
### srcset with Width Descriptors
```html
<img
src="hero-800.jpg"
srcset="
hero-400.jpg 400w,
hero-800.jpg 800w,
hero-1200.jpg 1200w
"
sizes="(max-width: 768px) 100vw, 50vw"
alt="Hero image"
>
```
**How it works**:
- `srcset` lists available images with their actual widths (`w` descriptors)
- `sizes` tells the browser how wide the image will display
- Browser picks the best file based on viewport width AND device pixel ratio
### Picture Element for Art Direction
When you need different crops/compositions (not just resolutions):
```html
<picture>
<source media="(min-width: 768px)" srcset="wide.jpg">
<source media="(max-width: 767px)" srcset="tall.jpg">
<img src="fallback.jpg" alt="...">
</picture>
```
## Layout Adaptation Patterns
**Navigation**: Three stages—hamburger + drawer on mobile, horizontal compact on tablet, full with labels on desktop. **Tables**: Transform to cards on mobile using `display: block` and `data-label` attributes. **Progressive disclosure**: Use `<details>/<summary>` for content that can collapse on mobile.
## Testing: Don't Trust DevTools Alone
DevTools device emulation is useful for layout but misses:
- Actual touch interactions
- Real CPU/memory constraints
- Network latency patterns
- Font rendering differences
- Browser chrome/keyboard appearances
**Test on at least**: One real iPhone, one real Android, a tablet if relevant. Cheap Android phones reveal performance issues you'll never see on simulators.
---
**Avoid**: Desktop-first design. Device detection instead of feature detection. Separate mobile/desktop codebases. Ignoring tablet and landscape. Assuming all mobile devices are powerful.
@@ -1,100 +0,0 @@
# Spatial Design
## Spacing Systems
### Use 4pt Base, Not 8pt
8pt systems are too coarse—you'll frequently need 12px (between 8 and 16). Use 4pt for granularity: 4, 8, 12, 16, 24, 32, 48, 64, 96px.
### Name Tokens Semantically
Name by relationship (`--space-sm`, `--space-lg`), not value (`--spacing-8`). Use `gap` instead of margins for sibling spacing—it eliminates margin collapse and cleanup hacks.
## Grid Systems
### The Self-Adjusting Grid
Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. Columns are at least 280px, as many as fit per row, leftovers stretch. For complex layouts, use named grid areas (`grid-template-areas`) and redefine them at breakpoints.
## Visual Hierarchy
### The Squint Test
Blur your eyes (or screenshot and blur). Can you still identify:
- The most important element?
- The second most important?
- Clear groupings?
If everything looks the same weight blurred, you have a hierarchy problem.
### Hierarchy Through Multiple Dimensions
Don't rely on size alone. Combine:
| Tool | Strong Hierarchy | Weak Hierarchy |
|------|------------------|----------------|
| **Size** | 3:1 ratio or more | <2:1 ratio |
| **Weight** | Bold vs Regular | Medium vs Regular |
| **Color** | High contrast | Similar tones |
| **Position** | Top/left (primary) | Bottom/right |
| **Space** | Surrounded by white space | Crowded |
**The best hierarchy uses 2-3 dimensions at once**: A heading that's larger, bolder, AND has more space above it.
### Cards Are Not Required
Cards are overused. Spacing and alignment create visual grouping naturally. Use cards only when content is truly distinct and actionable, items need visual comparison in a grid, or content needs clear interaction boundaries. **Never nest cards inside cards**—use spacing, typography, and subtle dividers for hierarchy within a card.
## Container Queries
Viewport queries are for page layouts. **Container queries are for components**:
```css
.card-container {
container-type: inline-size;
}
.card {
display: grid;
gap: var(--space-md);
}
/* Card layout changes based on its container, not viewport */
@container (min-width: 400px) {
.card {
grid-template-columns: 120px 1fr;
}
}
```
**Why this matters**: A card in a narrow sidebar stays compact, while the same card in a main content area expands—automatically, without viewport hacks.
## Optical Adjustments
Text at `margin-left: 0` looks indented due to letterform whitespace—use negative margin (`-0.05em`) to optically align. Geometrically centered icons often look off-center; play icons need to shift right, arrows shift toward their direction.
### Touch Targets vs Visual Size
Buttons can look small but need large touch targets (44px minimum). Use padding or pseudo-elements:
```css
.icon-button {
width: 24px; /* Visual size */
height: 24px;
position: relative;
}
.icon-button::before {
content: '';
position: absolute;
inset: -10px; /* Expand tap target to 44px */
}
```
## Depth & Elevation
Create semantic z-index scales (dropdown → sticky → modal-backdrop → modal → toast → tooltip) instead of arbitrary numbers. For shadows, create a consistent elevation scale (sm → md → lg → xl). **Key insight**: Shadows should be subtle—if you can clearly see it, it's probably too strong.
---
**Avoid**: Arbitrary spacing values outside your scale. Making all spacing equal (variety creates hierarchy). Creating hierarchy through size alone - combine size, weight, color, and space.
@@ -1,133 +0,0 @@
# Typography
## Classic Typography Principles
### Vertical Rhythm
Your line-height should be the base unit for ALL vertical spacing. If body text has `line-height: 1.5` on `16px` type (= 24px), spacing values should be multiples of 24px. This creates subconscious harmony—text and space share a mathematical foundation.
### Modular Scale & Hierarchy
The common mistake: too many font sizes that are too close together (14px, 15px, 16px, 18px...). This creates muddy hierarchy.
**Use fewer sizes with more contrast.** A 5-size system covers most needs:
| Role | Typical Ratio | Use Case |
|------|---------------|----------|
| xs | 0.75rem | Captions, legal |
| sm | 0.875rem | Secondary UI, metadata |
| base | 1rem | Body text |
| lg | 1.25-1.5rem | Subheadings, lead text |
| xl+ | 2-4rem | Headlines, hero text |
Popular ratios: 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth). Pick one and commit.
### Readability & Measure
Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scales inversely with line length—narrow columns need tighter leading, wide columns need more.
**Non-obvious**: Increase line-height for light text on dark backgrounds. The perceived weight is lighter, so text needs more breathing room. Add 0.05-0.1 to your normal line-height.
## Font Selection & Pairing
### Choosing Distinctive Fonts
**Avoid the invisible defaults**: Inter, Roboto, Open Sans, Lato, Montserrat. These are everywhere, making your design feel generic. They're fine for documentation or tools where personality isn't the goal—but if you want distinctive design, look elsewhere.
**Better Google Fonts alternatives**:
- Instead of Inter → **Instrument Sans**, **Plus Jakarta Sans**, **Outfit**
- Instead of Roboto → **Onest**, **Figtree**, **Urbanist**
- Instead of Open Sans → **Source Sans 3**, **Nunito Sans**, **DM Sans**
- For editorial/premium feel → **Fraunces**, **Newsreader**, **Lora**
**System fonts are underrated**: `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui` looks native, loads instantly, and is highly readable. Consider this for apps where performance > personality.
### Pairing Principles
**The non-obvious truth**: You often don't need a second font. One well-chosen font family in multiple weights creates cleaner hierarchy than two competing typefaces. Only add a second font when you need genuine contrast (e.g., display headlines + body serif).
When pairing, contrast on multiple axes:
- Serif + Sans (structure contrast)
- Geometric + Humanist (personality contrast)
- Condensed display + Wide body (proportion contrast)
**Never pair fonts that are similar but not identical** (e.g., two geometric sans-serifs). They create visual tension without clear hierarchy.
### Web Font Loading
The layout shift problem: fonts load late, text reflows, and users see content jump. Here's the fix:
```css
/* 1. Use font-display: swap for visibility */
@font-face {
font-family: 'CustomFont';
src: url('font.woff2') format('woff2');
font-display: swap;
}
/* 2. Match fallback metrics to minimize shift */
@font-face {
font-family: 'CustomFont-Fallback';
src: local('Arial');
size-adjust: 105%; /* Scale to match x-height */
ascent-override: 90%; /* Match ascender height */
descent-override: 20%; /* Match descender depth */
line-gap-override: 10%; /* Match line spacing */
}
body {
font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif;
}
```
Tools like [Fontaine](https://github.com/unjs/fontaine) calculate these overrides automatically.
## Modern Web Typography
### Fluid Type
Fluid typography via `clamp(min, preferred, max)` scales text smoothly with the viewport. The middle value (e.g., `5vw + 1rem`) controls scaling rate—higher vw = faster scaling. Add a rem offset so it doesn't collapse to 0 on small screens.
**Use fluid type for**: Headings and display text on marketing/content pages where text dominates the layout and needs to breathe across viewport sizes.
**Use fixed `rem` scales for**: App UIs, dashboards, and data-dense interfaces. No major app design system (Material, Polaris, Primer, Carbon) uses fluid type in product UI — fixed scales with optional breakpoint adjustments give the spatial predictability that container-based layouts need. Body text should also be fixed even on marketing pages, since the size difference across viewports is too small to warrant it.
### OpenType Features
Most developers don't know these exist. Use them for polish:
```css
/* Tabular numbers for data alignment */
.data-table { font-variant-numeric: tabular-nums; }
/* Proper fractions */
.recipe-amount { font-variant-numeric: diagonal-fractions; }
/* Small caps for abbreviations */
abbr { font-variant-caps: all-small-caps; }
/* Disable ligatures in code */
code { font-variant-ligatures: none; }
/* Enable kerning (usually on by default, but be explicit) */
body { font-kerning: normal; }
```
Check what features your font supports at [Wakamai Fondue](https://wakamaifondue.com/).
## Typography System Architecture
Name tokens semantically (`--text-body`, `--text-heading`), not by value (`--font-size-16`). Include font stacks, size scale, weights, line-heights, and letter-spacing in your token system.
## Accessibility Considerations
Beyond contrast ratios (which are well-documented), consider:
- **Never disable zoom**: `user-scalable=no` breaks accessibility. If your layout breaks at 200% zoom, fix the layout.
- **Use rem/em for font sizes**: This respects user browser settings. Never `px` for body text.
- **Minimum 16px body text**: Smaller than this strains eyes and fails WCAG on mobile.
- **Adequate touch targets**: Text links need padding or line-height that creates 44px+ tap targets.
---
**Avoid**: More than 2-3 font families per project. Skipping fallback font definitions. Ignoring font loading performance (FOUT/FOIT). Using decorative fonts for body text.
@@ -1,107 +0,0 @@
# UX Writing
## The Button Label Problem
**Never use "OK", "Submit", or "Yes/No".** These are lazy and ambiguous. Use specific verb + object patterns:
| Bad | Good | Why |
|-----|------|-----|
| OK | Save changes | Says what will happen |
| Submit | Create account | Outcome-focused |
| Yes | Delete message | Confirms the action |
| Cancel | Keep editing | Clarifies what "cancel" means |
| Click here | Download PDF | Describes the destination |
**For destructive actions**, name the destruction:
- "Delete" not "Remove" (delete is permanent, remove implies recoverable)
- "Delete 5 items" not "Delete selected" (show the count)
## Error Messages: The Formula
Every error message should answer: (1) What happened? (2) Why? (3) How to fix it? Example: "Email address isn't valid. Please include an @ symbol." not "Invalid input".
### Error Message Templates
| Situation | Template |
|-----------|----------|
| **Format error** | "[Field] needs to be [format]. Example: [example]" |
| **Missing required** | "Please enter [what's missing]" |
| **Permission denied** | "You don't have access to [thing]. [What to do instead]" |
| **Network error** | "We couldn't reach [thing]. Check your connection and [action]." |
| **Server error** | "Something went wrong on our end. We're looking into it. [Alternative action]" |
### Don't Blame the User
Reframe errors: "Please enter a date in MM/DD/YYYY format" not "You entered an invalid date".
## Empty States Are Opportunities
Empty states are onboarding moments: (1) Acknowledge briefly, (2) Explain the value of filling it, (3) Provide a clear action. "No projects yet. Create your first one to get started." not just "No items".
## Voice vs Tone
**Voice** is your brand's personality—consistent everywhere.
**Tone** adapts to the moment.
| Moment | Tone Shift |
|--------|------------|
| Success | Celebratory, brief: "Done! Your changes are live." |
| Error | Empathetic, helpful: "That didn't work. Here's what to try..." |
| Loading | Reassuring: "Saving your work..." |
| Destructive confirm | Serious, clear: "Delete this project? This can't be undone." |
**Never use humor for errors.** Users are already frustrated. Be helpful, not cute.
## Writing for Accessibility
**Link text** must have standalone meaning—"View pricing plans" not "Click here". **Alt text** describes information, not the image—"Revenue increased 40% in Q4" not "Chart". Use `alt=""` for decorative images. **Icon buttons** need `aria-label` for screen reader context.
## Writing for Translation
### Plan for Expansion
German text is ~30% longer than English. Allocate space:
| Language | Expansion |
|----------|-----------|
| German | +30% |
| French | +20% |
| Finnish | +30-40% |
| Chinese | -30% (fewer chars, but same width) |
### Translation-Friendly Patterns
Keep numbers separate ("New messages: 3" not "You have 3 new messages"). Use full sentences as single strings (word order varies by language). Avoid abbreviations ("5 minutes ago" not "5 mins ago"). Give translators context about where strings appear.
## Consistency: The Terminology Problem
Pick one term and stick with it:
| Inconsistent | Consistent |
|--------------|------------|
| Delete / Remove / Trash | Delete |
| Settings / Preferences / Options | Settings |
| Sign in / Log in / Enter | Sign in |
| Create / Add / New | Create |
Build a terminology glossary and enforce it. Variety creates confusion.
## Avoid Redundant Copy
If the heading explains it, the intro is redundant. If the button is clear, don't explain it again. Say it once, say it well.
## Loading States
Be specific: "Saving your draft..." not "Loading...". For long waits, set expectations ("This usually takes 30 seconds") or show progress.
## Confirmation Dialogs: Use Sparingly
Most confirmation dialogs are design failures—consider undo instead. When you must confirm: name the action, explain consequences, use specific button labels ("Delete project" / "Keep project", not "Yes" / "No").
## Form Instructions
Show format with placeholders, not instructions. For non-obvious fields, explain why you're asking.
---
**Avoid**: Jargon without explanation. Blaming users ("You made an error" → "This field is required"). Vague errors ("Something went wrong"). Varying terminology for variety. Humor for errors.
-354
View File
@@ -1,354 +0,0 @@
---
name: harden
description: Improve interface resilience through better error handling, i18n support, text overflow handling, and edge case management. Makes interfaces robust and production-ready.
user-invocable: true
argument-hint: [TARGET=<value>]
---
Strengthen interfaces against edge cases, errors, internationalization issues, and real-world usage scenarios that break idealized designs.
## Assess Hardening Needs
Identify weaknesses and edge cases:
1. **Test with extreme inputs**:
- Very long text (names, descriptions, titles)
- Very short text (empty, single character)
- Special characters (emoji, RTL text, accents)
- Large numbers (millions, billions)
- Many items (1000+ list items, 50+ options)
- No data (empty states)
2. **Test error scenarios**:
- Network failures (offline, slow, timeout)
- API errors (400, 401, 403, 404, 500)
- Validation errors
- Permission errors
- Rate limiting
- Concurrent operations
3. **Test internationalization**:
- Long translations (German is often 30% longer than English)
- RTL languages (Arabic, Hebrew)
- Character sets (Chinese, Japanese, Korean, emoji)
- Date/time formats
- Number formats (1,000 vs 1.000)
- Currency symbols
**CRITICAL**: Designs that only work with perfect data aren't production-ready. Harden against reality.
## Hardening Dimensions
Systematically improve resilience:
### Text Overflow & Wrapping
**Long text handling**:
```css
/* Single line with ellipsis */
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Multi-line with clamp */
.line-clamp {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Allow wrapping */
.wrap {
word-wrap: break-word;
overflow-wrap: break-word;
hyphens: auto;
}
```
**Flex/Grid overflow**:
```css
/* Prevent flex items from overflowing */
.flex-item {
min-width: 0; /* Allow shrinking below content size */
overflow: hidden;
}
/* Prevent grid items from overflowing */
.grid-item {
min-width: 0;
min-height: 0;
}
```
**Responsive text sizing**:
- Use `clamp()` for fluid typography
- Set minimum readable sizes (14px on mobile)
- Test text scaling (zoom to 200%)
- Ensure containers expand with text
### Internationalization (i18n)
**Text expansion**:
- Add 30-40% space budget for translations
- Use flexbox/grid that adapts to content
- Test with longest language (usually German)
- Avoid fixed widths on text containers
```jsx
// ❌ Bad: Assumes short English text
<button className="w-24">Submit</button>
// ✅ Good: Adapts to content
<button className="px-4 py-2">Submit</button>
```
**RTL (Right-to-Left) support**:
```css
/* Use logical properties */
margin-inline-start: 1rem; /* Not margin-left */
padding-inline: 1rem; /* Not padding-left/right */
border-inline-end: 1px solid; /* Not border-right */
/* Or use dir attribute */
[dir="rtl"] .arrow { transform: scaleX(-1); }
```
**Character set support**:
- Use UTF-8 encoding everywhere
- Test with Chinese/Japanese/Korean (CJK) characters
- Test with emoji (they can be 2-4 bytes)
- Handle different scripts (Latin, Cyrillic, Arabic, etc.)
**Date/Time formatting**:
```javascript
// ✅ Use Intl API for proper formatting
new Intl.DateTimeFormat('en-US').format(date); // 1/15/2024
new Intl.DateTimeFormat('de-DE').format(date); // 15.1.2024
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(1234.56); // $1,234.56
```
**Pluralization**:
```javascript
// ❌ Bad: Assumes English pluralization
`${count} item${count !== 1 ? 's' : ''}`
// ✅ Good: Use proper i18n library
t('items', { count }) // Handles complex plural rules
```
### Error Handling
**Network errors**:
- Show clear error messages
- Provide retry button
- Explain what happened
- Offer offline mode (if applicable)
- Handle timeout scenarios
```jsx
// Error states with recovery
{error && (
<ErrorMessage>
<p>Failed to load data. {error.message}</p>
<button onClick={retry}>Try again</button>
</ErrorMessage>
)}
```
**Form validation errors**:
- Inline errors near fields
- Clear, specific messages
- Suggest corrections
- Don't block submission unnecessarily
- Preserve user input on error
**API errors**:
- Handle each status code appropriately
- 400: Show validation errors
- 401: Redirect to login
- 403: Show permission error
- 404: Show not found state
- 429: Show rate limit message
- 500: Show generic error, offer support
**Graceful degradation**:
- Core functionality works without JavaScript
- Images have alt text
- Progressive enhancement
- Fallbacks for unsupported features
### Edge Cases & Boundary Conditions
**Empty states**:
- No items in list
- No search results
- No notifications
- No data to display
- Provide clear next action
**Loading states**:
- Initial load
- Pagination load
- Refresh
- Show what's loading ("Loading your projects...")
- Time estimates for long operations
**Large datasets**:
- Pagination or virtual scrolling
- Search/filter capabilities
- Performance optimization
- Don't load all 10,000 items at once
**Concurrent operations**:
- Prevent double-submission (disable button while loading)
- Handle race conditions
- Optimistic updates with rollback
- Conflict resolution
**Permission states**:
- No permission to view
- No permission to edit
- Read-only mode
- Clear explanation of why
**Browser compatibility**:
- Polyfills for modern features
- Fallbacks for unsupported CSS
- Feature detection (not browser detection)
- Test in target browsers
### Input Validation & Sanitization
**Client-side validation**:
- Required fields
- Format validation (email, phone, URL)
- Length limits
- Pattern matching
- Custom validation rules
**Server-side validation** (always):
- Never trust client-side only
- Validate and sanitize all inputs
- Protect against injection attacks
- Rate limiting
**Constraint handling**:
```html
<!-- Set clear constraints -->
<input
type="text"
maxlength="100"
pattern="[A-Za-z0-9]+"
required
aria-describedby="username-hint"
/>
<small id="username-hint">
Letters and numbers only, up to 100 characters
</small>
```
### Accessibility Resilience
**Keyboard navigation**:
- All functionality accessible via keyboard
- Logical tab order
- Focus management in modals
- Skip links for long content
**Screen reader support**:
- Proper ARIA labels
- Announce dynamic changes (live regions)
- Descriptive alt text
- Semantic HTML
**Motion sensitivity**:
```css
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
```
**High contrast mode**:
- Test in Windows high contrast mode
- Don't rely only on color
- Provide alternative visual cues
### Performance Resilience
**Slow connections**:
- Progressive image loading
- Skeleton screens
- Optimistic UI updates
- Offline support (service workers)
**Memory leaks**:
- Clean up event listeners
- Cancel subscriptions
- Clear timers/intervals
- Abort pending requests on unmount
**Throttling & Debouncing**:
```javascript
// Debounce search input
const debouncedSearch = debounce(handleSearch, 300);
// Throttle scroll handler
const throttledScroll = throttle(handleScroll, 100);
```
## Testing Strategies
**Manual testing**:
- Test with extreme data (very long, very short, empty)
- Test in different languages
- Test offline
- Test slow connection (throttle to 3G)
- Test with screen reader
- Test keyboard-only navigation
- Test on old browsers
**Automated testing**:
- Unit tests for edge cases
- Integration tests for error scenarios
- E2E tests for critical paths
- Visual regression tests
- Accessibility tests (axe, WAVE)
**IMPORTANT**: Hardening is about expecting the unexpected. Real users will do things you never imagined.
**NEVER**:
- Assume perfect input (validate everything)
- Ignore internationalization (design for global)
- Leave error messages generic ("Error occurred")
- Forget offline scenarios
- Trust client-side validation alone
- Use fixed widths for text
- Assume English-length text
- Block entire interface when one component errors
## Verify Hardening
Test thoroughly with edge cases:
- **Long text**: Try names with 100+ characters
- **Emoji**: Use emoji in all text fields
- **RTL**: Test with Arabic or Hebrew
- **CJK**: Test with Chinese/Japanese/Korean
- **Network issues**: Disable internet, throttle connection
- **Large datasets**: Test with 1000+ items
- **Concurrent actions**: Click submit 10 times rapidly
- **Errors**: Force API errors, test all error states
- **Empty**: Remove all data, test empty states
Remember: You're hardening for production reality, not demo perfection. Expect users to input weird data, lose connection mid-flow, and use your product in unexpected ways. Build resilience into every component.
-70
View File
@@ -1,70 +0,0 @@
---
name: normalize
description: Normalize design to match your design system and ensure consistency
user-invocable: true
argument-hint: [FEATURE=<value>]
---
Analyze and redesign the feature to perfectly match our design system standards, aesthetics, and established patterns.
## MANDATORY PREPARATION
Use the frontend-design skill — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run teach-impeccable first.
---
## Plan
Before making changes, deeply understand the context:
1. **Discover the design system**: Search for design system documentation, UI guidelines, component libraries, or style guides (grep for "design system", "ui guide", "style guide", etc.). Study it thoroughly until you understand:
- Core design principles and aesthetic direction
- Target audience and personas
- Component patterns and conventions
- Design tokens (colors, typography, spacing)
**CRITICAL**: If something isn't clear, ask. Don't guess at design system principles.
2. **Analyze the current feature**: Assess what works and what doesn't:
- Where does it deviate from design system patterns?
- Which inconsistencies are cosmetic vs. functional?
- What's the root cause—missing tokens, one-off implementations, or conceptual misalignment?
3. **Create a normalization plan**: Define specific changes that will align the feature with the design system:
- Which components can be replaced with design system equivalents?
- Which styles need to use design tokens instead of hard-coded values?
- How can UX patterns match established user flows?
**IMPORTANT**: Great design is effective design. Prioritize UX consistency and usability over visual polish alone. Think through the best possible experience for your use case and personas first.
## Execute
Systematically address all inconsistencies across these dimensions:
- **Typography**: Use design system fonts, sizes, weights, and line heights. Replace hard-coded values with typographic tokens or classes.
- **Color & Theme**: Apply design system color tokens. Remove one-off color choices that break the palette.
- **Spacing & Layout**: Use spacing tokens (margins, padding, gaps). Align with grid systems and layout patterns used elsewhere.
- **Components**: Replace custom implementations with design system components. Ensure props and variants match established patterns.
- **Motion & Interaction**: Match animation timing, easing, and interaction patterns to other features.
- **Responsive Behavior**: Ensure breakpoints and responsive patterns align with design system standards.
- **Accessibility**: Verify contrast ratios, focus states, ARIA labels match design system requirements.
- **Progressive Disclosure**: Match information hierarchy and complexity management to established patterns.
**NEVER**:
- Create new one-off components when design system equivalents exist
- Hard-code values that should use design tokens
- Introduce new patterns that diverge from the design system
- Compromise accessibility for visual consistency
This is not an exhaustive list—apply judgment to identify all areas needing normalization.
## Clean Up
After normalization, ensure code quality:
- **Consolidate reusable components**: If you created new components that should be shared, move them to the design system or shared UI component path.
- **Remove orphaned code**: Delete unused implementations, styles, or files made obsolete by normalization.
- **Verify quality**: Lint, type-check, and test according to repository guidelines. Ensure normalization didn't introduce regressions.
- **Ensure DRYness**: Look for duplication introduced during refactoring and consolidate.
Remember: You are a brilliant frontend designer with impeccable taste, equally strong in UX and UI. Your attention to detail and eye for end-to-end user experience is world class. Execute with precision and thoroughness.
-245
View File
@@ -1,245 +0,0 @@
---
name: onboard
description: Design or improve onboarding flows, empty states, and first-time user experiences. Helps users get started successfully and understand value quickly.
user-invocable: true
argument-hint: [TARGET=<value>]
---
## MANDATORY PREPARATION
Use the frontend-design skill — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run teach-impeccable first. Additionally gather: the "aha moment" you want users to reach, and users' experience level.
---
Create or improve onboarding experiences that help users understand, adopt, and succeed with the product quickly.
## Assess Onboarding Needs
Understand what users need to learn and why:
1. **Identify the challenge**:
- What are users trying to accomplish?
- What's confusing or unclear about current experience?
- Where do users get stuck or drop off?
- What's the "aha moment" we want users to reach?
2. **Understand the users**:
- What's their experience level? (Beginners, power users, mixed?)
- What's their motivation? (Excited and exploring? Required by work?)
- What's their time commitment? (5 minutes? 30 minutes?)
- What alternatives do they know? (Coming from competitor? New to category?)
3. **Define success**:
- What's the minimum users need to learn to be successful?
- What's the key action we want them to take? (First project? First invite?)
- How do we know onboarding worked? (Completion rate? Time to value?)
**CRITICAL**: Onboarding should get users to value as quickly as possible, not teach everything possible.
## Onboarding Principles
Follow these core principles:
### Show, Don't Tell
- Demonstrate with working examples, not just descriptions
- Provide real functionality in onboarding, not separate tutorial mode
- Use progressive disclosure - teach one thing at a time
### Make It Optional (When Possible)
- Let experienced users skip onboarding
- Don't block access to product
- Provide "Skip" or "I'll explore on my own" options
### Time to Value
- Get users to their "aha moment" ASAP
- Front-load most important concepts
- Teach 20% that delivers 80% of value
- Save advanced features for contextual discovery
### Context Over Ceremony
- Teach features when users need them, not upfront
- Empty states are onboarding opportunities
- Tooltips and hints at point of use
### Respect User Intelligence
- Don't patronize or over-explain
- Be concise and clear
- Assume users can figure out standard patterns
## Design Onboarding Experiences
Create appropriate onboarding for the context:
### Initial Product Onboarding
**Welcome Screen**:
- Clear value proposition (what is this product?)
- What users will learn/accomplish
- Time estimate (honest about commitment)
- Option to skip (for experienced users)
**Account Setup**:
- Minimal required information (collect more later)
- Explain why you're asking for each piece of information
- Smart defaults where possible
- Social login when appropriate
**Core Concept Introduction**:
- Introduce 1-3 core concepts (not everything)
- Use simple language and examples
- Interactive when possible (do, don't just read)
- Progress indication (step 1 of 3)
**First Success**:
- Guide users to accomplish something real
- Pre-populated examples or templates
- Celebrate completion (but don't overdo it)
- Clear next steps
### Feature Discovery & Adoption
**Empty States**:
Instead of blank space, show:
- What will appear here (description + screenshot/illustration)
- Why it's valuable
- Clear CTA to create first item
- Example or template option
Example:
```
No projects yet
Projects help you organize your work and collaborate with your team.
[Create your first project] or [Start from template]
```
**Contextual Tooltips**:
- Appear at relevant moment (first time user sees feature)
- Point directly at relevant UI element
- Brief explanation + benefit
- Dismissable (with "Don't show again" option)
- Optional "Learn more" link
**Feature Announcements**:
- Highlight new features when they're released
- Show what's new and why it matters
- Let users try immediately
- Dismissable
**Progressive Onboarding**:
- Teach features when users encounter them
- Badges or indicators on new/unused features
- Unlock complexity gradually (don't show all options immediately)
### Guided Tours & Walkthroughs
**When to use**:
- Complex interfaces with many features
- Significant changes to existing product
- Industry-specific tools needing domain knowledge
**How to design**:
- Spotlight specific UI elements (dim rest of page)
- Keep steps short (3-7 steps max per tour)
- Allow users to click through tour freely
- Include "Skip tour" option
- Make replayable (help menu)
**Best practices**:
- Interactive > passive (let users click real buttons)
- Focus on workflow, not features ("Create a project" not "This is the project button")
- Provide sample data so actions work
### Interactive Tutorials
**When to use**:
- Users need hands-on practice
- Concepts are complex or unfamiliar
- High stakes (better to practice in safe environment)
**How to design**:
- Sandbox environment with sample data
- Clear objectives ("Create a chart showing sales by region")
- Step-by-step guidance
- Validation (confirm they did it right)
- Graduation moment (you're ready!)
### Documentation & Help
**In-product help**:
- Contextual help links throughout interface
- Keyboard shortcut reference
- Search-able help center
- Video tutorials for complex workflows
**Help patterns**:
- `?` icon near complex features
- "Learn more" links in tooltips
- Keyboard shortcut hints (`⌘K` shown on search box)
## Empty State Design
Every empty state needs:
### What Will Be Here
"Your recent projects will appear here"
### Why It Matters
"Projects help you organize your work and collaborate with your team"
### How to Get Started
[Create project] or [Import from template]
### Visual Interest
Illustration or icon (not just text on blank page)
### Contextual Help
"Need help getting started? [Watch 2-min tutorial]"
**Empty state types**:
- **First use**: Never used this feature (emphasize value, provide template)
- **User cleared**: Intentionally deleted everything (light touch, easy to recreate)
- **No results**: Search or filter returned nothing (suggest different query, clear filters)
- **No permissions**: Can't access (explain why, how to get access)
- **Error state**: Failed to load (explain what happened, retry option)
## Implementation Patterns
### Technical approaches:
**Tooltip libraries**: Tippy.js, Popper.js
**Tour libraries**: Intro.js, Shepherd.js, React Joyride
**Modal patterns**: Focus trap, backdrop, ESC to close
**Progress tracking**: LocalStorage for "seen" states
**Analytics**: Track completion, drop-off points
**Storage patterns**:
```javascript
// Track which onboarding steps user has seen
localStorage.setItem('onboarding-completed', 'true');
localStorage.setItem('feature-tooltip-seen-reports', 'true');
```
**IMPORTANT**: Don't show same onboarding twice (annoying). Track completion and respect dismissals.
**NEVER**:
- Force users through long onboarding before they can use product
- Patronize users with obvious explanations
- Show same tooltip repeatedly (respect dismissals)
- Block all UI during tour (let users explore)
- Create separate tutorial mode disconnected from real product
- Overwhelm with information upfront (progressive disclosure!)
- Hide "Skip" or make it hard to find
- Forget about returning users (don't show initial onboarding again)
## Verify Onboarding Quality
Test with real users:
- **Time to completion**: Can users complete onboarding quickly?
- **Comprehension**: Do users understand after completing?
- **Action**: Do users take desired next step?
- **Skip rate**: Are too many users skipping? (Maybe it's too long/not valuable)
- **Completion rate**: Are users completing? (If low, simplify)
- **Time to value**: How long until users get first value?
Remember: You're a product educator with excellent teaching instincts. Get users to their "aha moment" as quickly as possible. Teach the essential, make it contextual, respect user time and intelligence.
-265
View File
@@ -1,265 +0,0 @@
---
name: optimize
description: Improve interface performance across loading speed, rendering, animations, images, and bundle size. Makes experiences faster and smoother.
user-invocable: true
argument-hint: [TARGET=<value>]
---
Identify and fix performance issues to create faster, smoother user experiences.
## Assess Performance Issues
Understand current performance and identify problems:
1. **Measure current state**:
- **Core Web Vitals**: LCP, FID/INP, CLS scores
- **Load time**: Time to interactive, first contentful paint
- **Bundle size**: JavaScript, CSS, image sizes
- **Runtime performance**: Frame rate, memory usage, CPU usage
- **Network**: Request count, payload sizes, waterfall
2. **Identify bottlenecks**:
- What's slow? (Initial load? Interactions? Animations?)
- What's causing it? (Large images? Expensive JavaScript? Layout thrashing?)
- How bad is it? (Perceivable? Annoying? Blocking?)
- Who's affected? (All users? Mobile only? Slow connections?)
**CRITICAL**: Measure before and after. Premature optimization wastes time. Optimize what actually matters.
## Optimization Strategy
Create systematic improvement plan:
### Loading Performance
**Optimize Images**:
- Use modern formats (WebP, AVIF)
- Proper sizing (don't load 3000px image for 300px display)
- Lazy loading for below-fold images
- Responsive images (`srcset`, `picture` element)
- Compress images (80-85% quality is usually imperceptible)
- Use CDN for faster delivery
```html
<img
src="hero.webp"
srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1200.webp 1200w"
sizes="(max-width: 400px) 400px, (max-width: 800px) 800px, 1200px"
loading="lazy"
alt="Hero image"
/>
```
**Reduce JavaScript Bundle**:
- Code splitting (route-based, component-based)
- Tree shaking (remove unused code)
- Remove unused dependencies
- Lazy load non-critical code
- Use dynamic imports for large components
```javascript
// Lazy load heavy component
const HeavyChart = lazy(() => import('./HeavyChart'));
```
**Optimize CSS**:
- Remove unused CSS
- Critical CSS inline, rest async
- Minimize CSS files
- Use CSS containment for independent regions
**Optimize Fonts**:
- Use `font-display: swap` or `optional`
- Subset fonts (only characters you need)
- Preload critical fonts
- Use system fonts when appropriate
- Limit font weights loaded
```css
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom.woff2') format('woff2');
font-display: swap; /* Show fallback immediately */
unicode-range: U+0020-007F; /* Basic Latin only */
}
```
**Optimize Loading Strategy**:
- Critical resources first (async/defer non-critical)
- Preload critical assets
- Prefetch likely next pages
- Service worker for offline/caching
- HTTP/2 or HTTP/3 for multiplexing
### Rendering Performance
**Avoid Layout Thrashing**:
```javascript
// ❌ Bad: Alternating reads and writes (causes reflows)
elements.forEach(el => {
const height = el.offsetHeight; // Read (forces layout)
el.style.height = height * 2; // Write
});
// ✅ Good: Batch reads, then batch writes
const heights = elements.map(el => el.offsetHeight); // All reads
elements.forEach((el, i) => {
el.style.height = heights[i] * 2; // All writes
});
```
**Optimize Rendering**:
- Use CSS `contain` property for independent regions
- Minimize DOM depth (flatter is faster)
- Reduce DOM size (fewer elements)
- Use `content-visibility: auto` for long lists
- Virtual scrolling for very long lists (react-window, react-virtualized)
**Reduce Paint & Composite**:
- Use `transform` and `opacity` for animations (GPU-accelerated)
- Avoid animating layout properties (width, height, top, left)
- Use `will-change` sparingly for known expensive operations
- Minimize paint areas (smaller is faster)
### Animation Performance
**GPU Acceleration**:
```css
/* ✅ GPU-accelerated (fast) */
.animated {
transform: translateX(100px);
opacity: 0.5;
}
/* ❌ CPU-bound (slow) */
.animated {
left: 100px;
width: 300px;
}
```
**Smooth 60fps**:
- Target 16ms per frame (60fps)
- Use `requestAnimationFrame` for JS animations
- Debounce/throttle scroll handlers
- Use CSS animations when possible
- Avoid long-running JavaScript during animations
**Intersection Observer**:
```javascript
// Efficiently detect when elements enter viewport
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// Element is visible, lazy load or animate
}
});
});
```
### React/Framework Optimization
**React-specific**:
- Use `memo()` for expensive components
- `useMemo()` and `useCallback()` for expensive computations
- Virtualize long lists
- Code split routes
- Avoid inline function creation in render
- Use React DevTools Profiler
**Framework-agnostic**:
- Minimize re-renders
- Debounce expensive operations
- Memoize computed values
- Lazy load routes and components
### Network Optimization
**Reduce Requests**:
- Combine small files
- Use SVG sprites for icons
- Inline small critical assets
- Remove unused third-party scripts
**Optimize APIs**:
- Use pagination (don't load everything)
- GraphQL to request only needed fields
- Response compression (gzip, brotli)
- HTTP caching headers
- CDN for static assets
**Optimize for Slow Connections**:
- Adaptive loading based on connection (navigator.connection)
- Optimistic UI updates
- Request prioritization
- Progressive enhancement
## Core Web Vitals Optimization
### Largest Contentful Paint (LCP < 2.5s)
- Optimize hero images
- Inline critical CSS
- Preload key resources
- Use CDN
- Server-side rendering
### First Input Delay (FID < 100ms) / INP (< 200ms)
- Break up long tasks
- Defer non-critical JavaScript
- Use web workers for heavy computation
- Reduce JavaScript execution time
### Cumulative Layout Shift (CLS < 0.1)
- Set dimensions on images and videos
- Don't inject content above existing content
- Use `aspect-ratio` CSS property
- Reserve space for ads/embeds
- Avoid animations that cause layout shifts
```css
/* Reserve space for image */
.image-container {
aspect-ratio: 16 / 9;
}
```
## Performance Monitoring
**Tools to use**:
- Chrome DevTools (Lighthouse, Performance panel)
- WebPageTest
- Core Web Vitals (Chrome UX Report)
- Bundle analyzers (webpack-bundle-analyzer)
- Performance monitoring (Sentry, DataDog, New Relic)
**Key metrics**:
- LCP, FID/INP, CLS (Core Web Vitals)
- Time to Interactive (TTI)
- First Contentful Paint (FCP)
- Total Blocking Time (TBT)
- Bundle size
- Request count
**IMPORTANT**: Measure on real devices with real network conditions. Desktop Chrome with fast connection isn't representative.
**NEVER**:
- Optimize without measuring (premature optimization)
- Sacrifice accessibility for performance
- Break functionality while optimizing
- Use `will-change` everywhere (creates new layers, uses memory)
- Lazy load above-fold content
- Optimize micro-optimizations while ignoring major issues (optimize the biggest bottleneck first)
- Forget about mobile performance (often slower devices, slower connections)
## Verify Improvements
Test that optimizations worked:
- **Before/after metrics**: Compare Lighthouse scores
- **Real user monitoring**: Track improvements for real users
- **Different devices**: Test on low-end Android, not just flagship iPhone
- **Slow connections**: Throttle to 3G, test experience
- **No regressions**: Ensure functionality still works
- **User perception**: Does it *feel* faster?
Remember: Performance is a feature. Fast experiences feel more responsive, more polished, more professional. Optimize systematically, measure ruthlessly, and prioritize user-perceived performance.
-141
View File
@@ -1,141 +0,0 @@
---
name: overdrive
description: Push interfaces past conventional limits with technically ambitious implementations. Whether that's a shader, a 60fps virtual table, spring physics on a dialog, or scroll-driven reveals — make users ask "how did they do that?"
user-invocable: true
argument-hint: [TARGET=<value>]
---
Start your response with:
```
──────────── ⚡ OVERDRIVE ─────────────
》》》 Entering overdrive mode...
```
Push an interface past conventional limits. This isn't just about visual effects — it's about using the full power of the browser to make any part of an interface feel extraordinary: a table that handles a million rows, a dialog that morphs from its trigger, a form that validates in real-time with streaming feedback, a page transition that feels cinematic.
## MANDATORY PREPARATION
Use the frontend-design skill — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run teach-impeccable first.
**EXTRA IMPORTANT FOR THIS SKILL**: Context determines what "extraordinary" means. A particle system on a creative portfolio is impressive. The same particle system on a settings page is embarrassing. But a settings page with instant optimistic saves and animated state transitions? That's extraordinary too. Understand the project's personality and goals before deciding what's appropriate.
### Propose Before Building
This skill has the highest potential to misfire. Do NOT jump straight into implementation. You MUST:
1. **Think through 2-3 different directions** — consider different techniques, levels of ambition, and aesthetic approaches. For each direction, briefly describe what the result would look and feel like.
2. **ask the user directly to clarify what you cannot infer.** to present these directions and get the user's pick before writing any code. Explain trade-offs (browser support, performance cost, complexity).
3. Only proceed with the direction the user confirms.
Skipping this step risks building something embarrassing that needs to be thrown away.
### Iterate with Browser Automation
Technically ambitious effects almost never work on the first try. You MUST actively use browser automation tools to preview your work, visually verify the result, and iterate. Do not assume the effect looks right — check it. Expect multiple rounds of refinement. The gap between "technically works" and "looks extraordinary" is closed through visual iteration, not code alone.
---
## Assess What "Extraordinary" Means Here
The right kind of technical ambition depends entirely on what you're working with. Before choosing a technique, ask: **what would make a user of THIS specific interface say "wow, that's nice"?**
### For visual/marketing surfaces
Pages, hero sections, landing pages, portfolios — the "wow" is often sensory: a scroll-driven reveal, a shader background, a cinematic page transition, generative art that responds to the cursor.
### For functional UI
Tables, forms, dialogs, navigation — the "wow" is in how it FEELS: a dialog that morphs from the button that triggered it via View Transitions, a data table that renders 100k rows at 60fps via virtual scrolling, a form with streaming validation that feels instant, drag-and-drop with spring physics.
### For performance-critical UI
The "wow" is invisible but felt: a search that filters 50k items without a flicker, a complex form that never blocks the main thread, an image editor that processes in near-real-time. The interface just never hesitates.
### For data-heavy interfaces
Charts and dashboards — the "wow" is in fluidity: GPU-accelerated rendering via Canvas/WebGL for massive datasets, animated transitions between data states, force-directed graph layouts that settle naturally.
**The common thread**: something about the implementation goes beyond what users expect from a web interface. The technique serves the experience, not the other way around.
## The Toolkit
Organized by what you're trying to achieve, not by technology name.
### Make transitions feel cinematic
- **View Transitions API** (same-document: all browsers; cross-document: no Firefox) — shared element morphing between states. A list item expanding into a detail page. A button morphing into a dialog. This is the closest thing to native FLIP animations.
- **`@starting-style`** (all browsers) — animate elements from `display: none` to visible with CSS only, including entry keyframes
- **Spring physics** — natural motion with mass, tension, and damping instead of cubic-bezier. Libraries: motion (formerly Framer Motion), GSAP, or roll your own spring solver.
### Tie animation to scroll position
- **Scroll-driven animations** (`animation-timeline: scroll()`) — CSS-only, no JS. Parallax, progress bars, reveal sequences all driven by scroll position. (Chrome/Edge/Safari; Firefox: flag only — always provide a static fallback)
### Render beyond CSS
- **WebGL** (all browsers) — shader effects, post-processing, particle systems. Libraries: Three.js, OGL (lightweight), regl. Use for effects CSS can't express.
- **WebGPU** (Chrome/Edge; Safari partial; Firefox: flag only) — next-gen GPU compute. More powerful than WebGL but limited browser support. Always fall back to WebGL2.
- **Canvas 2D / OffscreenCanvas** — custom rendering, pixel manipulation, or moving heavy rendering off the main thread entirely via Web Workers + OffscreenCanvas.
- **SVG filter chains** — displacement maps, turbulence, morphology for organic distortion effects. CSS-animatable.
### Make data feel alive
- **Virtual scrolling** — render only visible rows for tables/lists with tens of thousands of items. No library required for simple cases; TanStack Virtual for complex ones.
- **GPU-accelerated charts** — Canvas or WebGL-rendered data visualization for datasets too large for SVG/DOM. Libraries: deck.gl, regl-based custom renderers.
- **Animated data transitions** — morph between chart states rather than replacing. D3's `transition()` or View Transitions for DOM-based charts.
### Animate complex properties
- **`@property`** (all browsers) — register custom CSS properties with types, enabling animation of gradients, colors, and complex values that CSS can't normally interpolate.
- **Web Animations API** (all browsers) — JavaScript-driven animations with the performance of CSS. Composable, cancellable, reversible. The foundation for complex choreography.
### Push performance boundaries
- **Web Workers** — move computation off the main thread. Heavy data processing, image manipulation, search indexing — anything that would cause jank.
- **OffscreenCanvas** — render in a Worker thread. The main thread stays free while complex visuals render in the background.
- **WASM** — near-native performance for computation-heavy features. Image processing, physics simulations, codecs.
### Interact with the device
- **Web Audio API** — spatial audio, audio-reactive visualizations, sonic feedback. Requires user gesture to start.
- **Device APIs** — orientation, ambient light, geolocation. Use sparingly and always with user permission.
**NOTE**: This skill is about enhancing how an interface FEELS, not changing what a product DOES. Adding real-time collaboration, offline support, or new backend capabilities are product decisions, not UI enhancements. Focus on making existing features feel extraordinary.
## Implement with Discipline
### Progressive enhancement is non-negotiable
Every technique must degrade gracefully. The experience without the enhancement must still be good.
```css
@supports (animation-timeline: scroll()) {
.hero { animation-timeline: scroll(); }
}
```
```javascript
if ('gpu' in navigator) { /* WebGPU */ }
else if (canvas.getContext('webgl2')) { /* WebGL2 fallback */ }
/* CSS-only fallback must still look good */
```
### Performance rules
- Target 60fps. If dropping below 50, simplify.
- Respect `prefers-reduced-motion` — always. Provide a beautiful static alternative.
- Lazy-initialize heavy resources (WebGL contexts, WASM modules) only when near viewport.
- Pause off-screen rendering. Kill what you can't see.
- Test on real mid-range devices, not just your development machine.
### Polish is the difference
The gap between "cool" and "extraordinary" is in the last 20% of refinement: the easing curve on a spring animation, the timing offset in a staggered reveal, the subtle secondary motion that makes a transition feel physical. Don't ship the first version that works — ship the version that feels inevitable.
**NEVER**:
- Ignore `prefers-reduced-motion` — this is an accessibility requirement, not a suggestion
- Ship effects that cause jank on mid-range devices
- Use bleeding-edge APIs without a functional fallback
- Add sound without explicit user opt-in
- Use technical ambition to mask weak design fundamentals — fix those first with other skills
- Layer multiple competing extraordinary moments — focus creates impact, excess creates noise
## Verify the Result
- **The wow test**: Show it to someone who hasn't seen it. Do they react?
- **The removal test**: Take it away. Does the experience feel diminished, or does nobody notice?
- **The device test**: Run it on a phone, a tablet, a Chromebook. Still smooth?
- **The accessibility test**: Enable reduced motion. Still beautiful?
- **The context test**: Does this make sense for THIS brand and audience?
Remember: "Technically extraordinary" isn't about using the newest API. It's about making an interface do something users didn't think a website could do.
-202
View File
@@ -1,202 +0,0 @@
---
name: polish
description: Final quality pass before shipping. Fixes alignment, spacing, consistency, and detail issues that separate good from great.
user-invocable: true
argument-hint: [TARGET=<value>]
---
## MANDATORY PREPARATION
Use the frontend-design skill — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run teach-impeccable first. Additionally gather: quality bar (MVP vs flagship).
---
Perform a meticulous final pass to catch all the small details that separate good work from great work. The difference between shipped and polished.
## Pre-Polish Assessment
Understand the current state and goals:
1. **Review completeness**:
- Is it functionally complete?
- Are there known issues to preserve (mark with TODOs)?
- What's the quality bar? (MVP vs flagship feature?)
- When does it ship? (How much time for polish?)
2. **Identify polish areas**:
- Visual inconsistencies
- Spacing and alignment issues
- Interaction state gaps
- Copy inconsistencies
- Edge cases and error states
- Loading and transition smoothness
**CRITICAL**: Polish is the last step, not the first. Don't polish work that's not functionally complete.
## Polish Systematically
Work through these dimensions methodically:
### Visual Alignment & Spacing
- **Pixel-perfect alignment**: Everything lines up to grid
- **Consistent spacing**: All gaps use spacing scale (no random 13px gaps)
- **Optical alignment**: Adjust for visual weight (icons may need offset for optical centering)
- **Responsive consistency**: Spacing and alignment work at all breakpoints
- **Grid adherence**: Elements snap to baseline grid
**Check**:
- Enable grid overlay and verify alignment
- Check spacing with browser inspector
- Test at multiple viewport sizes
- Look for elements that "feel" off
### Typography Refinement
- **Hierarchy consistency**: Same elements use same sizes/weights throughout
- **Line length**: 45-75 characters for body text
- **Line height**: Appropriate for font size and context
- **Widows & orphans**: No single words on last line
- **Hyphenation**: Appropriate for language and column width
- **Kerning**: Adjust letter spacing where needed (especially headlines)
- **Font loading**: No FOUT/FOIT flashes
### Color & Contrast
- **Contrast ratios**: All text meets WCAG standards
- **Consistent token usage**: No hard-coded colors, all use design tokens
- **Theme consistency**: Works in all theme variants
- **Color meaning**: Same colors mean same things throughout
- **Accessible focus**: Focus indicators visible with sufficient contrast
- **Tinted neutrals**: No pure gray or pure black—add subtle color tint (0.01 chroma)
- **Gray on color**: Never put gray text on colored backgrounds—use a shade of that color or transparency
### Interaction States
Every interactive element needs all states:
- **Default**: Resting state
- **Hover**: Subtle feedback (color, scale, shadow)
- **Focus**: Keyboard focus indicator (never remove without replacement)
- **Active**: Click/tap feedback
- **Disabled**: Clearly non-interactive
- **Loading**: Async action feedback
- **Error**: Validation or error state
- **Success**: Successful completion
**Missing states create confusion and broken experiences**.
### Micro-interactions & Transitions
- **Smooth transitions**: All state changes animated appropriately (150-300ms)
- **Consistent easing**: Use ease-out-quart/quint/expo for natural deceleration. Never bounce or elastic—they feel dated.
- **No jank**: 60fps animations, only animate transform and opacity
- **Appropriate motion**: Motion serves purpose, not decoration
- **Reduced motion**: Respects `prefers-reduced-motion`
### Content & Copy
- **Consistent terminology**: Same things called same names throughout
- **Consistent capitalization**: Title Case vs Sentence case applied consistently
- **Grammar & spelling**: No typos
- **Appropriate length**: Not too wordy, not too terse
- **Punctuation consistency**: Periods on sentences, not on labels (unless all labels have them)
### Icons & Images
- **Consistent style**: All icons from same family or matching style
- **Appropriate sizing**: Icons sized consistently for context
- **Proper alignment**: Icons align with adjacent text optically
- **Alt text**: All images have descriptive alt text
- **Loading states**: Images don't cause layout shift, proper aspect ratios
- **Retina support**: 2x assets for high-DPI screens
### Forms & Inputs
- **Label consistency**: All inputs properly labeled
- **Required indicators**: Clear and consistent
- **Error messages**: Helpful and consistent
- **Tab order**: Logical keyboard navigation
- **Auto-focus**: Appropriate (don't overuse)
- **Validation timing**: Consistent (on blur vs on submit)
### Edge Cases & Error States
- **Loading states**: All async actions have loading feedback
- **Empty states**: Helpful empty states, not just blank space
- **Error states**: Clear error messages with recovery paths
- **Success states**: Confirmation of successful actions
- **Long content**: Handles very long names, descriptions, etc.
- **No content**: Handles missing data gracefully
- **Offline**: Appropriate offline handling (if applicable)
### Responsiveness
- **All breakpoints**: Test mobile, tablet, desktop
- **Touch targets**: 44x44px minimum on touch devices
- **Readable text**: No text smaller than 14px on mobile
- **No horizontal scroll**: Content fits viewport
- **Appropriate reflow**: Content adapts logically
### Performance
- **Fast initial load**: Optimize critical path
- **No layout shift**: Elements don't jump after load (CLS)
- **Smooth interactions**: No lag or jank
- **Optimized images**: Appropriate formats and sizes
- **Lazy loading**: Off-screen content loads lazily
### Code Quality
- **Remove console logs**: No debug logging in production
- **Remove commented code**: Clean up dead code
- **Remove unused imports**: Clean up unused dependencies
- **Consistent naming**: Variables and functions follow conventions
- **Type safety**: No TypeScript `any` or ignored errors
- **Accessibility**: Proper ARIA labels and semantic HTML
## Polish Checklist
Go through systematically:
- [ ] Visual alignment perfect at all breakpoints
- [ ] Spacing uses design tokens consistently
- [ ] Typography hierarchy consistent
- [ ] All interactive states implemented
- [ ] All transitions smooth (60fps)
- [ ] Copy is consistent and polished
- [ ] Icons are consistent and properly sized
- [ ] All forms properly labeled and validated
- [ ] Error states are helpful
- [ ] Loading states are clear
- [ ] Empty states are welcoming
- [ ] Touch targets are 44x44px minimum
- [ ] Contrast ratios meet WCAG AA
- [ ] Keyboard navigation works
- [ ] Focus indicators visible
- [ ] No console errors or warnings
- [ ] No layout shift on load
- [ ] Works in all supported browsers
- [ ] Respects reduced motion preference
- [ ] Code is clean (no TODOs, console.logs, commented code)
**IMPORTANT**: Polish is about details. Zoom in. Squint at it. Use it yourself. The little things add up.
**NEVER**:
- Polish before it's functionally complete
- Spend hours on polish if it ships in 30 minutes (triage)
- Introduce bugs while polishing (test thoroughly)
- Ignore systematic issues (if spacing is off everywhere, fix the system)
- Perfect one thing while leaving others rough (consistent quality level)
## Final Verification
Before marking as done:
- **Use it yourself**: Actually interact with the feature
- **Test on real devices**: Not just browser DevTools
- **Ask someone else to review**: Fresh eyes catch things
- **Compare to design**: Match intended design
- **Check all states**: Don't just test happy path
Remember: You have impeccable attention to detail and exquisite taste. Polish until it feels effortless, looks intentional, and works flawlessly. Sweat the details - they matter.
-102
View File
@@ -1,102 +0,0 @@
---
name: quieter
description: Tone down overly bold or visually aggressive designs. Reduces intensity while maintaining design quality and impact.
user-invocable: true
argument-hint: [TARGET=<value>]
---
Reduce visual intensity in designs that are too bold, aggressive, or overstimulating, creating a more refined and approachable aesthetic without losing effectiveness.
## MANDATORY PREPARATION
Use the frontend-design skill — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run teach-impeccable first.
---
## Assess Current State
Analyze what makes the design feel too intense:
1. **Identify intensity sources**:
- **Color saturation**: Overly bright or saturated colors
- **Contrast extremes**: Too much high-contrast juxtaposition
- **Visual weight**: Too many bold, heavy elements competing
- **Animation excess**: Too much motion or overly dramatic effects
- **Complexity**: Too many visual elements, patterns, or decorations
- **Scale**: Everything is large and loud with no hierarchy
2. **Understand the context**:
- What's the purpose? (Marketing vs tool vs reading experience)
- Who's the audience? (Some contexts need energy)
- What's working? (Don't throw away good ideas)
- What's the core message? (Preserve what matters)
If any of these are unclear from the codebase, ask the user directly to clarify what you cannot infer.
**CRITICAL**: "Quieter" doesn't mean boring or generic. It means refined, sophisticated, and easier on the eyes. Think luxury, not laziness.
## Plan Refinement
Create a strategy to reduce intensity while maintaining impact:
- **Color approach**: Desaturate or shift to more sophisticated tones?
- **Hierarchy approach**: Which elements should stay bold (very few), which should recede?
- **Simplification approach**: What can be removed entirely?
- **Sophistication approach**: How can we signal quality through restraint?
**IMPORTANT**: Great quiet design is harder than great bold design. Subtlety requires precision.
## Refine the Design
Systematically reduce intensity across these dimensions:
### Color Refinement
- **Reduce saturation**: Shift from fully saturated to 70-85% saturation
- **Soften palette**: Replace bright colors with muted, sophisticated tones
- **Reduce color variety**: Use fewer colors more thoughtfully
- **Neutral dominance**: Let neutrals do more work, use color as accent (10% rule)
- **Gentler contrasts**: High contrast only where it matters most
- **Tinted grays**: Use warm or cool tinted grays instead of pure gray—adds sophistication without loudness
- **Never gray on color**: If you have gray text on a colored background, use a darker shade of that color or transparency instead
### Visual Weight Reduction
- **Typography**: Reduce font weights (900 → 600, 700 → 500), decrease sizes where appropriate
- **Hierarchy through subtlety**: Use weight, size, and space instead of color and boldness
- **White space**: Increase breathing room, reduce density
- **Borders & lines**: Reduce thickness, decrease opacity, or remove entirely
### Simplification
- **Remove decorative elements**: Gradients, shadows, patterns, textures that don't serve purpose
- **Simplify shapes**: Reduce border radius extremes, simplify custom shapes
- **Reduce layering**: Flatten visual hierarchy where possible
- **Clean up effects**: Reduce or remove blur effects, glows, multiple shadows
### Motion Reduction
- **Reduce animation intensity**: Shorter distances (10-20px instead of 40px), gentler easing
- **Remove decorative animations**: Keep functional motion, remove flourishes
- **Subtle micro-interactions**: Replace dramatic effects with gentle feedback
- **Refined easing**: Use ease-out-quart for smooth, understated motion—never bounce or elastic
- **Remove animations entirely** if they're not serving a clear purpose
### Composition Refinement
- **Reduce scale jumps**: Smaller contrast between sizes creates calmer feeling
- **Align to grid**: Bring rogue elements back into systematic alignment
- **Even out spacing**: Replace extreme spacing variations with consistent rhythm
**NEVER**:
- Make everything the same size/weight (hierarchy still matters)
- Remove all color (quiet ≠ grayscale)
- Eliminate all personality (maintain character through refinement)
- Sacrifice usability for aesthetics (functional elements still need clear affordances)
- Make everything small and light (some anchors needed)
## Verify Quality
Ensure refinement maintains quality:
- **Still functional**: Can users still accomplish tasks easily?
- **Still distinctive**: Does it have character, or is it generic now?
- **Better reading**: Is text easier to read for extended periods?
- **Sophistication**: Does it feel more refined and premium?
Remember: Quiet design is confident design. It doesn't need to shout. Less is more, but less is also harder. Refine with precision and maintain intentionality.
-71
View File
@@ -1,71 +0,0 @@
---
name: teach-impeccable
description: One-time setup that gathers design context for your project and saves it to your AI config file. Run once to establish persistent design guidelines.
user-invocable: true
---
Gather design context for this project, then persist it for all future sessions.
## Step 1: Explore the Codebase
Before asking questions, thoroughly scan the project to discover what you can:
- **README and docs**: Project purpose, target audience, any stated goals
- **Package.json / config files**: Tech stack, dependencies, existing design libraries
- **Existing components**: Current design patterns, spacing, typography in use
- **Brand assets**: Logos, favicons, color values already defined
- **Design tokens / CSS variables**: Existing color palettes, font stacks, spacing scales
- **Any style guides or brand documentation**
Note what you've learned and what remains unclear.
## Step 2: Ask UX-Focused Questions
ask the user directly to clarify what you cannot infer. Focus only on what you couldn't infer from the codebase:
### Users & Purpose
- Who uses this? What's their context when using it?
- What job are they trying to get done?
- What emotions should the interface evoke? (confidence, delight, calm, urgency, etc.)
### Brand & Personality
- How would you describe the brand personality in 3 words?
- Any reference sites or apps that capture the right feel? What specifically about them?
- What should this explicitly NOT look like? Any anti-references?
### Aesthetic Preferences
- Any strong preferences for visual direction? (minimal, bold, elegant, playful, technical, organic, etc.)
- Light mode, dark mode, or both?
- Any colors that must be used or avoided?
### Accessibility & Inclusion
- Specific accessibility requirements? (WCAG level, known user needs)
- Considerations for reduced motion, color blindness, or other accommodations?
Skip questions where the answer is already clear from the codebase exploration.
## Step 3: Write Design Context
Synthesize your findings and the user's answers into a `## Design Context` section:
```markdown
## Design Context
### Users
[Who they are, their context, the job to be done]
### Brand Personality
[Voice, tone, 3-word personality, emotional goals]
### Aesthetic Direction
[Visual tone, references, anti-references, theme]
### Design Principles
[3-5 principles derived from the conversation that should guide all design decisions]
```
Write this section to `.impeccable.md` in the project root. If the file already exists, update the Design Context section in place.
Then ask the user directly to clarify what you cannot infer. whether they'd also like the Design Context appended to .github/copilot-instructions.md. If yes, append or update the section there as well.
Confirm completion and summarize the key design principles that will now guide all future work.
-115
View File
@@ -1,115 +0,0 @@
---
name: typeset
description: Improve typography by fixing font choices, hierarchy, sizing, weight consistency, and readability. Makes text feel intentional and polished.
user-invocable: true
argument-hint: [TARGET=<value>]
---
Assess and improve typography that feels generic, inconsistent, or poorly structured — turning default-looking text into intentional, well-crafted type.
## MANDATORY PREPARATION
Use the frontend-design skill — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run teach-impeccable first.
---
## Assess Current Typography
Analyze what's weak or generic about the current type:
1. **Font choices**:
- Are we using invisible defaults? (Inter, Roboto, Arial, Open Sans, system defaults)
- Does the font match the brand personality? (A playful brand shouldn't use a corporate typeface)
- Are there too many font families? (More than 2-3 is almost always a mess)
2. **Hierarchy**:
- Can you tell headings from body from captions at a glance?
- Are font sizes too close together? (14px, 15px, 16px = muddy hierarchy)
- Are weight contrasts strong enough? (Medium vs Regular is barely visible)
3. **Sizing & scale**:
- Is there a consistent type scale, or are sizes arbitrary?
- Does body text meet minimum readability? (16px+)
- Is the sizing strategy appropriate for the context? (Fixed `rem` scales for app UIs; fluid `clamp()` for marketing/content page headings)
4. **Readability**:
- Are line lengths comfortable? (45-75 characters ideal)
- Is line-height appropriate for the font and context?
- Is there enough contrast between text and background?
5. **Consistency**:
- Are the same elements styled the same way throughout?
- Are font weights used consistently? (Not bold in one section, semibold in another for the same role)
- Is letter-spacing intentional or default everywhere?
**CRITICAL**: The goal isn't to make text "fancier" — it's to make it clearer, more readable, and more intentional. Good typography is invisible; bad typography is distracting.
## Plan Typography Improvements
Consult the [typography reference](reference/typography.md) from the frontend-design skill for detailed guidance on scales, pairing, and loading strategies.
Create a systematic plan:
- **Font selection**: Do fonts need replacing? What fits the brand/context?
- **Type scale**: Establish a modular scale (e.g., 1.25 ratio) with clear hierarchy
- **Weight strategy**: Which weights serve which roles? (Regular for body, Semibold for labels, Bold for headings — or whatever fits)
- **Spacing**: Line-heights, letter-spacing, and margins between typographic elements
## Improve Typography Systematically
### Font Selection
If fonts need replacing:
- Choose fonts that reflect the brand personality
- Pair with genuine contrast (serif + sans, geometric + humanist) — or use a single family in multiple weights
- Ensure web font loading doesn't cause layout shift (`font-display: swap`, metric-matched fallbacks)
### Establish Hierarchy
Build a clear type scale:
- **5 sizes cover most needs**: caption, secondary, body, subheading, heading
- **Use a consistent ratio** between levels (1.25, 1.333, or 1.5)
- **Combine dimensions**: Size + weight + color + space for strong hierarchy — don't rely on size alone
- **App UIs**: Use a fixed `rem`-based type scale, optionally adjusted at 1-2 breakpoints. Fluid sizing undermines the spatial predictability that dense, container-based layouts need
- **Marketing / content pages**: Use fluid sizing via `clamp(min, preferred, max)` for headings and display text. Keep body text fixed
### Fix Readability
- Set `max-width` on text containers using `ch` units (`max-width: 65ch`)
- Adjust line-height per context: tighter for headings (1.1-1.2), looser for body (1.5-1.7)
- Increase line-height slightly for light-on-dark text
- Ensure body text is at least 16px / 1rem
### Refine Details
- Use `tabular-nums` for data tables and numbers that should align
- Apply proper `letter-spacing`: slightly open for small caps and uppercase, default or tight for large display text
- Use semantic token names (`--text-body`, `--text-heading`), not value names (`--font-16`)
- Set `font-kerning: normal` and consider OpenType features where appropriate
### Weight Consistency
- Define clear roles for each weight and stick to them
- Don't use more than 3-4 weights (Regular, Medium, Semibold, Bold is plenty)
- Load only the weights you actually use (each weight adds to page load)
**NEVER**:
- Use more than 2-3 font families
- Pick sizes arbitrarily — commit to a scale
- Set body text below 16px
- Use decorative/display fonts for body text
- Disable browser zoom (`user-scalable=no`)
- Use `px` for font sizes — use `rem` to respect user settings
- Default to Inter/Roboto/Open Sans when personality matters
- Pair fonts that are similar but not identical (two geometric sans-serifs)
## Verify Typography Improvements
- **Hierarchy**: Can you identify heading vs body vs caption instantly?
- **Readability**: Is body text comfortable to read in long passages?
- **Consistency**: Are same-role elements styled identically throughout?
- **Personality**: Does the typography reflect the brand?
- **Performance**: Are web fonts loading efficiently without layout shift?
- **Accessibility**: Does text meet WCAG contrast ratios? Is it zoomable to 200%?
Remember: Typography is the foundation of interface design — it carries the majority of information. Getting it right is the highest-leverage improvement you can make.
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/adapt
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/animate
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/arrange
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/audit
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/bolder
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/clarify
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/colorize
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/critique
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/delight
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/distill
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/extract
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/frontend-design
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/harden
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/normalize
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/onboard
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/optimize
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/overdrive
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/polish
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/quieter
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/teach-impeccable
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/typeset
-24
View File
@@ -1,24 +0,0 @@
# Copilot Instructions
## Design Context
### Users
The primary users are tech-savvy individuals (including IT professionals and dedicated enthusiasts) who need to migrate their Bose SoundTouch speakers from the Bose Cloud to a local service. While the initial setup requires technical knowledge (SSH, network config), the ultimate goal is a "set and forget" solution that provides a seamless experience for everyday use.
### Brand Personality
* **Voice & Tone:** Open, Transparent, Technical.
* **3-word personality:** Reliable, Precise, Empowering.
* **Emotional Goal:** Confidence. The interface should make users feel that their hardware is safe and will continue to work indefinitely.
### Aesthetic Direction
* **Visual Tone:** "Pro-consumer" but clean. It should feel like a high-quality utility rather than a marketing site.
* **References:** Bose (clean typography, generous whitespace, focused interactions).
* **Anti-references:** "AI Slop" (generic, cluttered, over-saturated designs).
* **Theme:** Light mode by default. Dark mode is a nice-to-have but secondary.
### Design Principles
1. **Clarity over Flash:** Prioritize clear instructions and immediate feedback. The interface exists to solve a technical problem, not to sell a product.
2. **Confidence-Building Interactions:** Every action (especially destructive ones like migration) must have clear confirmations and status indicators (e.g., ✅ Success/❌ Error).
3. **Utility with Polish:** Maintain a technical feel (using code blocks and data tables where necessary) but elevate them with consistent spacing, refined typography, and subtle visual cues.
4. **Future-Proof Consistency:** Design components and patterns that can scale to a consumer-oriented mobile or desktop application.
5. **Component-Based Scalability:** Move away from inline styles toward a clean, token-based CSS approach to improve maintainability and responsiveness.
+9
View File
@@ -28,6 +28,15 @@
},
{
"pattern": "^\\.\\./images/(dashboard-home|account-creation|account-dashboard|usb-remote-services|device-discovery|device-registration|account-migration|migration-setup|migration-progress|migration-health|migration-complete|backup-setup)\\.png$"
},
{
"pattern": "https://www.contributor-covenant.org/version/2/0/code_of_conduct.html"
},
{
"pattern": "https://www.apkmirror.com/apk/bose-corporation/bose-soundtouch/"
},
{
"pattern": "https://apkpure.com/bose-soundtouch/com.bose.soundtouch"
}
],
"replacementPatterns": [
+10 -6
View File
@@ -43,8 +43,14 @@ jobs:
- name: Run tests
run: go test -v -race -coverprofile=coverage.out ./...
- name: Build service
run: make build-service
- name: Run HTTP client integration tests
run: make test-http-client
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v6
with:
file: ./coverage.out
flags: unittests
@@ -141,11 +147,9 @@ jobs:
uses: actions/checkout@v6
- name: Check documentation links
uses: gaurav-nelson/github-action-markdown-link-check@v1
with:
use-quiet-mode: "yes"
use-verbose-mode: "yes"
config-file: ".github/markdown-link-check.json"
run: |
npm install -g markdown-link-check
find . -name "*.md" -not -path "./tests/*" -not -path "./node_modules/*" -print0 | xargs -0 -n1 markdown-link-check -q -v -c .github/markdown-link-check.json
- name: Warn on pending images
run: |
+2 -2
View File
@@ -22,7 +22,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Pages
uses: actions/configure-pages@v5
uses: actions/configure-pages@v6
- name: Build with Jekyll
uses: actions/jekyll-build-pages@v1
with:
@@ -34,4 +34,4 @@ jobs:
path: '_site'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v5
-59
View File
@@ -1,59 +0,0 @@
## Design Context
### Users
The primary users are tech-savvy individuals (including IT professionals and dedicated enthusiasts) who need to migrate their Bose SoundTouch speakers from the Bose Cloud to a local service. While the initial setup requires technical knowledge (SSH, network config), the ultimate goal is a "set and forget" solution that provides a seamless experience for everyday use.
### Brand Personality
* **Voice & Tone:** Open, Transparent, Technical.
* **3-word personality:** Reliable, Precise, Empowering.
* **Emotional Goal:** Confidence. The interface should make users feel that their hardware is safe and will continue to work indefinitely.
### Aesthetic Direction
* **Visual Tone:** "Pro-consumer" but clean. It should feel like a high-quality utility rather than a marketing site.
* **References:** Bose (clean typography, generous whitespace, focused interactions).
* **Anti-references:** "AI Slop" (generic, cluttered, over-saturated designs).
* **Theme:** Light mode by default. Dark mode is a nice-to-have but secondary.
### Design Principles
1. **Clarity over Flash:** Prioritize clear instructions and immediate feedback. The interface exists to solve a technical problem, not to sell a product.
2. **Confidence-Building Interactions:** Every action (especially destructive ones like migration) must have clear confirmations and status indicators (e.g., ✅ Success/❌ Error).
3. **Utility with Polish:** Maintain a technical feel (using code blocks and data tables where necessary) but elevate them with consistent spacing, refined typography, and subtle visual cues.
4. **Future-Proof Consistency:** Design components and patterns that can scale to a consumer-oriented mobile or desktop application.
5. **Component-Based Scalability:** Move away from inline styles toward a clean, token-based CSS approach to improve maintainability and responsiveness.
## Component Library (Standard Patterns)
### 1. Callouts & Status Indicators
Use for feedback, warnings, and instructions.
* **Base Class:** `.callout` or `.status`
* **Variants:** `.callout-info` (blue), `.callout-success` (green), `.callout-warning` (yellow), `.callout-error` (red)
* **Usage:**
```html
<div class="callout callout-info">
<strong>Title:</strong> Your message here.
</div>
```
### 2. Buttons
* **Primary:** `.btn-primary` (Blue/Accent) - Main action.
* **Secondary:** `.btn-secondary` (Grey) - Neutral action.
* **Success:** `.btn-success` (Green) - Positive/Confirm action.
* **Warning:** `.btn-warning` (Yellow) - Revert/Caution action.
* **Danger:** `.btn-danger` (Red) - Destructive action.
* **Spotify:** `.btn-spotify` (Spotify Green) - Spotify-specific actions.
### 3. Data Containers
* **Cards:** `.card` or `.summary-box` - Grouped information.
* **Consoles:** `.console-box` or `pre` - Monospace output, logs, or code.
* **Viewers:** `.viewer-box` - Dark-themed container for detailed technical analysis (JSON/XML).
### 4. Layout Utilities
* **Flexbox:** `.flex-row`, `.flex-between`, `.flex-grow`.
* **Spacing:** `.mt-{size}`, `.mb-{size}`, `.ml-{size}`, `.p-{size}`.
* Sizes: `xs` (4px), `sm` (8px), `md` (16px), `lg` (24px), `xl` (32px).
* **Visibility:** `.hidden`, `.visible`, `.flex-visible`.
### 5. Typography
* **Subtitle:** `.subtitle` - Small italicized text below headings.
* **Help Text:** `.help-text` or `.help-text-sm` - Descriptive text for inputs or status.
* **Badges:** `.badge` with variants `.badge-info`, `.badge-success`, etc.
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/adapt
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/animate
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/arrange
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/audit
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/bolder
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/clarify
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/colorize
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/critique
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/delight
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/distill
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/extract
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/frontend-design
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/harden
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/normalize
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/onboard
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/optimize
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/overdrive
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/polish
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/quieter
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/teach-impeccable
-1
View File
@@ -1 +0,0 @@
../../.agents/skills/typeset
+47 -1
View File
@@ -103,7 +103,53 @@ test-coverage:
$(GOCMD) tool cover -html=coverage.out -o coverage.html
@echo "Coverage report generated: coverage.html"
check: fmt vet test
check: fmt vet test test-http-client
test-http-client:
@echo "Starting services with docker compose..."
@docker compose -f docker-compose.yml -f docker-compose.ci.yml up -d --build
@echo "Waiting for services to start..."
@sleep 10
@echo "Running .http tests..."
@docker run --rm --network soundtouch-test-net \
-v "$(PWD)/tests/integration/http-client:/workdir" \
jetbrains/intellij-http-client:2026.1 \
--env-file /workdir/http-client.env.json \
--env ci \
/workdir/spotify_registration.http \
/workdir/create_account.http \
/workdir/register_device.http \
/workdir/spotify_full_flow.http \
/workdir/customer_support.http \
/workdir/power_on.http \
/workdir/get_bmx_services.http \
/workdir/get_sourceproviders.http \
/workdir/get_software_update.http \
/workdir/get_soundtouch_updates.http \
/workdir/get_streaming_token.http \
/workdir/post_oauth_token.http \
/workdir/get_provider_settings.http \
/workdir/tunein_playback_station.http \
/workdir/set_preset_6.http \
/workdir/get_presets.http \
/workdir/delete_preset_6.http \
/workdir/set_preset_5.http \
/workdir/post_recent.http \
/workdir/get_recents.http \
/workdir/get_account_presets.http \
/workdir/get_account_devices.http \
/workdir/get_account_sources.http \
/workdir/get_api_versions.http \
/workdir/post_musicprovider_is_eligible.http \
/workdir/get_full_account.http \
/workdir/get_group.http \
/workdir/unregister_device.http \
--report; \
EXIT_CODE=$$?; \
docker compose -f docker-compose.yml -f docker-compose.ci.yml logs soundtouch-service; \
docker compose -f docker-compose.yml -f docker-compose.ci.yml logs spotify-mock; \
docker compose -f docker-compose.yml -f docker-compose.ci.yml down; \
exit $$EXIT_CODE
fmt:
@echo "Formatting code..."
+1 -1
View File
@@ -480,7 +480,7 @@ SoundTouch is a trademark of Bose Corporation.
This Go library will continue to work as it uses the local Web API for direct device control, which is unaffected by the cloud service discontinuation. The local preset management functionality implemented in this library (discovered through the [SoundTouch Plus Wiki](https://github.com/thlucas1/homeassistantcomponent_soundtouchplus/wiki/SoundTouch-WebServices-API)) provides an alternative to the cloud-based preset features that will be discontinued.
**Community Alternatives**: See the [Related Projects](#related-projects) section below for additional tools like SoundCork that provide cloud service alternatives and the SoundTouch Plus project that offers comprehensive Home Assistant integration.
**Community Alternatives**: See the [Related Projects & Credits](#related-projects--credits) section below for additional tools like SoundCork that provide cloud service alternatives and the SoundTouch Plus project that offers comprehensive Home Assistant integration.
## Related Projects & Credits
+23
View File
@@ -0,0 +1,23 @@
// Package main provides a mock Spotify server for testing purposes.
package main
import (
"flag"
"fmt"
"log"
"net/http"
"github.com/gesellix/bose-soundtouch/pkg/testutils/spotify"
)
func main() {
port := flag.Int("port", 8080, "Port to listen on")
flag.Parse()
log.Printf("Starting mock Spotify server on port %d", *port)
if err := http.ListenAndServe(fmt.Sprintf(":%d", *port), spotify.NewSpotifyHandler()); err != nil {
log.Fatal(err)
}
}
+67
View File
@@ -652,6 +652,73 @@ func listMusicServiceAccounts(c *cli.Context) error {
return nil
}
// pairDevice triggers the Stockholm registration flow via WebSocket
func pairDevice(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
accountID := c.String("id")
token := c.String("token")
PrintDeviceHeader("Pairing device with Marge account", clientConfig.Host, clientConfig.Port)
fmt.Printf(" Account ID: %s\n", accountID)
// We need a WebSocket client for this
ws := client.NewWebSocketClient(nil)
err = ws.Connect()
if err != nil {
return fmt.Errorf("failed to connect to device WebSocket: %w", err)
}
defer func() { _ = ws.Disconnect() }()
err = ws.PairWithAccount(accountID, token)
if err != nil {
return fmt.Errorf("failed to send pairing request: %w", err)
}
PrintSuccess("Pairing request sent successfully")
fmt.Println("💡 The device will now register itself with the cloud service.")
return nil
}
// unpairDevice triggers the Stockholm unregistration flow via WebSocket
func unpairDevice(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
PrintDeviceHeader("Unpairing device from Marge account", clientConfig.Host, clientConfig.Port)
// We need a WebSocket client for this
ws := client.NewWebSocketClient(nil)
err = ws.Connect()
if err != nil {
return fmt.Errorf("failed to connect to device WebSocket: %w", err)
}
defer func() { _ = ws.Disconnect() }()
err = ws.UnPairFromAccount()
if err != nil {
return fmt.Errorf("failed to send unpairing request: %w", err)
}
PrintSuccess("Unpairing request sent successfully")
return nil
}
// getServiceDisplayName returns a user-friendly display name for a service
func getServiceDisplayName(source string) string {
switch source {
+2 -2
View File
@@ -196,7 +196,7 @@ var httpClient = &http.Client{
}
func fetchTuneInMetadata(url string) (*Metadata, error) {
if !strings.Contains(url, "tunein.com/radio/") {
if !strings.Contains(url, "tunein.com/radio/") && !strings.Contains(url, "127.0.0.1") && !strings.Contains(url, "localhost") {
return nil, fmt.Errorf("url is not a TuneIn radio URL")
}
@@ -256,7 +256,7 @@ func fetchTuneInMetadata(url string) (*Metadata, error) {
}
func fetchSpotifyMetadata(url string) (*Metadata, error) {
if !strings.Contains(url, "open.spotify.com/") {
if !strings.Contains(url, "open.spotify.com/") && !strings.Contains(url, "127.0.0.1") && !strings.Contains(url, "localhost") {
return nil, fmt.Errorf("url is not a Spotify URL")
}
+4 -4
View File
@@ -7,7 +7,7 @@ import (
)
func TestFetchTuneInMetadata(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
html := `
<!doctype html>
<html>
@@ -30,7 +30,7 @@ func TestFetchTuneInMetadata(t *testing.T) {
defer func() { httpClient = oldClient }()
metadata, err := fetchTuneInMetadata("https://tunein.com/radio/WDR-2-Rheinland-1004-s213886/")
metadata, err := fetchTuneInMetadata(ts.URL + "/radio/WDR-2-Rheinland-1004-s213886/")
if err != nil {
t.Fatalf("fetchTuneInMetadata() error = %v", err)
}
@@ -162,7 +162,7 @@ func TestResolveLocationSpotify(t *testing.T) {
}
func TestFetchSpotifyMetadata(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
html := `
<!doctype html>
<html>
@@ -185,7 +185,7 @@ func TestFetchSpotifyMetadata(t *testing.T) {
defer func() { httpClient = oldClient }()
metadata, err := fetchSpotifyMetadata("https://open.spotify.com/album/7F50uh7oGitmAEScRKV6pD")
metadata, err := fetchSpotifyMetadata(ts.URL + "/album/7F50uh7oGitmAEScRKV6pD")
if err != nil {
t.Fatalf("fetchSpotifyMetadata() error = %v", err)
}
+24
View File
@@ -2038,6 +2038,30 @@ func main() {
},
},
},
{
Name: "pair",
Usage: "Pair the device with a Marge cloud account (Stockholm registration)",
Action: pairDevice,
Before: RequireHost,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "id",
Usage: "Marge account ID (e.g., 1234567)",
Required: true,
},
&cli.StringFlag{
Name: "token",
Usage: "User authorization token",
Required: true,
},
},
},
{
Name: "unpair",
Usage: "Unpair the device from its Marge cloud account",
Action: unpairDevice,
Before: RequireHost,
},
},
},
// Token commands
+161 -44
View File
@@ -55,6 +55,53 @@ func updateBuildInfo() {
}
}
func initializeDefaultSources(ds *datastore.DataStore) {
// Ensure default sources exist for all known devices on startup
allDevices, _ := ds.ListAllDevices()
for i := range allDevices {
dev := &allDevices[i]
if sources, errGet := ds.GetConfiguredSources(dev.AccountID, dev.DeviceID); errGet == nil {
log.Printf("Initializing default Sources.xml for existing device %s", dev.DeviceID)
// Find default sources and merge them if missing or outdated tokens
defaults := ds.GetDefaultSources()
modified := false
for i := range defaults {
def := defaults[i]
found := false
for j := range sources {
if sources[j].SourceKeyType == def.SourceKeyType {
found = true
if sources[j].Secret == "" && def.Secret != "" {
log.Printf("Initializing missing token for source %s on device %s", def.SourceKeyType, dev.DeviceID)
sources[j].Secret = def.Secret
sources[j].SecretType = def.SecretType
modified = true
}
break
}
}
if !found {
log.Printf("Adding missing default source %s to device %s", def.SourceKeyType, dev.DeviceID)
sources = append(sources, def)
modified = true
}
}
if modified {
if errSave := ds.SaveConfiguredSources(dev.AccountID, dev.DeviceID, sources); errSave != nil {
log.Printf("Failed to save updated sources for %s: %v", dev.DeviceID, errSave)
}
}
}
}
}
func main() {
updateBuildInfo()
@@ -162,6 +209,16 @@ func main() {
Value: "ueberboese-login://spotify",
EnvVars: []string{"SPOTIFY_REDIRECT_URI"},
},
&cli.StringFlag{
Name: "spotify-token-url",
Usage: "Spotify OAuth token URL (for testing)",
EnvVars: []string{"SPOTIFY_TOKEN_URL"},
},
&cli.StringFlag{
Name: "spotify-api-base",
Usage: "Spotify API base URL (for testing)",
EnvVars: []string{"SPOTIFY_API_BASE"},
},
&cli.StringFlag{
Name: "mgmt-username",
Usage: "Management API username for HTTP Basic Auth",
@@ -258,6 +315,14 @@ func main() {
config.spotifyRedirectURI,
config.dataDir,
)
if config.spotifyTokenURL != "" || config.spotifyAPIBase != "" {
spotifyService.SetEndpoints(config.spotifyTokenURL, config.spotifyAPIBase)
}
if err := spotifyService.Load(); err != nil {
log.Printf("[Spotify] Failed to load accounts: %v", err)
}
server.SetSpotifyService(spotifyService)
clientIDPrefix := config.spotifyClientID
@@ -321,6 +386,8 @@ func main() {
server.SetRecorder(recorder)
initializeDefaultSources(ds)
tlsConfig, err := cm.GetServerTLSConfig(config.domains)
if err != nil {
log.Printf("Warning: Failed to setup TLS: %v", err)
@@ -386,6 +453,8 @@ type serviceConfig struct {
spotifyClientID string
spotifyClientSecret string
spotifyRedirectURI string
spotifyTokenURL string
spotifyAPIBase string
mgmtUsername string
mgmtPassword string
migrationEnabled bool
@@ -450,6 +519,8 @@ func loadConfig(c *cli.Context) serviceConfig {
spotifyClientID := c.String("spotify-client-id")
spotifyClientSecret := c.String("spotify-client-secret")
spotifyRedirectURI := c.String("spotify-redirect-uri")
spotifyTokenURL := c.String("spotify-token-url")
spotifyAPIBase := c.String("spotify-api-base")
mgmtUsername := c.String("mgmt-username")
mgmtPassword := c.String("mgmt-password")
mirrorEnabled := c.Bool("mirror-enabled")
@@ -483,6 +554,8 @@ func loadConfig(c *cli.Context) serviceConfig {
spotifyClientID: spotifyClientID,
spotifyClientSecret: spotifyClientSecret,
spotifyRedirectURI: spotifyRedirectURI,
spotifyTokenURL: spotifyTokenURL,
spotifyAPIBase: spotifyAPIBase,
mgmtUsername: mgmtUsername,
mgmtPassword: mgmtPassword,
migrationEnabled: migrationEnabled,
@@ -661,70 +734,109 @@ func setupRouter(server *handlers.Server) *chi.Mux {
r.Route("/bmx", func(r chi.Router) {
r.Get("/registry/v1/services", server.HandleBMXRegistry)
r.Get("/tunein/v1/playback/station/{stationID}", server.HandleTuneInPlayback)
r.Get("/tunein/v1/playback/episodes/{podcastID}", server.HandleTuneInPodcastInfo)
r.Get("/tunein/v1/playback/episode/{podcastID}", server.HandleTuneInPlaybackPodcast)
r.Get("/registry/v1/servicesAvailability", server.HandleBMXServicesAvailability)
r.Route("/tunein", func(r chi.Router) {
r.Get("/v1/playback/station/{stationID}", server.HandleTuneInPlayback)
r.Get("/v1/playback/episodes/{podcastID}", server.HandleTuneInPodcastInfo)
r.Get("/v1/playback/episode/{podcastID}", server.HandleTuneInPlaybackPodcast)
r.Post("/v1/token", server.HandleTuneInToken)
r.Post("/v1/report", server.HandleTuneInReport)
r.Get("/v1/navigate", server.HandleTuneInNavigate)
r.Get("/v1/navigate/*", server.HandleTuneInNavigate)
r.Get("/v1/search", server.HandleTuneInSearch)
})
r.Post("/orion/v1/playback/station/{data}", server.HandleOrionPlayback)
})
// Legacy or direct domain calls without /bmx prefix
r.Get("/registry/v1/services", server.HandleBMXRegistry)
r.Get("/tunein/v1/playback/station/{stationID}", server.HandleTuneInPlayback)
r.Get("/tunein/v1/playback/episodes/{podcastID}", server.HandleTuneInPodcastInfo)
r.Get("/tunein/v1/playback/episode/{podcastID}", server.HandleTuneInPlaybackPodcast)
r.Post("/orion/v1/playback/station/{data}", server.HandleOrionPlayback)
r.Get("/custom/v1/playback/{encodedURL}", server.HandleCustomPlayback)
streamingRoutes := func(r chi.Router) {
r.Route("/streaming", func(r chi.Router) {
r.Get("/sourceproviders", server.HandleMargeSourceProviders)
r.Get("/account/{account}/device/{device}/recent", server.HandleMargeRecents)
r.Post("/account/{account}/device/{device}/recent", server.HandleMargeAddRecent)
r.Get("/account/{account}/device/{device}/presets", server.HandleMargePresets)
r.Post("/account/{account}/device/{device}/presets/{presetNumber}", server.HandleMargeUpdatePreset)
r.Post("/support/power_on", server.HandleMargePowerOn)
r.Get("/account/{account}/provider_settings", server.HandleMargeProviderSettings)
r.Post("/account", server.HandleMargeCreateAccount)
r.Post("/account/login", server.HandleMargeLogin)
r.Post("/account/{account}/source", server.HandleMargeAddSource)
r.Route("/account/{account}", func(r chi.Router) {
r.Get("/emailaddress", server.HandleMargeGetEmailAddress)
r.Get("/full", server.HandleMargeAccountFull)
r.Get("/sources", server.HandleMargeAccountSources)
r.Get("/devices", server.HandleMargeAccountDevices)
r.Get("/presets", server.HandleMargeAccountPresets)
r.Get("/presets/all", server.HandleMargeAccountPresets)
r.Get("/provider_settings", server.HandleMargeProviderSettings)
r.Route("/device", func(r chi.Router) {
r.Post("/", server.HandleMargeAddDevice)
r.Post("/{device}", server.HandleMargeAddDevice)
})
r.Route("/device/{device}", func(r chi.Router) {
r.Get("/presets", server.HandleMargePresets)
r.Post("/presets/{presetNumber}", server.HandleMargeUpdatePreset)
r.Put("/preset/{presetNumber}", server.HandleMargeUpdatePreset)
r.Delete("/preset/{presetNumber}", server.HandleMargeRemovePreset)
r.Get("/recent", server.HandleMargeRecents)
r.Get("/recents", server.HandleMargeRecents)
r.Post("/recent", server.HandleMargeAddRecent)
r.Get("/group", server.HandleMargeDeviceGroup)
r.Get("/group/", server.HandleMargeDeviceGroup)
r.Get("/group/server", server.HandleMargeDeviceGroupServer)
r.Get("/group/member", server.HandleMargeDeviceGroupMember)
})
r.Delete("/device/{device}", server.HandleMargeRemoveDevice)
})
r.Get("/device/{device}/streaming_token", server.HandleMargeStreamingToken)
r.Post("/support/customersupport", server.HandleMargeCustomerSupport)
r.Get("/device_setting/account/{account}/device/{device}/device_settings", server.HandleMargeGetDeviceSettings)
r.Get("/account/{account}/device/{device}/group", server.HandleMargeDeviceGroup)
r.Get("/account/{account}/device/{device}/group/", server.HandleMargeDeviceGroup)
r.Get("/account/{account}/device/{device}/group/server", server.HandleMargeDeviceGroupServer)
r.Get("/account/{account}/device/{device}/group/member", server.HandleMargeDeviceGroupMember)
r.Post("/device_setting/account/{account}/device/{device}/device_settings", server.HandleMargeUpdateDeviceSettings)
r.Get("/account/{account}/emailaddress", server.HandleMargeGetEmailAddress)
r.Get("/account/{account}/full", server.HandleMargeAccountFull)
r.Get("/software/update/account/{account}", server.HandleMargeSoftwareUpdate)
r.Route("/support", func(r chi.Router) {
r.Post("/power_on", server.HandleMargePowerOn)
r.Post("/customersupport", server.HandleMargeCustomerSupport)
})
r.Route("/stats", func(r chi.Router) {
r.Post("/usage", server.HandleUsageStats)
r.Post("/error", server.HandleErrorStats)
})
}
accountsRoutes := func(r chi.Router) {
r.Get("/{account}/full", server.HandleMargeAccountFull)
r.Get("/{account}/devices/{device}/presets", server.HandleMargePresets)
r.Post("/{account}/devices/{device}/presets/{presetNumber}", server.HandleMargeUpdatePreset)
r.Get("/{account}/devices/{device}/recents", server.HandleMargeRecents)
r.Post("/{account}/devices/{device}/recents", server.HandleMargeAddRecent)
r.Post("/{account}/devices", server.HandleMargeAddDevice)
r.Delete("/{account}/devices/{device}", server.HandleMargeRemoveDevice)
r.Get("/{account}/devices/{device}/group", server.HandleMargeDeviceGroup)
r.Get("/{account}/devices/{device}/group/", server.HandleMargeDeviceGroup)
r.Get("/{account}/devices/{device}/group/server", server.HandleMargeDeviceGroupServer)
r.Get("/{account}/devices/{device}/group/member", server.HandleMargeDeviceGroupMember)
}
r.Route("/music", func(r chi.Router) {
r.Route("/musicprovider/{providerID}", func(r chi.Router) {
r.Post("/is_eligible", server.HandleMusicProviderIsEligible)
})
})
r.Route("/marge", func(r chi.Router) {
r.Route("/streaming", streamingRoutes)
r.Route("/accounts", accountsRoutes)
r.Get("/resources/api_versions.xml", server.HandleMargeAPIVersions)
})
r.Get("/updates/soundtouch", server.HandleMargeSoftwareUpdate)
r.Route("/accounts", func(r chi.Router) {
r.Route("/{account}", func(r chi.Router) {
r.Get("/full", server.HandleMargeAccountFull)
r.Get("/sources", server.HandleMargeAccountSources)
r.Get("/devices", server.HandleMargeAccountDevices)
r.Post("/devices", server.HandleMargeAddDevice)
r.Delete("/devices/{device}", server.HandleMargeRemoveDevice)
r.Get("/devices/{device}/group", server.HandleMargeDeviceGroup)
r.Get("/devices/{device}/group/", server.HandleMargeDeviceGroup)
r.Get("/devices/{device}/group/server", server.HandleMargeDeviceGroupServer)
r.Get("/devices/{device}/group/member", server.HandleMargeDeviceGroupMember)
r.Get("/devices/{device}/presets", server.HandleMargePresets)
r.Get("/devices/{device}/recents", server.HandleMargeRecents)
r.Post("/devices/{device}/presets/{presetNumber}", server.HandleMargeUpdatePreset)
r.Post("/devices/{device}/recents", server.HandleMargeAddRecent)
})
})
// Legacy or direct domain calls without /marge prefix
r.Route("/streaming", streamingRoutes)
r.Route("/accounts", accountsRoutes)
r.Get("/updates/soundtouch", server.HandleMargeSoftwareUpdate)
r.Route("/customer", func(r chi.Router) {
@@ -736,12 +848,17 @@ func setupRouter(server *handlers.Server) *chi.Mux {
r.Route("/oauth", func(r chi.Router) {
r.Post("/device/{deviceID}/music/musicprovider/{sourceID}/token/cs3", server.HandleBoseToken)
r.Post("/device/{deviceID}/music/musicprovider/{sourceID}/token", server.HandleBoseLegacyToken)
r.Post("/account/{account}/music/musicprovider/{sourceID}/token/cs", server.HandleBoseAccountToken)
r.HandleFunc("/*", server.HandleBoseProxy)
})
r.Route("/v1", func(r chi.Router) {
r.Post("/stapp/{deviceId}", server.HandleAppEvents)
r.Post("/scmudc/{deviceId}", server.HandleAppEvents)
// Return 405 Method Not Allowed as the upstream behavior also returns 405
r.Get("/blacklist/{deviceId}", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusMethodNotAllowed)
})
})
r.Route("/mgmt", func(r chi.Router) {
+104
View File
@@ -0,0 +1,104 @@
package main
import (
"fmt"
"net/http"
"os"
"reflect"
"runtime"
"sort"
"strings"
"testing"
"github.com/gesellix/bose-soundtouch/pkg/service/handlers"
"github.com/go-chi/chi/v5"
)
func TestPrintRoutes(t *testing.T) {
// Initialize a minimal server to get the router
server := handlers.NewServer(nil, nil, "http://localhost:8000", true, true, true)
r := setupRouter(server)
var routes []string
walkFunc := func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
route = strings.ReplaceAll(route, "/*/", "/")
handlerName := runtime.FuncForPC(reflect.ValueOf(handler).Pointer()).Name()
// Clean up the handler name (remove package path)
// For example, "github.com/gesellix/bose-soundtouch/cmd/soundtouch-service.setupRouter.func1"
// or "command-line-arguments.setupRouter.func1"
// or "main.setupRouter.func1"
parts := strings.Split(handlerName, "/")
if len(parts) > 0 {
handlerName = parts[len(parts)-1]
}
// Now we might have "soundtouch-service.setupRouter.func1"
// or "command-line-arguments.setupRouter.func1"
// or "main.setupRouter.func1"
// Let's remove the first part if it's a known varying package name
if idx := strings.Index(handlerName, "setupRouter"); idx != -1 {
handlerName = handlerName[idx:]
}
// In case it's not setupRouter but still has a package prefix
for {
dotIdx := strings.Index(handlerName, ".")
if dotIdx == -1 {
break
}
prefix := handlerName[:dotIdx]
if prefix == "main" || prefix == "command-line-arguments" || strings.Contains(prefix, "soundtouch-service") {
handlerName = handlerName[dotIdx+1:]
} else {
break
}
}
// Also remove any ".funcN" suffix if it's an anonymous function
if idx := strings.Index(handlerName, ".func"); idx != -1 {
handlerName = handlerName[:idx]
}
routes = append(routes, fmt.Sprintf("%-8s %-60s %s", method, route, handlerName))
return nil
}
if err := chi.Walk(r, walkFunc); err != nil {
t.Fatalf("Failed to walk routes: %v", err)
}
sort.Strings(routes)
output := strings.Join(routes, "\n") + "\n"
// Define snapshot path
snapshotPath := "testdata/router_routes.txt"
actualPath := "testdata/router_routes.actual.txt"
// Always write the current (actual) routes to a file
if err := os.WriteFile(actualPath, []byte(output), 0644); err != nil {
t.Fatalf("Failed to write actual routes: %v", err)
}
// Check if snapshot exists
if _, err := os.Stat(snapshotPath); os.IsNotExist(err) {
// Create testdata directory if it doesn't exist
if err := os.MkdirAll("testdata", 0755); err != nil {
t.Fatalf("Failed to create testdata directory: %v", err)
}
// Initial snapshot creation
if err := os.WriteFile(snapshotPath, []byte(output), 0644); err != nil {
t.Fatalf("Failed to write snapshot: %v", err)
}
t.Logf("Initial snapshot created at %s", snapshotPath)
return
}
// Read existing snapshot
existingOutput, err := os.ReadFile(snapshotPath)
if err != nil {
t.Fatalf("Failed to read snapshot: %v", err)
}
if string(existingOutput) != output {
t.Errorf("Router routes changed! Diff the snapshot at %s with %s", snapshotPath, actualPath)
}
}
@@ -0,0 +1 @@
*.actual.txt
+134
View File
@@ -0,0 +1,134 @@
CONNECT /oauth/* handlers.(*Server).HandleBoseProxy-fm
DELETE /accounts/{account}/devices/{device} handlers.(*Server).HandleMargeRemoveDevice-fm
DELETE /oauth/* handlers.(*Server).HandleBoseProxy-fm
DELETE /setup/devices/{deviceId} handlers.(*Server).HandleRemoveDevice-fm
DELETE /setup/dns-discoveries handlers.(*Server).HandleClearDNSDiscoveries-fm
DELETE /setup/interactions/sessions handlers.(*Server).HandleCleanupSessions-fm
DELETE /setup/interactions/sessions/{session} handlers.(*Server).HandleDeleteSession-fm
DELETE /setup/parity-mismatches handlers.(*Server).HandleClearParityMismatches-fm
DELETE /streaming/account/{account}/device/{device}/preset/{presetNumber} handlers.(*Server).HandleMargeRemovePreset-fm
GET / handlers.(*Server).HandleRoot-fm
GET /accounts/{account}/devices handlers.(*Server).HandleMargeAccountDevices-fm
GET /accounts/{account}/devices/{device}/group handlers.(*Server).HandleMargeDeviceGroup-fm
GET /accounts/{account}/devices/{device}/group/ handlers.(*Server).HandleMargeDeviceGroup-fm
GET /accounts/{account}/devices/{device}/group/member handlers.(*Server).HandleMargeDeviceGroupMember-fm
GET /accounts/{account}/devices/{device}/group/server handlers.(*Server).HandleMargeDeviceGroupServer-fm
GET /accounts/{account}/devices/{device}/presets handlers.(*Server).HandleMargePresets-fm
GET /accounts/{account}/devices/{device}/recents handlers.(*Server).HandleMargeRecents-fm
GET /accounts/{account}/full handlers.(*Server).HandleMargeAccountFull-fm
GET /accounts/{account}/sources handlers.(*Server).HandleMargeAccountSources-fm
GET /bmx/registry/v1/services handlers.(*Server).HandleBMXRegistry-fm
GET /bmx/registry/v1/servicesAvailability handlers.(*Server).HandleBMXServicesAvailability-fm
GET /bmx/tunein/v1/navigate handlers.(*Server).HandleTuneInNavigate-fm
GET /bmx/tunein/v1/navigate/* handlers.(*Server).HandleTuneInNavigate-fm
GET /bmx/tunein/v1/playback/episode/{podcastID} handlers.(*Server).HandleTuneInPlaybackPodcast-fm
GET /bmx/tunein/v1/playback/episodes/{podcastID} handlers.(*Server).HandleTuneInPodcastInfo-fm
GET /bmx/tunein/v1/playback/station/{stationID} handlers.(*Server).HandleTuneInPlayback-fm
GET /bmx/tunein/v1/search handlers.(*Server).HandleTuneInSearch-fm
GET /custom/v1/playback/{encodedURL} handlers.(*Server).HandleCustomPlayback-fm
GET /customer/account/{account} handlers.(*Server).HandleMargeAccountProfile-fm
GET /docs/* handlers.(*Server).HandleDocs-fm
GET /favicon.ico setupRouter
GET /health handlers.(*Server).HandleHealth-fm
GET /media/* handlers.(*Server).HandleMedia
GET /mgmt/accounts/ handlers.(*Server).HandleMgmtListAccounts-fm
GET /mgmt/accounts/{accountId} handlers.(*Server).HandleMgmtAccountDetails-fm
GET /mgmt/accounts/{accountId}/speakers handlers.(*Server).HandleMgmtListSpeakers-fm
GET /mgmt/devices/{deviceId}/events handlers.(*Server).HandleMgmtDeviceEvents-fm
GET /mgmt/spotify/accounts handlers.(*Server).HandleMgmtSpotifyAccounts-fm
GET /mgmt/spotify/callback handlers.(*Server).HandleMgmtSpotifyCallback-fm
GET /mgmt/spotify/token handlers.(*Server).HandleMgmtSpotifyToken-fm
GET /oauth/* handlers.(*Server).HandleBoseProxy-fm
GET /proxy/* handlers.(*Server).HandleProxyRequest-fm
GET /setup/ca.crt handlers.(*Server).HandleGetCACert-fm
GET /setup/devices handlers.(*Server).HandleListDiscoveredDevices-fm
GET /setup/devices/{deviceId}/events handlers.(*Server).HandleGetDeviceEvents-fm
GET /setup/discovery-status handlers.(*Server).HandleGetDiscoveryStatus-fm
GET /setup/dns-discoveries handlers.(*Server).HandleGetDNSDiscoveries-fm
GET /setup/dns-discoveries/download handlers.(*Server).HandleDownloadDNSDiscoveries-fm
GET /setup/info/{deviceId} handlers.(*Server).HandleGetDeviceInfo-fm
GET /setup/interaction-content handlers.(*Server).HandleGetInteractionContent-fm
GET /setup/interaction-stats handlers.(*Server).HandleGetInteractionStats-fm
GET /setup/interactions handlers.(*Server).HandleListInteractions-fm
GET /setup/interactions/sessions/{session}/download handlers.(*Server).HandleDownloadSession-fm
GET /setup/parity-mismatches handlers.(*Server).HandleListParityMismatches-fm
GET /setup/proxy-settings handlers.(*Server).HandleGetProxySettings-fm
GET /setup/settings handlers.(*Server).HandleGetSettings-fm
GET /setup/summary/{deviceId} handlers.(*Server).HandleGetMigrationSummary-fm
GET /setup/version handlers.(*Server).HandleGetVersionInfo-fm
GET /streaming/account/{account}/device/{device}/group handlers.(*Server).HandleMargeDeviceGroup-fm
GET /streaming/account/{account}/device/{device}/group/ handlers.(*Server).HandleMargeDeviceGroup-fm
GET /streaming/account/{account}/device/{device}/group/member handlers.(*Server).HandleMargeDeviceGroupMember-fm
GET /streaming/account/{account}/device/{device}/group/server handlers.(*Server).HandleMargeDeviceGroupServer-fm
GET /streaming/account/{account}/device/{device}/presets handlers.(*Server).HandleMargePresets-fm
GET /streaming/account/{account}/device/{device}/recent handlers.(*Server).HandleMargeRecents-fm
GET /streaming/account/{account}/device/{device}/recents handlers.(*Server).HandleMargeRecents-fm
GET /streaming/account/{account}/devices handlers.(*Server).HandleMargeAccountDevices-fm
GET /streaming/account/{account}/emailaddress handlers.(*Server).HandleMargeGetEmailAddress-fm
GET /streaming/account/{account}/full handlers.(*Server).HandleMargeAccountFull-fm
GET /streaming/account/{account}/presets handlers.(*Server).HandleMargeAccountPresets-fm
GET /streaming/account/{account}/presets/all handlers.(*Server).HandleMargeAccountPresets-fm
GET /streaming/account/{account}/provider_settings handlers.(*Server).HandleMargeProviderSettings-fm
GET /streaming/account/{account}/sources handlers.(*Server).HandleMargeAccountSources-fm
GET /streaming/device/{device}/streaming_token handlers.(*Server).HandleMargeStreamingToken-fm
GET /streaming/device_setting/account/{account}/device/{device}/device_settings handlers.(*Server).HandleMargeGetDeviceSettings-fm
GET /streaming/resources/api_versions.xml handlers.(*Server).HandleMargeAPIVersions-fm
GET /streaming/software/update/account/{account} handlers.(*Server).HandleMargeSoftwareUpdate-fm
GET /streaming/sourceproviders handlers.(*Server).HandleMargeSourceProviders-fm
GET /updates/soundtouch handlers.(*Server).HandleMargeSoftwareUpdate-fm
GET /v1/blacklist/{deviceId} setupRouter
GET /web/* setupRouter.(*Server).HandleWeb
HEAD /oauth/* handlers.(*Server).HandleBoseProxy-fm
OPTIONS /oauth/* handlers.(*Server).HandleBoseProxy-fm
PATCH /oauth/* handlers.(*Server).HandleBoseProxy-fm
POST /accounts/{account}/devices handlers.(*Server).HandleMargeAddDevice-fm
POST /accounts/{account}/devices/{device}/presets/{presetNumber} handlers.(*Server).HandleMargeUpdatePreset-fm
POST /accounts/{account}/devices/{device}/recents handlers.(*Server).HandleMargeAddRecent-fm
POST /bmx/orion/v1/playback/station/{data} handlers.(*Server).HandleOrionPlayback-fm
POST /bmx/tunein/v1/report handlers.(*Server).HandleTuneInReport-fm
POST /bmx/tunein/v1/token handlers.(*Server).HandleTuneInToken-fm
POST /customer/account/{account} handlers.(*Server).HandleMargeUpdateAccountProfile-fm
POST /customer/account/{account}/password handlers.(*Server).HandleMargeChangePassword-fm
POST /mgmt/accounts/{accountId}/language handlers.(*Server).HandleMgmtUpdateAccountLanguage-fm
POST /mgmt/accounts/{accountId}/provider-settings handlers.(*Server).HandleMgmtUpdateAccountProviderSetting-fm
POST /mgmt/spotify/confirm handlers.(*Server).HandleMgmtSpotifyConfirm-fm
POST /mgmt/spotify/entity handlers.(*Server).HandleMgmtSpotifyEntity-fm
POST /mgmt/spotify/init handlers.(*Server).HandleMgmtSpotifyInit-fm
POST /mgmt/spotify/prime handlers.(*Server).HandleMgmtPrimeDevice-fm
POST /oauth/* handlers.(*Server).HandleBoseProxy-fm
POST /oauth/account/{account}/music/musicprovider/{sourceID}/token/cs handlers.(*Server).HandleBoseAccountToken-fm
POST /oauth/device/{deviceID}/music/musicprovider/{sourceID}/token handlers.(*Server).HandleBoseLegacyToken-fm
POST /oauth/device/{deviceID}/music/musicprovider/{sourceID}/token/cs3 handlers.(*Server).HandleBoseToken-fm
POST /setup/backup/{deviceId} handlers.(*Server).HandleBackupConfig-fm
POST /setup/devices handlers.(*Server).HandleAddManualDevice-fm
POST /setup/discover handlers.(*Server).HandleTriggerDiscovery-fm
POST /setup/ensure-remote-services/{deviceId} handlers.(*Server).HandleEnsureRemoteServices-fm
POST /setup/migrate/{deviceId} handlers.(*Server).HandleMigrateDevice-fm
POST /setup/proxy-settings handlers.(*Server).HandleUpdateProxySettings-fm
POST /setup/reboot/{deviceId} handlers.(*Server).HandleRebootDevice-fm
POST /setup/remove-remote-services/{deviceId} handlers.(*Server).HandleRemoveRemoteServices-fm
POST /setup/revert/{deviceId} handlers.(*Server).HandleRevertMigration-fm
POST /setup/settings handlers.(*Server).HandleUpdateSettings-fm
POST /setup/sync/{deviceId} handlers.(*Server).HandleInitialSync-fm
POST /setup/test-connection/{deviceId} handlers.(*Server).HandleTestConnection-fm
POST /setup/test-dns/{deviceId} handlers.(*Server).HandleTestDNSRedirection-fm
POST /setup/test-hosts/{deviceId} handlers.(*Server).HandleTestHostsRedirection-fm
POST /setup/trust-ca/{deviceId} handlers.(*Server).HandleTrustCACert-fm
POST /streaming/account handlers.(*Server).HandleMargeCreateAccount-fm
POST /streaming/account/login handlers.(*Server).HandleMargeLogin-fm
POST /streaming/account/{account}/device/ handlers.(*Server).HandleMargeAddDevice-fm
POST /streaming/account/{account}/device/{device} handlers.(*Server).HandleMargeAddDevice-fm
POST /streaming/account/{account}/device/{device}/presets/{presetNumber} handlers.(*Server).HandleMargeUpdatePreset-fm
POST /streaming/account/{account}/device/{device}/recent handlers.(*Server).HandleMargeAddRecent-fm
POST /streaming/account/{account}/source handlers.(*Server).HandleMargeAddSource-fm
POST /streaming/device_setting/account/{account}/device/{device}/device_settings handlers.(*Server).HandleMargeUpdateDeviceSettings-fm
POST /streaming/music/musicprovider/{providerID}/is_eligible handlers.(*Server).HandleMusicProviderIsEligible-fm
POST /streaming/stats/error handlers.(*Server).HandleErrorStats-fm
POST /streaming/stats/usage handlers.(*Server).HandleUsageStats-fm
POST /streaming/support/customersupport handlers.(*Server).HandleMargeCustomerSupport-fm
POST /streaming/support/power_on handlers.(*Server).HandleMargePowerOn-fm
POST /v1/scmudc/{deviceId} handlers.(*Server).HandleAppEvents-fm
POST /v1/stapp/{deviceId} handlers.(*Server).HandleAppEvents-fm
PUT /oauth/* handlers.(*Server).HandleBoseProxy-fm
PUT /streaming/account/{account}/device/{device}/preset/{presetNumber} handlers.(*Server).HandleMargeUpdatePreset-fm
TRACE /oauth/* handlers.(*Server).HandleBoseProxy-fm
+10
View File
@@ -0,0 +1,10 @@
services:
soundtouch-service:
build: .
volumes:
- ./tests/integration/testdata:/app/data
environment:
- SPOTIFY_CLIENT_ID=mock-id
- SPOTIFY_CLIENT_SECRET=mock-secret
- SPOTIFY_TOKEN_URL=http://spotify-mock:8080/api/token
- SPOTIFY_API_BASE=http://spotify-mock:8080
+18
View File
@@ -8,6 +8,8 @@ services:
ports:
- "8000:8000"
- "8443:8443"
networks:
- soundtouch-test-net
environment:
- PORT=8000
- HTTPS_PORT=8443
@@ -35,6 +37,22 @@ services:
cpus: '0.25'
memory: 128M
spotify-mock:
image: golang:1.26.1-alpine
container_name: spotify-mock
working_dir: /app
volumes:
- .:/app
command: go run ./cmd/mock-spotify/main.go -port 8080
ports:
- "8081:8080"
networks:
- soundtouch-test-net
networks:
soundtouch-test-net:
name: soundtouch-test-net
volumes:
soundtouch-data:
# Named volumes are preferred in Swarm. For multi-node persistence,
+114
View File
@@ -0,0 +1,114 @@
# Bose SoundTouch Device Setup Flow
This document details the multi-step process required to fully set up a Bose SoundTouch device, as derived from the Stockholm firmware (`setup/js/`) analysis.
A complete setup flow involves a sequence of local (WebSocket) and cloud (HTTP) actions that move the device from a factory-reset state to a fully registered, functional system.
## 1. Local Coordination Stage (WebSocket)
Before a device can be controlled, it must be configured on the local network and named. These actions occur via a WebSocket connection to the device on port 8080.
### 1.1 Language Configuration (Optional)
If the device is in a factory-reset state, the UI typically ensures the device language matches the user's choice.
- **WebSocket Action**: `set_language`
- **Internal Logic**: `SetupWizard.js` handles this via `set_device_language`.
### 1.2 Network Configuration (WiFi)
Configures the device to connect to a specific wireless access point.
- **File Reference**: `setup/js/workflow_wifi_setup.js`
- **Logic**: Triggers a site survey, then sends SSID and credentials.
- **WebSocket Command**: `set_WIFI_OLED` or similar internal method calls to configure the network profile.
### 1.3 Device Naming (Rename Step)
Assigns a user-friendly name (e.g., "Living Room") to the device.
- **File Reference**: `setup/js/workflow_rename.js`
- **WebSocket Action**: `name`
- **XML Payload**:
```xml
<name>Living Room</name>
```
- **Implementation**: The `RenameDevices.do_rename_devices()` function sends this to the device. The device then updates its local name and mDNS/SSDP broadcasts.
## 2. Cloud Interaction Stage (HTTP)
The device needs to be linked to a Bose "Marge" account to enable cloud-based features and music services.
### 2.1 Account Creation (Registration)
If a user doesn't have an account, the setup client creates one.
- **File Reference**: `setup/js/workflow_marge.js`
- **Cloud Endpoint**: `POST https://streaming.bose.com/streaming/account`
- **Payload**: XML containing name, email, password, and country.
- **Content-Type**: `application/vnd.bose.customer-v1.0+xml`
### 2.2 Cloud Authentication (Login)
The setup client must obtain a valid `accountId` and `userAuthToken` to pair the device.
- **File Reference**: `setup/js/workflow_marge.js`
- **Cloud Endpoint**: `POST https://streaming.bose.com/streaming/account/login`
- **Payload**: XML containing username and password.
- **Content-Type**: `application/vnd.bose.streaming-v1.2+xml`
- **Result**: Returns a session token in the `Credentials` response header and the user's `account ID` in the XML body.
## 3. Registration Bridge (WebSocket to Cloud)
This is the final "pairing" step where the client tells the device which account it belongs to.
### 3.1 Device Registration (The "Pair" Step)
The client sends the user's credentials to the device, which then registers itself with the cloud.
- **File Reference**: `setup/js/workflow_add_devices.js`
- **WebSocket Action**: `setMargeAccount`
- **XML Payload**:
```xml
<PairDeviceWithAccount>
<accountId>12345</accountId>
<userAuthToken>jGwE... (truncated)</userAuthToken>
</PairDeviceWithAccount>
```
- **Device Reaction**: Upon receiving this, the device makes its own outbound HTTP POST to the Marge service:
`POST https://streaming.bose.com/{accountId}/devices`
## 4. Finalization
Once the registration is complete, the setup application (Stockholm) performs final cleanup. It's important to distinguish between **App State** (the Stockholm UI's persistent settings) and **Device State** (the physical speaker's configuration).
### 4.1 Exiting Setup Mode (App Settings)
The Stockholm app communicates with its "native container" (the WebView bridge on iOS/Android/Windows/macOS) using a `setData` command in **JSON format**. This is an internal message to the application's persistent storage, **not a network command sent to the physical speaker**.
This command tells the Stockholm app which page to load on startup, effectively marking the setup as complete in the UI.
- **Internal Command**: `setData`
- **Parameter**: `startupPage`
- **Normal Value**: `index.html` (Normal mode)
- **Setup Value**: `setup/index.html` (Setup mode)
**JSON Payload (Internal to Stockholm App)**:
```json
{
"method": "setData",
"params": {
"name": "startupPage",
"value": "index.html"
}
}
```
**Other Common Internal Parameters**:
- `changeStartupPage`: Set to `false` after a successful setup or update.
- `tipsEnabled`: Set to `false` to suppress the "Getting Started" tutorials.
- `promptUpdate`: Set to `true` if a firmware update was deferred during setup.
### 4.2 Device Finalization
The physical speaker considers the setup "done" once it successfully processes the `<PairDeviceWithAccount>` XML message and completes its own handshake with the Marge cloud. There is no specific "Finalize" XML command sent to the speaker; the successful registration is the signal.
The `SetupWizard.js` calls `single_device_setup_done()` to trigger the internal `setData` updates described above. If these are not saved in the app's local storage, the Stockholm UI may return to the setup flow on next launch, even if the speaker is already paired.
---
## Summary of Scriptable Requirements
To automate a device setup using a custom tool (like `soundtouch-cli`), you must perform the following:
1. **Configure WiFi**: (Assumed if device is reachable over IP).
2. **Set Name**: Send the `<name>` WebSocket message (XML) to update the device identity.
3. **Obtain Token**: Authenticate against the cloud service (Marge) via HTTP.
4. **Pair Device**: Send the `<PairDeviceWithAccount>` WebSocket message (XML) with the account ID and token.
**Note**: The JSON `setData` commands are only necessary if you are building/controlling a version of the Stockholm UI itself. They are not required to configure the physical hardware.
+66
View File
@@ -0,0 +1,66 @@
# Technical Proposal: External Service Provider Abstraction
This document outlines a strategy to refactor the SoundTouch Service's content handling into a modular provider-based system.
## 1. Problem Statement
Currently, content handling for BMX (Bose Media Exchange) services like TuneIn or RadioBrowser is deeply intertwined with the HTTP handlers and XML models. Adding a new content provider (e.g., Local Media, Podcast RSS) requires modifying several files and duplicating boilerplate code for HTTP requests and error handling.
## 2. Proposed Architecture
### 2.1 The Provider Interface
We define a generic `ContentProvider` interface that abstracts away the source-specific logic (API calls, data parsing).
```go
package provider
import "github.com/gesellix/bose-soundtouch/pkg/models"
type ContentProvider interface {
// ID returns the unique identifier for this provider (e.g. "RADIO_BROWSER")
ID() string
// Resolve returns playback details for a given content identifier
Resolve(id string) (*models.BmxPlaybackResponse, error)
// Search allows finding content within this provider
Search(query string) ([]models.ContentItem, error)
}
```
### 2.2 Provider Registry
A central registry in `soundtouch-service` manages the lifecycle and selection of providers.
```go
type Registry struct {
providers map[string]ContentProvider
}
func (r *Registry) Register(p ContentProvider) { ... }
func (r *Registry) Get(id string) ContentProvider { ... }
```
## 3. Implementation Plan
### 3.1 Phase 1: Modularize RadioBrowser
1. **Extract Logic**: Move current RadioBrowser logic from `bmx.go` into a new package `pkg/service/providers/radiobrowser`.
2. **Add Failover**: Implement the **API Failover** logic inspired by OpenCloudTouch.
- Maintain a list of active RadioBrowser mirrors (e.g., `de1.api.radio-browser.info`, `nl1.api.radio-browser.info`).
- Implement a round-robin or health-based selection strategy.
3. **Implements Interface**: Ensure the new package satisfies the `ContentProvider` interface.
### 3.2 Phase 2: Refactor BMX Handlers
- Update `HandleTuneInPlayback` and `HandleOrionPlayback` to use the registry.
- The handlers will look up the provider based on the request context or URL parameters and delegate the resolution.
### 3.3 Phase 3: Dynamic Service Advertising
- Modify `HandleBMXRegistry` to dynamically generate the `bmx_services.json` content based on the currently registered and enabled providers.
## 4. Benefits
- **Resilience**: Centralized error handling and failover strategies for all external APIs.
- **Extensibility**: New services can be added by simply implementing the interface and registering them at startup.
- **Testability**: Providers can be unit-tested in isolation without mocking the entire HTTP server stack.
- **Unified UI**: A future Web UI can query the registry to show available content sources and their statuses.
## 5. Next Steps
1. Refine the `ContentProvider` interface to include metadata (icons, user-friendly names).
2. Create a prototype for the `radiobrowser` provider with failover support.
+45
View File
@@ -0,0 +1,45 @@
# Parity Analysis: Bose-SoundTouch (Go) vs. OpenCloudTouch (Python)
This document provides a comparative analysis of the current Go implementation and the `scheilch/opencloudtouch` project, identifying functional gaps and potential improvements.
## 1. Core Architecture and Language
- **Bose-SoundTouch (Go)**: A high-performance, strongly typed backend with a CLI and background service. Focuses on full API coverage, parity testing, and robust hardware control (DSP, zones).
- **OpenCloudTouch (OCT)**: A modern full-stack application (FastAPI + React/TypeScript). Prioritizes user experience with a web-based setup wizard and a clean abstraction for internet radio.
## 2. Functional Comparison
| Feature | Bose-SoundTouch (Go) | OpenCloudTouch (Python) |
|:------------------------|:-----------------------------------------------------------|:------------------------------------------------------------------------|
| **Setup Experience** | CLI-driven or manual API calls for migration (SSH, XML). | Web-based **Setup Wizard** guides through SSH, backup, and redirection. |
| **Radio Support** | Static integration of **RadioBrowser** and TuneIn. | Dynamic **RadioBrowserAdapter** with automatic **API Failover**. |
| **Commercial Services** | Deep integration (Spotify priming, Pandora, Deezer, etc.). | Basic support, focus is on local content and radio. |
| **Hardware Control** | Extensive (Bass, Treble, Soundbar levels, Clock display). | Basic playback and zone controls. |
| **Cloud Emulation** | High-fidelity parity (mirroring, discrepancy logging). | Functional emulation for local preset/recent persistence. |
| **Notifications** | Built-in **TTS** and custom URL audio alerts. | Not a primary focus. |
## 3. Key Strengths of OpenCloudTouch
- **Guided Onboarding**: The setup wizard reduces the entry barrier for non-technical users significantly.
- **Resilient Radio**: The API failover for RadioBrowser ensures continuous service even if specific community-hosted API instances go offline.
- **Modern API Stack**: Uses OpenAPI and generated TypeScript types for a seamless frontend integration.
- **Provider Abstraction**: A cleaner internal separation between the "Bose World" (XML/BMX) and external content providers (RadioBrowser).
## 4. Suggested Improvements for Bose-SoundTouch
### A. Web-based Setup Wizard (High Priority)
- Implement a state-driven wizard in the `soundtouch-service` to handle:
- SSH activation (checking `/remote_services` via USB).
- Automated backup of speaker configuration.
- Verification of DNS/Hosts redirection.
- Expose this via a simple embedded Web UI (using Go's `embed` package).
### B. RadioBrowser Failover (Medium Priority)
- Adapt the failover logic from OCT:
- Periodically refresh the list of available RadioBrowser API servers.
- Implement a retry mechanism that switches servers on 5xx errors or timeouts.
### C. External Service Abstraction (Medium Priority)
- Refactor the hardcoded BMX logic into a more modular **Provider System** (see `EXTERNAL-SERVICES-ABSTRACTION.md`).
- This will allow easier addition of new sources (e.g., local DLNA, generic M3U playlists) without touching the core BMX handlers.
## 5. Summary
While our Go project provides the most complete technical coverage of SoundTouch hardware and commercial services, OpenCloudTouch sets a higher standard for **user onboarding** and **service resilience** for community-driven content. Integrating a setup wizard and a more robust radio backend would make our project significantly more accessible and reliable.
+44 -44
View File
@@ -21,7 +21,7 @@ This document describes the most important patterns for the Bose SoundTouch API
**Key Aspects:**
- **Native Builds**: Full API functionality for CLI and server
- **WASM Builds**: Browser-compatible subset functionality
- **WASM Builds**: Browser-compatible subset functionality
- **Cross-Platform**: Linux, macOS, Windows support
- **Embedded Assets**: Web UI directly embedded in binary
@@ -66,7 +66,7 @@ func (c *Client) GetNowPlaying() (*models.NowPlaying, error) {
return nil, err
}
defer resp.Body.Close()
var nowPlaying models.NowPlaying
err = xml.NewDecoder(resp.Body).Decode(&nowPlaying)
return &nowPlaying, err
@@ -77,7 +77,7 @@ func (c *Client) GetNowPlaying() (*models.NowPlaying, error) {
```go
func (c *Client) SendKey(key models.Key) error {
keyXML := fmt.Sprintf(`<key state="press" sender="GoClient">%s</key>`, key)
resp, err := c.httpClient.Post(
c.baseURL+"/key",
"application/xml",
@@ -117,14 +117,14 @@ func (d *DiscoveryService) DiscoverDevices() ([]Device, error) {
return nil, err
}
defer conn.Close()
// Send M-SEARCH request
searchRequest := "M-SEARCH * HTTP/1.1\r\n" +
"HOST: 239.255.255.250:1900\r\n" +
"MAN: \"ssdp:discover\"\r\n" +
"ST: urn:schemas-upnp-org:device:MediaRenderer:1\r\n" +
"MX: 3\r\n\r\n"
// Implementation details...
return devices, nil
}
@@ -158,13 +158,13 @@ func (e *EventClient) Subscribe(eventType string, handler EventHandler) {
func (e *EventClient) Start() error {
u := url.URL{Scheme: "ws", Host: e.client.host + ":8090", Path: "/"}
conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
return err
}
e.conn = conn
go e.eventLoop()
return nil
}
@@ -184,7 +184,7 @@ func (e *EventClient) eventLoop() {
}
return
}
if handler, exists := e.handlers[event.Type]; exists {
go handler(event)
}
@@ -220,7 +220,7 @@ func wasmDiscoverDevices(this js.Value, args []js.Value) interface{} {
handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
go func() {
devices, err := discovery.NewDiscoveryService(5*time.Second).DiscoverDevices()
result := make(map[string]interface{})
if err != nil {
result["error"] = err.Error()
@@ -228,13 +228,13 @@ func wasmDiscoverDevices(this js.Value, args []js.Value) interface{} {
devicesJSON, _ := json.Marshal(devices)
result["devices"] = string(devicesJSON)
}
// Call JavaScript callback
args[0].Invoke(js.ValueOf(result))
}()
return nil
})
return handler
}
```
@@ -280,7 +280,7 @@ func main() {
if err != nil {
return err
}
for i, device := range devices {
fmt.Printf("%d: %s (%s)\n", i+1, device.Name, device.Host)
}
@@ -300,7 +300,7 @@ func main() {
},
},
}
app.Run(os.Args)
}
@@ -311,7 +311,7 @@ func getClientFromContext(c *cli.Context) *client.Client {
devices, _ := discovery.DiscoverDevices()
deviceHost = selectDeviceInteractive(devices)
}
return client.NewClient(deviceHost, 8090)
}
```
@@ -327,34 +327,34 @@ var webAssets embed.FS
func main() {
mux := http.NewServeMux()
// Embedded web assets
webFS, err := fs.Sub(webAssets, "web")
if err != nil {
log.Fatal(err)
}
// SPA routing
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.FileServer(http.FS(webFS)).ServeHTTP(w, r)
return
}
data, err := webAssets.ReadFile("web/index.html")
if err != nil {
http.Error(w, "Not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "text/html")
w.Write(data)
})
// API endpoints
mux.HandleFunc("/api/devices", handleDeviceDiscovery)
mux.HandleFunc("/api/client/", handleClientProxy)
log.Println("SoundTouch Web UI starting on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
@@ -370,36 +370,36 @@ func handleClientProxy(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Invalid path", http.StatusBadRequest)
return
}
deviceIP := pathParts[3]
apiPath := "/" + strings.Join(pathParts[4:], "/")
// Proxy request to SoundTouch device
targetURL := fmt.Sprintf("http://%s:8090%s", deviceIP, apiPath)
proxyReq, err := http.NewRequest(r.Method, targetURL, r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Copy headers
for k, v := range r.Header {
proxyReq.Header[k] = v
}
resp, err := http.DefaultClient.Do(proxyReq)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer resp.Body.Close()
// Enable CORS
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
// Copy response
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
@@ -448,7 +448,7 @@ func (p *PlayStatus) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error
if err := d.DecodeElement(&s, &start); err != nil {
return err
}
switch s {
case string(PlayStatusPlaying), string(PlayStatusPaused), string(PlayStatusStopped):
*p = PlayStatus(s)
@@ -469,41 +469,41 @@ type Config struct {
// Server configuration
WebPort int `env:"WEB_PORT" default:"8080"`
APITimeout time.Duration `env:"API_TIMEOUT" default:"10s"`
// Discovery configuration
DiscoveryTimeout time.Duration `env:"DISCOVERY_TIMEOUT" default:"5s"`
CacheDevices bool `env:"CACHE_DEVICES" default:"true"`
// CORS configuration (for web proxy)
CORSOrigins []string `env:"CORS_ORIGINS" default:"*"`
// Logging
LogLevel string `env:"LOG_LEVEL" default:"info"`
}
func Load() Config {
var cfg Config
// Load from .env file
loadDotEnv()
// Parse environment variables with reflection
parseEnvVars(&cfg)
return cfg
}
func parseEnvVars(cfg interface{}) {
v := reflect.ValueOf(cfg).Elem()
t := v.Type()
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
fieldType := t.Field(i)
envTag := fieldType.Tag.Get("env")
defaultTag := fieldType.Tag.Get("default")
if envTag != "" {
if envValue := os.Getenv(envTag); envValue != "" {
setFieldValue(field, envValue)
@@ -545,11 +545,11 @@ func (m *MockClient) GetNowPlaying() (*models.NowPlaying, error) {
if err, exists := m.errors["now_playing"]; exists {
return nil, err
}
if resp, exists := m.responses["now_playing"]; exists {
return resp.(*models.NowPlaying), nil
}
return &models.NowPlaying{
Track: "Mock Track",
Artist: "Mock Artist",
@@ -577,8 +577,8 @@ CMD ["go", "test", "-v", "./..."]
```bash
# Makefile test target
test-integration:
docker-compose -f test/docker-compose.yml up --build --abort-on-container-exit
docker-compose -f test/docker-compose.yml down
docker compose -f test/docker-compose.yml up --build --abort-on-container-exit
docker compose -f test/docker-compose.yml down
```
## Recommended Project Structure
@@ -741,7 +741,7 @@ type APIError struct {
Message string `xml:",innerxml"`
}
// pkg/models/device.go
// pkg/models/device.go
type DeviceInfo struct {
XMLResponse
Name string `xml:"name"`
@@ -773,7 +773,7 @@ func main() {
},
},
}
app.Run(os.Args)
}
```
@@ -802,4 +802,4 @@ func main() {
## Conclusion
This pattern collection enables the development of robust API clients for hardware devices that function both as native tools and as web applications. The combination of Go's type safety, WASM support, and a structured build system makes it possible to use a single codebase for various deployment scenarios.
This pattern collection enables the development of robust API clients for hardware devices that function both as native tools and as web applications. The combination of Go's type safety, WASM support, and a structured build system makes it possible to use a single codebase for various deployment scenarios.
+3 -2
View File
@@ -8,7 +8,7 @@ Welcome to the documentation for the Bose SoundTouch Toolkit. This comprehensive
- **[Complete Migration Guide](guides/MIGRATION-GUIDE.md)** - Step-by-step guide from Bose Cloud to local control
- **[Getting Started](guides/GETTING-STARTED.md)** - Quick introduction to the toolkit
### For Existing Users
### For Existing Users
- **[Cloud Shutdown Survival Guide](guides/SURVIVAL-GUIDE.md)** - Prepare for the May 2026 shutdown
- **[SoundTouch Service Guide](guides/SOUNDTOUCH-SERVICE.md)** - Advanced service configuration
@@ -17,7 +17,7 @@ Welcome to the documentation for the Bose SoundTouch Toolkit. This comprehensive
The documentation is organized into three main categories:
### 1. **User Guides** - For everyday users migrating and managing devices
### 2. **Technical Reference** - For developers and advanced configuration
### 2. **Technical Reference** - For developers and advanced configuration
### 3. **Concept Documentation** - For contributors and system architects
## 🗂 Documentation Structure
@@ -47,6 +47,7 @@ The documentation is organized into three main categories:
### API Documentation
- [API Endpoints](reference/API-ENDPOINTS.md) - REST API reference
- [Spotify Account Addition](reference/spotify-account-addition.md) - Technical requests for Spotify
- [WebSocket Events](reference/WEBSOCKET-EVENTS.md) - Real-time events
- [Zone Management](reference/ZONE-MANAGEMENT.md) - Multi-room control
- [Preset Management](reference/PRESET-MANAGEMENT.md) - Preset operations
+6
View File
@@ -9,6 +9,7 @@
* [Getting Started](guides/GETTING-STARTED.md)
* [SoundTouch Service](guides/SOUNDTOUCH-SERVICE.md)
* [Initial Device Setup](guides/DEVICE-INITIAL-SETUP.md)
* [Device Setup Flow](DEVICE-SETUP.md)
* [MAC Address Mapping](guides/MAC-ADDRESS-MAPPING.md)
* [HTTPS Setup](guides/HTTPS-SETUP.md)
* [Deployment](guides/DEPLOYMENT.md)
@@ -28,6 +29,7 @@
## Technical Reference
* [API Cookbook](reference/API-COOKBOOK.md)
* [API Endpoints](reference/API-ENDPOINTS.md)
* [Spotify Account Addition](reference/spotify-account-addition.md)
* [Cloud API Emulation](reference/CLOUD-API.md)
* [System Endpoints](reference/SYSTEM-ENDPOINTS.md)
* [Speaker Endpoint](reference/SPEAKER-ENDPOINT.md)
@@ -56,12 +58,16 @@
* [Wiki API Comparison](analysis/WIKI-COMPARISON.md)
* [IoT Config Summary](analysis/IOT-CONFIG-SUMMARY.md)
* [IoT Configuration Analysis](analysis/IOT-CONFIGURATION-ANALYSIS.md)
* [Bose Lab Runbook](analysis/BOSE-LAB-RUNBOOK.md)
* [Missing Routes Spotify](analysis/MISSING-ROUTES-SPOTIFY.md)
## Parity Analysis
* [Parity Improvements](PARITY-IMPROVEMENTS.md)
* [Parity SoundCork](PARITY-SOUNDCORK.md)
* [Parity OpenCloudTouch](PARITY-OPENCLOUDTOUCH.md)
## Appendix (Other Documents)
* [External Services Abstraction](EXTERNAL-SERVICES-ABSTRACTION.md)
* [API Navigation Reference](API-NAVIGATION-REFERENCE.md)
* [Claude Instructions](CLAUDE.md)
* [Content Selection Implementation](CONTENT-SELECTION-IMPLEMENTATION.md)
+777
View File
@@ -0,0 +1,777 @@
# Bose SoundTouch Traffic Analysis Runbook
> **Goal:** Set up a Raspberry Pi as a transparent access point to fully observe the traffic of the Bose SoundTouch app specifically the pairing flow with the Bose Cloud. This serves as a basis for later reverse engineering / simulation of the cloud endpoints.
---
## Prerequisites
| Component | Details |
|--------------------|------------------------------------------------------------------|
| Raspberry Pi | Pi 3 or newer, Raspberry Pi OS (Bullseye, Bookworm, Trixie) |
| Network interfaces | `eth0` → LAN cable to FritzBox, `wlan0` → own Access Point |
| FritzBox | Unchanged, assigns an IP to the Pi via DHCP on eth0 |
| Custom DNS Server | Already present (or see Appendix A), incl. custom CA certificate |
| Phone | Android, connects to the Pi's Wi-Fi |
### Network Architecture
```
Internet
FritzBox (existing, unchanged)
↓ LAN cable (eth0)
Raspberry Pi
├── DNS Server → selective logging / redirection
├── hostapd → custom Wi-Fi Access Point ("Bose-Lab")
├── dnsmasq → DHCP for clients, DNS to custom server
├── iptables → NAT, Forwarding eth0 ↔ wlan0
├── tcpdump → full traffic capture
└── (optional) mitmproxy → HTTPS decryption
↓ Wi-Fi ("Bose-Lab")
Android Phone
└── Bose SoundTouch App
```
---
## Step 1 Install Packages
```bash
sudo apt update && sudo apt install -y \
hostapd \ # Wi-Fi Access Point daemon
dnsmasq \ # DHCP + DNS forwarding
nftables \ # Modern NAT / firewall / forwarding
tcpdump \ # Packet capture at all levels
wireshark-common # tshark CLI (optional, for live analysis)
```
---
## Step 2 Enable IP Forwarding
The Pi must forward packets between `wlan0` (phone) and `eth0` (FritzBox).
```bash
# Active immediately (no reboot required)
sudo sysctl -w net.ipv4.ip_forward=1
# Permanent (survives reboots)
# On modern Debian, using a dedicated file in sysctl.d/ is more reliable:
echo "net.ipv4.ip_forward=1" | sudo tee /etc/sysctl.d/99-ip-forward.conf
# Apply changes immediately
sudo sysctl --system
```
**Verify:**
```bash
# After a reboot, ensure it is still '1'
cat /proc/sys/net/ipv4/ip_forward
```
---
## Step 3 Static IP on wlan0 (systemd-networkd)
On modern Debian (Bookworm/Trixie), `dhcpcd` is replaced by `systemd-networkd`.
```bash
# Create network configuration
sudo tee /etc/systemd/network/08-wlan0.network << 'EOF'
[Match]
Name=wlan0
[Network]
Address=192.168.10.1/24
IPForward=yes
ConfigureWithoutCarrier=yes
DHCP=no
IPv6AcceptRA=no
EOF
# Restart service
sudo systemctl enable systemd-networkd
sudo systemctl restart systemd-networkd
# Ensure wpa_supplicant and NetworkManager don't interfere
sudo nmcli device set wlan0 managed no
sudo systemctl stop wpa_supplicant@wlan0
sudo systemctl mask wpa_supplicant@wlan0
```
**Verify:**
```bash
ip addr show wlan0
# Expected: ONLY inet 192.168.10.1/24 (NO second DHCP IP)
```
---
## Step 4 hostapd (Access Point)
```bash
sudo tee /etc/hostapd/hostapd.conf << 'EOF'
interface=wlan0
driver=nl80211
ssid=Bose-Lab
hw_mode=b
#hw_mode=g
channel=1
#channel=6
wmm_enabled=0
auth_algs=1
wpa=2
wpa_passphrase=secret123
wpa_key_mgmt=WPA-PSK
wpa_pairwise=CCMP
EOF
# The modern way is to just use hostapd.service which defaults to /etc/hostapd/hostapd.conf
sudo systemctl unmask hostapd
sudo systemctl enable --now hostapd
```
**Verify:**
```bash
sudo systemctl status hostapd
# Expected: active (running)
```
---
## Step 5 dnsmasq (DHCP + DNS)
dnsmasq gives the phone an IP and forwards DNS queries to the custom DNS server.
```bash
# Back up original config
sudo mv /etc/dnsmasq.conf /etc/dnsmasq.conf.bak
sudo tee /etc/dnsmasq.conf << 'EOF'
interface=wlan0
dhcp-range=192.168.10.100,192.168.10.200,24h
dhcp-option=3,192.168.10.1
dhcp-option=6,192.168.10.1
# DNS Upstream: custom server on localhost (adjust port if necessary)
server=127.0.0.1#5353 # Example: custom server on port 5353
# Alternatively: server=1.1.1.1 if DNS server runs directly on port 53
# Log all DNS queries (for initial analysis)
log-queries
log-facility=/var/log/dnsmasq.log
EOF
sudo systemctl restart dnsmasq
```
**Observe DNS log live:**
```bash
sudo tail -f /var/log/dnsmasq.log
```
---
## Step 6 NAT and Forwarding (nftables)
On modern Debian (Bookworm/Trixie), `nftables` is the default and recommended way to manage NAT and traffic forwarding.
```bash
# Define the NAT and Forwarding rules
sudo tee /etc/nftables.conf << 'EOF'
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain forward {
type filter hook forward priority 0; policy drop;
# Allow traffic from phone (wlan0) to internet (eth0)
iifname "wlan0" oifname "eth0" accept
# Allow established/related traffic back to the phone
iifname "eth0" oifname "wlan0" ct state established,related accept
}
}
table ip nat {
chain posterouting {
type nat hook postrouting priority 100; policy accept;
# MASQUERADE outgoing packets on eth0
oifname "eth0" masquerade
}
}
EOF
# Enable and start nftables
sudo systemctl enable nftables
sudo systemctl restart nftables
```
**Verify:**
```bash
sudo nft list ruleset
# Expected: ruleset showing the forward and nat chains
```
### WiFi "Bose-Lab" not visible?
If you cannot see the `Bose-Lab` SSID on your phone:
1. **Check hostapd status:** `sudo systemctl status hostapd`. If it failed with "nl80211: Driver does not support configured mode", try changing `hw_mode=g` to `hw_mode=b`.
2. **Interface blocking:** Ensure `rfkill` hasn't blocked WiFi: `sudo rfkill unblock wlan`.
3. **Country Code:** Some systems require a country code in `hostapd.conf` to enable the radio. Add `country_code=DE` (or your country) to the top of `/etc/hostapd/hostapd.conf` and restart hostapd: `sudo systemctl restart hostapd`.
4. **Local Radio Check:** You can verify that the radio is actually configured as an AP: `iw dev wlan0 info`. Look for `type AP` and your SSID.
> **Note:** Do NOT rely on `iw dev wlan0 scan` for your own SSID; many WiFi drivers cannot "scan" and "broadcast" simultaneously.
5. **Debug Mode:** If the scan still returns nothing, stop the service and run hostapd in the foreground to see real-time errors:
```bash
sudo systemctl stop hostapd
sudo hostapd -dd /etc/hostapd/hostapd.conf
```
Look for messages like `nl80211: Failed to set interface wlan0 into AP mode`. This usually means the hardware is busy or doesn't support the current `hw_mode` / `channel` combination.
6. **Conflicting Services:** Ensure nothing else is managing `wlan0`. NetworkManager is common on modern Debian:
```bash
sudo nmcli device set wlan0 managed no
```
7. **Ghost IP Conflict:** If `ip addr show wlan0` shows both `192.168.10.1` and another IP (like `192.168.178.x`), `hostapd` will fail. This is usually caused by NetworkManager managing the interface. Ensure you've run:
```bash
sudo nmcli device set wlan0 managed no
# If the ghost IP is still there, remove it manually:
sudo ip addr del 192.168.178.X/24 dev wlan0
```
---
## Step 7 Install Custom CA Certificate on the Phone
Since a custom DNS server with a custom CA certificate is used, it must be trusted on the phone otherwise, the app will block HTTPS connections to redirected domains.
### Copy CA Certificate to the Pi (if not already there)
If you haven't created a CA yet, follow **Appendix A** first.
```bash
# Certificate is located e.g. at /etc/my-dns-ca/ca.crt
# Temporarily make reachable via HTTP for easy download:
cd /etc/my-dns-ca/
python3 -m http.server 8080
# → Reachable at http://192.168.10.1:8080/ca.crt
```
### Install on Android
1. Connect phone to `Bose-Lab`
2. Open browser → `http://192.168.10.1:8080/ca.crt`
3. Download certificate
4. **Settings → Security → Credentials → Install CA Certificate**
5. Select certificate and confirm
> **Note:** Android distinguishes between system CAs and user CAs. User-installed CAs are accepted by many apps, but apps with certificate pinning (hardcoded certificate hashes) ignore them. Whether Bose uses pinning will be visible in the capture (Connection Reset after TLS ClientHello).
### Android 14+ Special Case
From Android 14 onwards, apps do not trust user CAs by default unless explicitly declared in the manifest. If the Bose app rejects the CA certificate:
```bash
# Option A: Root + Magisk module "MagiskTrustUserCerts"
# → moves user CAs to the system store
# Option B: Root + manually copy to system CA directory
adb push ca.crt /system/etc/security/cacerts/
adb shell chmod 644 /system/etc/security/cacerts/ca.crt
```
---
## Step 8 Capture Traffic
### All at once (recommended)
```bash
# Full capture of all protocols on wlan0
# Filename with timestamp for multiple sessions
sudo tcpdump -i wlan0 \
-w /tmp/bose-$(date +%Y%m%d-%H%M%S).pcap \
-s 0 # full packet length (no truncation)
# End session: Ctrl+C
```
### Targeted by protocol
```bash
# DNS only (Port 53) shows if app uses standard DNS
sudo tcpdump -i wlan0 -n port 53
# HTTPS only TLS connections to Bose Cloud
sudo tcpdump -i wlan0 -n 'tcp port 443'
# mDNS (ZeroConf) device discovery in LAN
# Multicast group 224.0.0.1, Port 5353
sudo tcpdump -i wlan0 -n 'udp port 5353'
# SSDP/UPnP alternative device discovery
sudo tcpdump -i wlan0 -n 'udp port 1900'
# Everything except DNS (reduces noise)
sudo tcpdump -i wlan0 -n 'not port 53' -w /tmp/bose-nodns.pcap
# Traffic of a specific host only (filter by phone IP)
# Read phone IP from dnsmasq.leases beforehand (see below)
sudo tcpdump -i wlan0 -n host 192.168.10.101
```
### Read SNI from TLS Traffic (without decryption)
```bash
# Extract domains from TLS ClientHello (SNI is unencrypted)
sudo tcpdump -i wlan0 -n 'tcp port 443' -A 2>/dev/null \
| grep -oP '(?<=\x00)([a-zA-Z0-9.-]+\.(?:com|net|io|cloud|bose\.com))'
```
### Readable mDNS Announcements output
```bash
# tshark decodes mDNS directly
sudo tshark -i wlan0 -f 'udp port 5353' -T fields \
-e dns.qry.name \
-e dns.resp.name \
-e dns.a
```
---
## Step 9 Analysis with Wireshark (on PC)
Transfer `.pcap` files from the Pi to the PC:
```bash
# From the PC (scp)
scp pi@192.168.10.1:/tmp/bose-*.pcap ~/Desktop/
```
**Important Wireshark Filters:**
```
# DNS only
dns
# HTTPS only
tcp.port == 443
# WebSocket connections (HTTP Upgrade)
websocket
# mDNS
mdns
# TLS Handshakes (SNI visible)
tls.handshake.extensions_server_name
# Traffic of a specific domain (resolve by IP)
http.host contains "bose"
# WebSocket frames
websocket.payload
```
> **Tip:** Wireshark decodes WebSocket frames automatically if it sees the HTTP Upgrade handshake in the same capture. For the pairing flow: filtering for `tls.handshake.extensions_server_name` shows all domains the app contacts, even without decryption.
---
## Step 10 mitmproxy (optional, for HTTPS content)
Only useful if the CA certificate on the phone is trusted and no certificate pinning is active. `mitmproxy` acts as a Man-in-the-Middle by generating fake, on-the-fly certificates for any domain (e.g., `global.api.bose.io`) using your custom CA.
### 1. Configure mitmproxy to use your Custom CA
By default, `mitmproxy` creates its own CA in `~/.mitmproxy/`. To ensure the phone (which already trusts your `ca.crt`) accepts the traffic, you must tell `mitmproxy` to use your existing CA:
```bash
# mitmproxy expects the CA in a specific PEM format (cert + key in one file)
sudo mkdir -p ~/.mitmproxy
sudo cat /etc/my-dns-ca/ca.crt /etc/my-dns-ca/ca.key | sudo tee ~/.mitmproxy/mitmproxy-ca.pem > /dev/null
```
### 2. Install and Start mitmproxy
```bash
# Install mitmproxy binary (stable version for aarch64)
cd /tmp
wget https://downloads.mitmproxy.org/12.2.1/mitmproxy-12.2.1-linux-aarch64.tar.gz
tar -xzf mitmproxy-12.2.1-linux-aarch64.tar.gz
sudo mv mitmproxy mitmdump mitmweb /usr/local/bin/
rm mitmproxy-12.2.1-linux-aarch64.tar.gz
mitmproxy --version
# Transparent proxy on port 8080
# It will now use the CA from ~/.mitmproxy/mitmproxy-ca.pem
mitmproxy --mode transparent --listen-port 8080
# Alternatively: mitmdump for automatic logging to file
# mitmdump --mode transparent --listen-port 8080 -w /tmp/bose-https.mitm
```
### 3. Troubleshooting: TLS Handshake Failures
If you see `Client TLS handshake failed. The client does not trust the proxy's certificate for www.google.com` (or other domains) in the `mitmproxy` logs:
1. **HSTS and Pre-installed Pinning:** High-security sites like `www.google.com` use **HSTS (HTTP Strict Transport Security)** and have their certificates hardcoded (pinned) into browsers like Chrome and the Android system. **These will always fail with a User-installed CA.**
2. **User vs. System CA Store:** On Android 7.0+, apps **do not trust User-installed CAs by default**. They only trust the "System" store.
* **The Bose app:** If it fails, it's because it only trusts the System store or uses its own certificate pinning.
* **The Fix (Rooted Phone):** Use a Magisk module like `AlwaysTrustUserCerts` or manually move your `ca.crt` to `/system/etc/security/cacerts/` (see Step 7).
3. **The "Golden Rule" - Verify the Proxy is Working:**
To confirm your CA and `mitmproxy` are correctly configured, test with a non-HSTS site on the phone's browser (e.g., `http://neverssl.com`). Once redirected to HTTPS, **inspect the certificate**. It should say it was issued by your "Bose-Lab Root CA" (or "SoundTouch Root CA").
* **If this works:** Your "factory" (mitmproxy + CA) is 100% correct. Any failure in the Bose app is due to its own security policy (ignore User Store or Pinning).
* **If this fails:** Your CA is not trusted by the browser or `mitmproxy` is not using your PEM file.
Alternatively, use `curl` from a terminal emulator on the phone:
```bash
# This should work if the CA is in the user store and curl is told to use it
curl -v --cacert /path/to/ca.crt https://example.com
```
4. **Check mitmproxy CA:** Ensure `mitmproxy` is actually using your CA. When it starts, it should NOT generate a new CA in `~/.mitmproxy/mitmproxy-ca.pem` if you've already placed yours there.
---
**nftables rule: redirect HTTPS traffic to mitmproxy**
```bash
# Create a temporary file for the redirection rule
sudo nft add table ip mitm
sudo nft add chain ip mitm prerouting { type nat hook prerouting priority -100 \; }
sudo nft add rule ip mitm prerouting iifname "wlan0" tcp dport 443 redirect to :8080
```
**Remove rule when no longer needed:**
```bash
sudo nft delete table ip mitm
```
> **Detecting Certificate Pinning:** If the app immediately disconnects after mitmproxy redirection (connection reset directly after TLS ClientHello), pinning is active. In this case, Frida + root is needed to patch the pinning.
---
## Step 11 Bypassing Android Trust Restrictions
If `neverssl.com` works in the browser but the Bose app shows `TLS handshake failed` in `mitmproxy`, the app is either ignoring the **User CA store** (common on Android 7+) or using **Certificate Pinning**.
### Option A: Move CA to System Store (Requires Root/Magisk)
This is the most reliable way to make apps trust your CA without modifying the app itself.
1. **Using Magisk (Recommended):**
Install the **"AlwaysTrustUserCerts"** or **"Move Certificates"** module in Magisk. It automatically mirrors all certificates from the User store to the System store on every boot.
2. **Manual Move (via ADB):**
Android system certificates are stored in `/system/etc/security/cacerts/` and must be named using the hash of the certificate.
```bash
# 1. Get the hash of your certificate
hash=$(openssl x509 -inform PEM -subject_hash_old -in ca.crt | head -1)
# 2. Rename the certificate locally
cp ca.crt ${hash}.0
# 3. Push to the phone (requires remounting /system as read-write)
adb push ${hash}.0 /sdcard/
adb shell
su
mount -o rw,remount /
cp /sdcard/${hash}.0 /system/etc/security/cacerts/
chmod 644 /system/etc/security/cacerts/${hash}.0
chown root:root /system/etc/security/cacerts/${hash}.0
reboot
```
### Option B: Patching the App (No Root Required)
If you cannot root your phone, you can modify the app's APK to trust user-installed certificates. This involves obtaining the APK, decompiling it, adding a network security configuration, and then repackaging and signing it.
#### 0. How to get the .apk file?
You have two main ways to get the official Bose SoundTouch APK:
**Method 1: Extract from your phone (Safest)**
If the app is already installed on your phone, you can pull it using `adb`:
```bash
# 1. Find the package name (usually com.bose.soundtouch)
adb shell pm list packages | grep bose
# 2. Get the full path to the APK on the phone
adb shell pm path com.bose.soundtouch
# Output: package:/data/app/~~...==/com.bose.soundtouch-.../base.apk
# 3. Pull the file to your computer
adb pull /data/app/~~...==/com.bose.soundtouch-.../base.apk Bose-SoundTouch.apk
```
**Method 2: Download from a Mirror (Easiest)**
You can download the APK from reputable third-party sites.
> **Warning:** Always verify the site's reputation.
* [APKMirror](https://www.apkmirror.com/apk/bose-corporation/bose-soundtouch/)
* [APKPure](https://apkpure.com/bose-soundtouch/com.bose.soundtouch)
#### 1. Automated Method: apk-mitm (Recommended)
The easiest way is to use `apk-mitm`, which automates the entire process including fixing common certificate pinning libraries.
```bash
# Requires Node.js installed on your PC
npx apk-mitm Bose-SoundTouch.apk
```
This will produce a `Bose-SoundTouch-patched.apk` which you can install on your phone.
#### 2. Manual Method: Network Security Config
If you prefer to do it manually:
1. **Decompile the APK:**
```bash
apktool d Bose-SoundTouch.apk
```
2. **Create/Modify `res/xml/network_security_config.xml`:**
```xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config>
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>
```
3. **Update `AndroidManifest.xml`:**
Ensure the `<application>` tag includes: `android:networkSecurityConfig="@xml/network_security_config"`.
4. **Repackage and Sign:**
```bash
apktool b Bose-SoundTouch -o Bose-SoundTouch-patched.apk
# Sign with your own key
# 1. Generate a keystore (if you don't have one)
# Note: You can use ANY name/values here. The phone does not need to "know" or "trust" this key beforehand.
# It only needs the APK to be digitally signed so the Android installer accepts it.
keytool -genkey -v -keystore my-release-key.keystore -alias alias_name -keyalg RSA -keysize 2048 -validity 10000
# 2. Sign the APK
apksigner sign --ks my-release-key.keystore --out Bose-SoundTouch-patched-signed.apk Bose-SoundTouch-patched.apk
# Alternatively, use uber-apk-signer (recommended for simplicity)
# It handles zipalign and signing automatically.
java -jar uber-apk-signer.jar --apk Bose-SoundTouch-patched.apk
```
#### 3. Install the Patched APK
Once you have your `Bose-SoundTouch-patched.apk` (and it is signed), you need to install it on your phone.
**Important:** You must **uninstall the original Bose app first**. Android will not allow you to "update" the official app with your patched version because the digital signatures won't match.
**Method 1: via ADB (Recommended)**
```bash
# 1. Uninstall the original app
adb uninstall com.bose.soundtouch
# 2. Install your patched version
adb install Bose-SoundTouch-patched.apk
```
**Method 2: Manual Transfer**
1. Copy the `Bose-SoundTouch-patched.apk` to your phone's storage (via USB, Google Drive, or the Pi's HTTP server).
2. On the phone, use a File Manager to open the APK.
3. If prompted, allow "Install from Unknown Sources" for your File Manager.
### Option C: Patching the App with Frida (Requires Root)
If the app uses **Certificate Pinning** (hardcoded hashes), even moving the CA to the System store won't work. You must disable the pinning check in the app's code.
1. **Install Frida** on your PC and `frida-server` on the rooted phone.
2. **Use a universal bypass script:**
```bash
frida -U -f com.bose.soundtouch -l https://codeshare.frida.re/@pcipolloni/universal-android-ssl-pinning-bypass-with-frida/ --no-pause
```
*(Replace `com.bose.soundtouch` with the actual package name if different).*
## Step 12 Alternative: Regular HTTP Proxy Mode
If the **Transparent AP** setup (Steps 16) is too complex or you are experiencing routing issues, you can use `mitmproxy` as a **Regular HTTP Proxy**.
### 1. How it works
In this mode, the Pi acts as a simple server on port 8080. You tell your phone's Wi-Fi settings to send all traffic to `192.168.10.1:8080`.
* **Pros:** No complex `nftables` or NAT rules required.
* **Cons:** Many Android apps (and background processes) ignore system-wide proxy settings. **HTTPS still requires a trusted CA for decryption.**
### 2. Start mitmproxy in Regular Mode
```bash
# Stop transparent mode first if it's running
# No special flags needed for regular mode
mitmproxy --listen-port 8080
```
### 3. Configure the Phone
1. Go to **Settings → Wi-Fi → Bose-Lab**.
2. Select **Modify Network** (or the "i" icon).
3. Set **Proxy** to **Manual**.
4. **Proxy hostname:** `192.168.10.1`
5. **Proxy port:** `8080`
6. Save and try to browse a site.
---
## Step 13 Extracting for soundtouch-service
```bash
# Which IPs did the phone receive?
cat /var/lib/misc/dnsmasq.leases
# Is the access point active?
sudo systemctl status hostapd
# Is dnsmasq active?
sudo systemctl status dnsmasq
# Check interfaces and IPs
ip addr show
# Check routing table
ip route show
# Show active nftables rules
sudo nft list ruleset
# All running tcpdump processes
pgrep -a tcpdump
# Test the Pi's own DNS resolution
dig @127.0.0.1 -p 5353 global.api.bose.io
# Check network connectivity from the phone (from the Pi)
ping 192.168.10.101 # Phone IP from dnsmasq.leases
```
---
## Restart Sequence
After a Pi reboot, everything should come up automatically. If not:
```bash
# Restart and enable all core services
sudo systemctl restart systemd-networkd
sudo systemctl enable --now hostapd
sudo systemctl enable --now dnsmasq
sudo systemctl restart nftables
# Verify the unmanaged state of wlan0 (nmcli)
sudo nmcli device set wlan0 managed no
```
---
## What to Expect
| Protocol | Port | Tool | Visibility |
|----------------------|------------|--------------------------|------------------------------------------------|
| DNS (Standard) | UDP 53 | tcpdump, dnsmasq log | Full, plaintext |
| HTTPS / REST | TCP 443 | tcpdump (SNI), mitmproxy | SNI without decryption, content with mitmproxy |
| WebSockets | TCP 443/80 | Wireshark | Frames decoded if TLS is broken |
| mDNS / ZeroConf | UDP 5353 | tcpdump, tshark | Full, plaintext |
| SSDP / UPnP | UDP 1900 | tcpdump | Full, plaintext |
| SoundTouch local API | TCP 8090 | tcpdump | Full, plaintext (no TLS) |
> **Expectation for Bose SoundTouch:** The app likely uses standard DNS (older app generation), REST/HTTPS for the pairing flow with the cloud, WebSockets for push events from the device, and mDNS for local device discovery. The local device API on port 8090 is HTTP without TLS this traffic is always readable.
---
## Next Steps After Analysis
1. Extract domains from DNS log and SNI → List of all Bose endpoints
2. HTTP methods and paths from mitmproxy log → Reconstruct API structure
3. Document auth flow (OAuth2? Proprietary? Token format?)
4. Build a minimal mock server simulating the critical endpoints
5. Testing: App against mock server → does pairing work offline?
---
## Appendix A Generating a Custom CA Certificate
If you don't have a custom DNS server with a CA yet, you can create one directly on the Pi. Alternatively, if you are already using the `soundtouch-service` from this repository, you can reuse its CA certificate located in the `data/certs/` directory.
### 0. (Optional) Copy an Existing CA from another host
If you are already using the `soundtouch-service` on another machine (e.g., your notebook), you can copy the existing CA to the Pi instead of generating a new one:
```bash
# On your Pi:
sudo mkdir -p /etc/my-dns-ca
sudo chown $USER:$USER /etc/my-dns-ca
# Run this on your notebook (replace hostnames and paths):
# Note: This is easiest if your SSH key is added to the Pi and soundtouch-service host.
# If you run into permission issues with sudo, ensure the source user has passwordless sudo for 'cat'.
# Step A: Download from source to your notebook
ssh soundtouch-service "sudo cat /var/lib/soundtouch-service/certs/ca.crt" > ca.crt
ssh soundtouch-service "sudo cat /var/lib/soundtouch-service/certs/ca.key" > ca.key
# Step B: Upload from notebook to the Pi
scp ca.crt ca.key soundtouch-access-point:/tmp/
ssh soundtouch-access-point "sudo mv /tmp/ca.crt /tmp/ca.key /etc/my-dns-ca/ && sudo chown root:root /etc/my-dns-ca/ca.*"
rm ca.crt ca.key
```
### 1. Create CA Key and Certificate
```bash
sudo mkdir -p /etc/my-dns-ca
cd /etc/my-dns-ca
# Generate CA private key
sudo openssl genrsa -out ca.key 4096
# Generate Root CA certificate
# Note: we explicitly add basicConstraints=CA:TRUE for modern TLS clients
sudo openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 \
-out ca.crt \
-subj "/C=DE/O=Bose-Lab/CN=Bose-Lab Root CA" \
-addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,keyCertSign,cRLSign"
```
### 2. Generate a Certificate for Interception (Example)
To intercept `global.api.bose.io`, you need a certificate for it, signed by your CA:
```bash
# Generate server key
sudo openssl genrsa -out bose.key 2048
# Create CSR (Certificate Signing Request) configuration
sudo tee bose.ext << 'EOF'
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = global.api.bose.io
DNS.2 = *.bose.io
EOF
# Generate CSR
sudo openssl req -new -key bose.key -out bose.csr \
-subj "/C=DE/O=Bose-Lab/CN=global.api.bose.io"
# Sign the certificate with your CA
sudo openssl x509 -req -in bose.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out bose.crt -days 365 -sha256 -extfile bose.ext
```
### 3. Usage in your DNS/HTTPS Server
Your custom server (e.g., a small Go or Python script) would then use `bose.crt` and `bose.key` to serve HTTPS traffic for those domains.
+43
View File
@@ -0,0 +1,43 @@
# Spotify Account Addition Implementation Status
To fully replace Bose cloud services for the Spotify account addition flow in the "Stockholm" SoundTouch application, the following routes have been implemented in the `soundtouch-service`:
## 1. OAuth Token Exchange (Bose Cloud)
The Stockholm background worker (in `worker_common.js` and `spotify_worker.js`) performs a token exchange using an authorization code.
* **Route**: `POST /oauth/account/{account}/music/musicprovider/{sourceID}/token/cs`
* **Purpose**: To exchange the Spotify authorization code for a Bose-mediated token.
* **Implementation**: `HandleBoseAccountToken` in `pkg/service/handlers/handlers_oauth.go`.
* **Registration**: Registered in `cmd/soundtouch-service/main.go` under the `/oauth` route group.
## 2. Cloud Source Registration (Marge Service)
The SoundTouch application registers a new music source (e.g., Spotify) with the Bose cloud profile.
* **Route**: `POST /streaming/account/{account}/source`
* **Purpose**: To add the new source (username, credentials, display name) to the user's emulated cloud profile.
* **Implementation**: `HandleMargeAddSource` in `pkg/service/handlers/handlers_marge.go`.
* **Registration**: Registered in `cmd/soundtouch-service/main.go` under the `/streaming` route group.
* **Payload Format**: XML `application/vnd.bose.streaming-v1.1+xml` containing `<source>` with `<username>`, `<sourceproviderid>`, and `<credential type="token_version_3">`.
## 3. Redirect Handling (Browser to App)
The `soundtouch://` deep link redirect URI is handled by the management interface which provides the OAuth callback.
* **Callback Route**: `GET /mgmt/spotify/callback`
* **Implementation**: `HandleMgmtSpotifyCallback` in `pkg/service/handlers/handlers_mgmt.go`.
* **Confirmation Route**: `POST /mgmt/spotify/confirm` (used by mobile apps for deep-link codes).
* **Implementation**: `HandleMgmtSpotifyConfirm` in `pkg/service/handlers/handlers_mgmt.go`.
## Implementation Details
1. **Marge Add Source**:
* `HandleMargeAddSource` in `pkg/service/handlers/handlers_marge.go` parses the incoming XML and persists the new source to the `DataStore` for the corresponding account.
2. **OAuth Account Token Exchange**:
* `HandleBoseAccountToken` in `pkg/service/handlers/handlers_oauth.go` supports the `/oauth/account/.../token/cs` path.
* It responds with a JSON payload including `access_token` and `token_type` "Bearer" after exchanging the code via `ExchangeCodeAndStore`.
3. **Router Registration**:
* These paths are registered in `cmd/soundtouch-service/main.go` within the `/streaming`, `/oauth`, and `/mgmt` route blocks.
+19 -19
View File
@@ -185,7 +185,7 @@ type NowPlaying struct {
type PlayStatus string
const (
PlayStatusPlaying PlayStatus = "PLAY_STATE"
PlayStatusPaused PlayStatus = "PAUSE_STATE"
PlayStatusPaused PlayStatus = "PAUSE_STATE"
PlayStatusStopped PlayStatus = "STOP_STATE"
)
@@ -277,19 +277,19 @@ type Config struct {
// Server configuration
WebPort int `env:"WEB_PORT" default:"8080"`
APITimeout time.Duration `env:"API_TIMEOUT" default:"10s"`
// Discovery configuration
// Discovery configuration
DiscoveryTimeout time.Duration `env:"DISCOVERY_TIMEOUT" default:"5s"`
CacheDevices bool `env:"CACHE_DEVICES" default:"true"`
CacheTTL time.Duration `env:"CACHE_TTL" default:"5m"`
// CORS configuration (for web proxy)
CORSOrigins []string `env:"CORS_ORIGINS" default:"*"`
// Logging
LogLevel string `env:"LOG_LEVEL" default:"info"`
LogFormat string `env:"LOG_FORMAT" default:"json"`
// Development
DevMode bool `env:"DEV_MODE" default:"false"`
}
@@ -537,7 +537,7 @@ build-all: build-linux build-darwin build-windows
dev-cli:
air -c .air-cli.toml
dev-webapp:
dev-webapp:
air -c .air-webapp.toml
dev-wasm:
@@ -556,7 +556,7 @@ check: fmt vet lint test
# Docker development environment
docker-dev:
docker-compose up --build
docker compose up --build
# Release packaging
release: build-all
@@ -596,7 +596,7 @@ import (
"fmt"
"log"
"time"
"github.com/gesellix/bose-soundtouch/pkg/client"
"github.com/gesellix/bose-soundtouch/pkg/discovery"
"github.com/gesellix/bose-soundtouch/pkg/models"
@@ -609,36 +609,36 @@ func main() {
if err != nil {
log.Fatal(err)
}
if len(devices) == 0 {
log.Fatal("No SoundTouch devices found")
}
// Create client for first device
client := client.NewClient(client.ClientConfig{
Host: devices[0].Host,
Port: 8090,
Timeout: 10 * time.Second,
})
// Get device info
info, err := client.GetDeviceInfo()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Connected to: %s\n", info.Name)
// Get current playback
nowPlaying, err := client.GetNowPlaying()
if err != nil {
log.Fatal(err)
}
if nowPlaying.PlayStatus == models.PlayStatusPlaying {
fmt.Printf("Playing: %s - %s (%s)\n",
fmt.Printf("Playing: %s - %s (%s)\n",
nowPlaying.Artist, nowPlaying.Track, nowPlaying.Album)
}
// Control playback
if nowPlaying.PlayStatus == models.PlayStatusPlaying {
client.SendKey(models.KeyPause)
@@ -737,11 +737,11 @@ docker run -p 8080:8080 soundtouch-webapp
```bash
# Local development with hot reload
make dev-webapp # Web app development
make dev-wasm # WASM development
make dev-wasm # WASM development
make dev-cli # CLI development
# Full development environment
docker-compose up # Mock devices + web app
docker compose up # Mock devices + web app
```
## Success Criteria
@@ -781,7 +781,7 @@ docker-compose up # Mock devices + web app
- [Bose SoundTouch Web API Documentation](https://assets.bosecreative.com/m/496577402d128874/original/SoundTouch-Web-API.pdf)
- [Go WebAssembly](https://github.com/golang/go/wiki/WebAssembly)
- [UPnP Device Architecture](http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.0.pdf)
- [UPnP Device Architecture](http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.0.pdf)
- [Go Embed Directive](https://pkg.go.dev/embed)
- [Gorilla WebSocket](https://github.com/gorilla/websocket)
- [PROJECT-PATTERNS.md](../PROJECT-PATTERNS.md) - Detailed pattern documentation
+16 -4
View File
@@ -816,7 +816,7 @@ func TestAccountManager_CreateAccount(t *testing.T) {
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := manager.CreateAccount(tt.input)
@@ -848,6 +848,18 @@ go test ./... -v -cover
go test -bench=. ./...
```
## Performance Requirements
### Response Time Targets
- Local API requests: < 100ms (95th percentile)
- Mirror requests: < 200ms overhead (asynchronous)
- Discovery time: < 5s for network scan
### Resource Constraints
- Memory usage: < 64MB for small deployments
- CPU usage: < 5% on dual-core ARM systems (idle)
- Storage: < 100MB for interaction logs (rotatable)
### Security Considerations
#### Simple Security Model
@@ -949,12 +961,12 @@ func (s *Server) HandleHealthCheck(w http.ResponseWriter, r *http.Request) {
Version: version,
Uptime: time.Since(startTime).String(),
}
// Simple checks
if !s.canWriteToDataDir() {
health.Status = "error"
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(health)
}
@@ -986,4 +998,4 @@ func (m *SimpleMetrics) Save(dataDir string) error {
}
```
This technical specification provides comprehensive details for implementing the enhanced state management system while maintaining compatibility with existing SoundTouch service functionality and meeting the performance requirements for small hardware deployments.
This technical specification provides comprehensive details for implementing the enhanced state management system while maintaining compatibility with existing SoundTouch service functionality and meeting the performance requirements for small hardware deployments.
+1 -1
View File
@@ -116,7 +116,7 @@ volumes:
And run:
```bash
docker-compose up -d
docker compose up -d
```
## Quick Start
+197
View File
@@ -0,0 +1,197 @@
# Spotify Account Addition Technical Reference
This document details the exact network requests performed by the Bose SoundTouch "Stockholm" application and the SoundTouch speaker when adding a new Spotify account. This information is based on analysis of the Stockholm firmware version `27.0.13-4277-8963611`.
## Flow Overview
1. **User Authorization Initiation**: The app opens the system browser to Spotify's authorization page.
2. **Redirect Handling**: After authorization, Spotify redirects back to the app via a custom URI scheme, delivering an authorization `code`.
3. **OAuth Token Exchange**: The app sends this `code` to the background worker, which exchanges it for a Bose-mediated token.
4. **Cloud Source Registration**: The app registers the Spotify account as a "source" in the user's Bose Cloud (Marge) profile.
5. **Local Device Sync**: The app notifies the local SoundTouch speaker about the new source, which then updates its internal configuration.
---
## 0. User Authorization Initiation
The process begins in the Stockholm UI when the user selects Spotify to add a new account.
### Request Details (App to Browser)
- **Action**: Open System Browser
- **Base URL**: `[SPOTIFY_AUTH_URL]` (e.g., `https://accounts.spotify.com/authorize`)
- **Query Parameters**:
- `client_id`: Bose Spotify Client ID
- `response_type`: `code`
- `redirect_uri`: `http://localhost` (often used as a placeholder or specifically handled by the app's internal webview/proxy)
- `scope`: `user-read-private user-read-email ...`
- `state`: A base64-encoded JSON object containing metadata, e.g., `{"service": "SPOTIFY"}`.
### Redirect (Browser to App)
Upon successful login and authorization, Spotify redirects the browser to a URL that the SoundTouch app intercepts.
- **URL Format**: `soundtouch://bose/musicservice/spotify/login?code=[AUTH_CODE]&state=[STATE]`
- **App Action**: The `UIMain` component (in `ui_main.js`) handles this "deep link". It extracts the `code` from the query parameters and prepares to send it to the background worker.
---
## 1. OAuth Token Exchange (Bose Cloud)
After the UI intercepts the redirect and extracts the `code`, it sends a `createOAuthAccountRequest` to the background `SpotifyWorker`. The worker then performs the exchange for a Bose-mediated token.
### What is a "Bose-mediated token"?
The "Bose-mediated token" is a token issued by the Bose OAuth proxy. When the app (or device) requests a token via `oauth.streaming.bose.com`, Bose's service performs the actual OAuth2 exchange with Spotify.
- **It is not directly a Spotify refresh token**: Instead, it is a Bose-issued token that *represents* the underlying Spotify session.
- **Token Version 3**: Modern firmware uses `token_version_3`, which signifies that the device doesn't store the raw Spotify tokens but instead uses a Bose-specific "secret" that the Bose Cloud uses to fetch fresh Spotify access tokens on the device's behalf.
- **Access vs Refresh**: The initial response from the `.../token/cs` endpoint typically contains an `access_token` (valid for ~1 hour) and a `token_type: "Bearer"`. The Bose cloud service manages the persistent refresh token internally.
### Internal Message (UI to Worker)
- **Message Type**: `createOAuthAccountRequest`
- **Payload**:
```json
{
"source": "SPOTIFY",
"code": "[AUTH_CODE_FROM_REDIRECT]",
"credentialType": "token_version_3"
}
```
### Outgoing Request (Worker to Bose OAuth Proxy)
- **Endpoint**: `https://oauth.streaming.bose.com/oauth/account/[ACCOUNT_ID]/music/musicprovider/15/token/cs`
- **Method**: `POST`
- **Headers**:
- `Content-Type: application/json`
- `Accept: application/json`
- `Authorization: Bearer [SESSION_TOKEN]` (The user's Bose account session token)
### Payload (JSON)
```json
{
"grant_type": "authorization_code",
"code": "[AUTH_CODE_FROM_SPOTIFY]",
"redirect_uri": "http://localhost"
}
```
### curl Example
```bash
curl -X POST "https://oauth.streaming.bose.com/oauth/account/[ACCOUNT_ID]/music/musicprovider/15/token/cs" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer [SESSION_TOKEN]" \
-d '{
"grant_type": "authorization_code",
"code": "[AUTH_CODE_FROM_SPOTIFY]",
"redirect_uri": "http://localhost"
}'
```
---
## 2. Cloud Source Registration (Marge)
The app now registers the Spotify account with the Bose "Marge" service. This makes the source available across all devices linked to the same Bose account.
### Request Details
- **Endpoint**: `https://streaming.bose.com/streaming/account/[ACCOUNT_ID]/source`
- **Method**: `POST`
- **Headers**:
- `Content-Type: application/vnd.bose.streaming-v1.1+xml`
- `Authorization: [MARGE_TOKEN]`
- `GUID: [DEVICE_GUID]`
- `ClientType: Stockholm`
### Payload (XML)
```xml
<?xml version="1.0" encoding="UTF-8"?>
<source>
<username>[SPOTIFY_USER_ID]</username>
<sourceproviderid>15</sourceproviderid>
<credential type="token_version_3">[SECRET_TOKEN_OBTAINED_IN_STEP_1]</credential>
<sourcename>[DISPLAY_NAME_E_G_EMAIL]</sourcename>
</source>
```
### curl Example
```bash
curl -X POST "https://streaming.bose.com/streaming/account/[ACCOUNT_ID]/source" \
-H "Content-Type: application/vnd.bose.streaming-v1.1+xml" \
-H "Authorization: [MARGE_TOKEN]" \
-d '<?xml version="1.0" encoding="UTF-8"?><source><username>[USER]</username><sourceproviderid>15</sourceproviderid><credential type="token_version_3">[TOKEN]</credential><sourcename>[NAME]</sourcename></source>'
```
---
### Local Device Sync (LISA API)
The app notifies the physical SoundTouch speaker about the new source. This is usually done via the device's management API on port 8090.
#### Modern Flow (OAuth)
- **Endpoint**: `http://[DEVICE_IP]:8090/setMusicServiceOAuthAccount`
- **Method**: `POST`
- **Payload**:
```xml
<OAuthCredentials source="SPOTIFY" displayName="[DISPLAY_NAME]">
<user>[SPOTIFY_USER_ID]</user>
<code>[AUTH_CODE_OR_TOKEN]</code>
<version>token_version_3</version>
</OAuthCredentials>
```
#### Marge-Sync Notification (Fall-back)
If the speaker returns `1029 UNKNOWN_ACTION_ERROR`, it signifies the LISA API version is too old for the OAuth flow. Stockholm-based firmware often expects the account to be registered in Marge first, followed by a notification to sync.
- **Endpoint**: `http://[DEVICE_IP]:8090/notification`
- **Method**: `POST`
- **Payload**:
```xml
<updates deviceID="[DEVICE_UID]">
<sourcesUpdated></sourcesUpdated>
</updates>
```
#### Legacy Flow (Fall-back)
For older firmware that doesn't use Marge for Spotify:
- **Endpoint**: `http://[DEVICE_IP]:8090/setMusicServiceAccount`
- **Method**: `POST`
- **Payload**:
```xml
<credentials source="SPOTIFY" displayName="Spotify Premium">
<user>[USER]</user>
<pass>[TOKEN]</pass>
</credentials>
```
---
## Implementation in SoundTouch-Service
This project implements the "Bose-mediated token" flow as follows:
1. **Surrogate Secrets**: When a user links their Spotify account via `soundtouch-service`, the service generates a 32-character hex string (a "Bose Secret").
2. **Marge & LISA registration**: This secret is sent to the speaker and stored in the emulated Marge cloud as the `credential`. The raw Spotify refresh token never leaves the server.
3. **Token Refresh Proxy**: When the speaker needs a fresh Spotify `access_token`, it calls the `soundtouch-service` proxy (`/oauth/device/.../token/cs3`) providing this secret. The server maps the secret back to the actual Spotify account, performs the refresh with Spotify, and returns a fresh short-lived `access_token` to the speaker.
---
## Placeholders and Constants
| Placeholder | Description |
|:------------------|:----------------------------------------------------|
| `[ACCOUNT_ID]` | The internal Bose account ID (UUID). |
| `[SESSION_TOKEN]` | Temporary token from Bose login. |
| `[MARGE_TOKEN]` | Persistent authorization token for Marge services. |
| `[DEVICE_GUID]` | Unique identifier for the controller app instance. |
| `[DEVICE_IP]` | Local IP address of the SoundTouch speaker. |
| `15` | Constant `sourceproviderid` for Spotify. |
| `token_version_3` | Credential type for modern OAuth2 Spotify accounts. |
---
## Resulting Persistence
Once these requests succeed, the device updates its `/mnt/nv/BoseApp-Persistence/1/Sources.xml` file:
```xml
<source displayName="user@example.com" secret="[SECRET_BLOB]" secretType="token_version_3">
<sourceKey type="SPOTIFY" account="user" />
</source>
```

Some files were not shown because too many files have changed in this diff Show More