TreeMap
A free, open-source disk-space visualizer. Squarified treemap, duplicate hunting, disk-full forecasting, and cleanup that only ever moves files to the Trash.
翻譯準備中
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
Une liste de courses partagée en temps réel, à héberger entièrement sur Cloudflare (Workers + Durable Objects, sans base de données externe).檢視完整 README ↗
Une liste de courses partagée en temps réel, à héberger entièrement sur Cloudflare (Workers + Durable Objects, sans base de données externe).
/l/CODE), avec le
partage natif du téléphone quand il est disponible.2 kg pommes ou yaourts x8, l'app détecte la quantité et l'affiche dans
un badge séparé, modifiable en un clic.Le seul contrôle d'accès à une liste est son code à 6 caractères (32^6, soit ~1 milliard de combinaisons) : il n'y a ni compte ni mot de passe. Toute personne qui obtient le code peut voir et modifier la liste — un rappel affiché sur l'écran d'accueil et dans la liste le précise aux utilisateurs. Aucune limitation de débit n'est en place sur la création ou la lecture d'une liste par code : c'est un choix assumé pour rester un projet personnel simple à héberger, pas un compromis invisible.
Les données de toute liste sont chiffrées au repos côté serveur (AES-GCM,
clé dérivée du code de la liste — voir worker/crypto.ts) : ça protège
contre un accès direct au stockage brut du Durable Object sans connaître le
code, mais pas contre quelqu'un qui a déjà le code/lien de partage, qui peut
ouvrir la liste normalement. À noter : la clé n'a pas plus d'entropie que le
code lui-même (~30 bits) — ça protège d'une lecture accidentelle du
stockage brut, pas d'un adversaire motivé qui l'obtiendrait et le
casserait hors-ligne. Les listes créées avant l'introduction de ce
chiffrement sont migrées automatiquement (sans étape manuelle) : elles sont
stockées en clair jusqu'à leur prochaine écriture, qui les rechiffre.
Les champs reçus du client (WebSocket ou fichier importé) sont revalidés
côté serveur avant stockage/diffusion (worker/reducer.ts) : un identifiant,
une priorité ou une couleur de catégorie mal formés sont corrigés ou ignorés
plutôt que stockés tels quels, et les noms/quantités sont plafonnés en
longueur.
Les photos d'articles sont stockées non chiffrées dans un bucket R2 séparé
(voir worker/index.ts) : même modèle de confiance que le reste (accessible
à qui obtient le code de la liste), plafonnées en taille et en type de
fichier accepté côté serveur (shared/photo.ts), jamais réutilisables d'une
liste à l'autre (id imprévisible, préfixé par le code — voir
worker/photos.ts).
@cloudflare/vite-plugin
pour un dev loop unique (front + Worker tournent dans le même processus,
avec workerd) + vite-plugin-pwa
pour le manifest/service worker.qrcode pour générer les QR codes
côté client.npm install
npm run dev
Ouvre l'URL affichée (http://localhost:5173 par défaut). Le plugin
Cloudflare fait tourner le Worker et le Durable Object localement.
npx wrangler r2 bucket create koikimanke-photos # une seule fois, avant le premier déploiement
npm run deploy
Ceci build le front (vite build, qui produit aussi une config Wrangler
prête à l'emploi dans dist/) puis déploie avec wrangler deploy. Il faut
être connecté à un compte Cloudflare (npx wrangler login la première
fois). Le bucket R2 (photos d'articles) doit exister avant le premier
déploiement : wrangler.json référence son nom mais ne le crée pas
lui-même. En local (npm run dev), le plugin Cloudflare le simule
automatiquement, sans compte ni bucket réel.
L'app est aussi accessible via une URL github.io, en plus de l'URL
Cloudflare Workers : le client est alors servi par GitHub Pages mais parle
toujours à l'unique Worker Cloudflare (API + WebSocket temps réel) via CORS,
donc les deux URLs donnent accès aux mêmes listes partagées.
C'est géré par .github/workflows/pages.yml, qui build le client avec
VITE_SYNC_WORKER_URL pointant vers l'URL publique du Worker (variable de
dépôt vars.DEPLOY_URL, la même que celle utilisée par deploy.yml) puis
publie dist/client sur GitHub Pages. Rien à faire manuellement une fois
Pages activé une première fois dans Settings → Pages (Source: "GitHub
Actions") ; VITE_SYNC_WORKER_URL absente (déploiement Cloudflare seul) =
chemins /api/... relatifs, comportement inchangé.
worker/ Worker Cloudflare (routes API), Durable Object ListRoom,
et reducer.ts (logique pure, testée unitairement)
shared/ Types et logique partagés entre le Worker et le client
(parsing de quantité inclus)
src/ Application front (vue Accueil / vue Liste, composants,
utilitaires : websocket, drag & drop, stockage local…)
e2e/ Tests fonctionnels Playwright (parcours principal, sync
temps réel multi-appareils)
wrangler.json Configuration Cloudflare (Durable Object, assets SPA)
npm run lint # oxlint
npm run typecheck
npm run test:coverage # Vitest — tout le dépôt (shared/, worker/, src/),
# 100% de couverture
npm run test:mutation # Stryker — mutation testing sur la seule logique pure
# (shared/, worker/, color.ts/sort.ts)
npm run test:e2e # Playwright, contre `vite dev`
worker/listRoom.ts (la fine couche Durable Object : stockage, hibernation
WebSocket) n'est volontairement pas couvert par les tests unitaires — toute
sa logique métier vit dans worker/reducer.ts, entièrement testé ; le
comportement de listRoom.ts lui-même est vérifié par les tests e2e contre
une vraie instance vite dev (Worker + Durable Object réels via workerd).
.github/workflows/ : ci.yml (lint, typecheck, tests + couverture,
mutation testing, e2e, audit, build) et deploy.yml (déploiement Cloudflare gaté sur la réussite
de la CI via workflow_run, jamais sur un simple push direct ; vérification
post-déploiement ; tag deploy-N à chaque déploiement réussi pour pouvoir
identifier/revenir à une version). Dependabot et CodeQL sont aussi
configurés.
Chaque liste est un unique objet JSON stocké dans son Durable Object :
articles, catégories, historique des noms déjà utilisés. Les mutations
(ajout, coche, déplacement, catégories…) sont envoyées en WebSocket sous
forme de petits messages typés (shared/types.ts), appliquées côté serveur,
persistées puis rediffusées à tous les clients connectés.
La photo d'un article fait exception : elle vit dans un bucket R2 séparé, un
article ne gardant qu'un identifiant (Item.photoId) vers elle — jamais ses
octets dans le ListState lui-même, qui est rediffusé en entier à chaque
mutation (voir ci-dessus) et mis en cache tel quel côté client
(localStorage). L'intégrer directement y aurait gonflé chaque diffusion et
chaque cache, même pour un appareil qui ne regarde jamais cette photo.