尚未收集儲存庫資訊。
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗Ahoj osle!
Stav k 11. 10. 2026 (commit po
cbac3fa). README bylo celé zrevidované proti kódu: sekce po sekci, s čísly řádků. Kde se kód a tenhle text rozejdou, pravdu má kód. Text pak oprav ve stejném commitu, ve kterém měníš kód.
Tohle čte Claude, který přebírá práci na appce Habity v nové konverzaci.
Cílem je zahučet do věci bez ztráty kontextu. Čti to celé, ať chápeš nejen
co tam je, ale hlavně proč. Spousta rozhodnutí má za sebou odladěné hrany.
Stručný rozcestník a pracovní pravidla jsou v CLAUDE.md. Uživatelský návod
pro lidi je v NAVOD.md.
Osobní tracker návyků, úkolů, pohybu a podpůrných vět. Jednosouborová PWA
(index.html), hostovaná na GitHub Pages (haryzek.github.io/habity/).
Roste organicky, „intuitivně, po malých kouscích mezi jinou prací“. Features
se přidávají podle reálné potřeby, ne dopředu přeengineerované.
Sdílená dohoda Bob ↔ Claude: stavět dál do jednoho souboru, dokud to drží
pohromadě. Claude má proaktivně křiknout, až bude refactoring nebo rozdělení
souboru na místě. Aktuálně má soubor ~5340 řádků a ~240 KB. Pořád se to dá
udržet, ale je to hranice, kde už se vyplatí hledat přes grep a ne číst
celé. Bob řezání nemá rád dělat předčasně.
PWA: iOS meta tagy a manifest i ikona jsou inline jako data: URI
(display:standalone). Service worker není, takže appka jde přidat na
plochu, ale offline cache nemá.
E:\_dev\habity (Claude Code, Windows/PowerShell).
Edituje se přímo index.html, po zásahu git commit a git push do main.
GitHub Pages pak sám nasadí (pár desítek sekund; ověř curl na Pages URL).ccr-…, ta se pushne a na Bobův pokyn se mergne do main. Cloud
nevidí složku local/ ani gh CLI. Po cloudové práci si Bob dělá lokálně
git pull.local/. Je globálně gitignorovaná přes
core.excludesFile (viz Bobova paměť). V repu je .gitignore jen na
.claude/ a _scratch/.E:\_dev\habity, Task Scheduler, viz 5c). Skript
je odtud pushuje a předtím si udělá pull --rebase. Lokál je jejich zdroj,
takže lokálně se kvůli nim pullovat nemusí. Pull je potřeba jen po práci
v cloudu. Cloudová session má naopak před pushem nebo mergem udělat
git fetch, protože na GitHubu mezitím mohly přibýt Garmin commity.index.html načti znovu před zásahem. Nespoléhej na paměť
z chatu, konverzace bývají dlouhé a paměť klame.new Function(code) nad obsahem
<script>) a párování tagů.$(npm root -g)/playwright, prohlížeče v /opt/pw-browsers). Postup:
file://…/index.html.localStorage["habity.v2"] na testovací stav.reload().pageerror.
Takhle byly ověřené Mix, slider i „terapie“.habity.v2. Nové pole doplní normalize().
Na přejmenování systémových backlogů je čítač blV (viz 3).Spodní lišta = 6 tabů = 6 sekcí. Vizuální pořadí zleva: Litánie ·
Progress · Tasks · Habits · Free · Move (ikony ❝ ▤ ✓ ↻ ☆ ◉). Při šířce
max-width:430px se schovají popisky (.tab .tl) a zůstanou jen ikony.
Ikony optický centruje centerTabIcons() (měří glyf na canvasu, volá se po
document.fonts.ready).
⚠️ Past: pořadí v navu ≠ curSection index. Litánie je v navu první, ale
má curSection=5. Udělalo se to schválně, aby se nepřečíslovávala Move
(curSection===4 je na spoustě míst v kódu). Pořadí .page sekcí i tabů je
čistě vizuální, logiku řídí index. Když přidáváš sekci, dej jí nový index na
konec a tab umísti, kam chceš.
| idx | tab | panely v tracku | default view | co to je |
|---|---|---|---|---|
| 0 | Progress | 1 (seznam projektů) | 0 | Projekty s odškrtávacími kroky, vážený progres (3c). |
| 1 | Tasks | 1 (Chill náhled + kalendář; v něm i backlog view) | 0 | Kalendářní task manager s backlogy a repeaty (4). Startovní sekce (curSection=1). |
| 2 | Habits | 2: Check · Přehled | 1 (Přehled) | Habit tracker, mřížka 90 dnů (3a). |
| 3 | Free | 2: Check · Přehled | 1 (Přehled) | Volnočasové aktivity se štítky, filtrem a hledáním (3b). |
| 4 | Move | 5: Nálada · Spánek · Garmin · Běh · Otužování | 0 (Nálada) | Kolečka po týdnech (5). |
| 5 | Litánie | 2: Litánie · Otázky | 0 | Dvě kolekce vět s tagy. Soukromá data (6). |
SEC_PAGES / SEC_TRACKS / SEC_TITLES / SEC_DEFAULT_VIEW = [0,0,1,1,0,0]
mají 6 položek. Klik na tab nastaví curSection a resetuje curView na
SEC_DEFAULT_VIEW[curSection].
Názvy sekcí v UI jsou anglicky, zbytek appky česky. hTitle v hlavičce ukazuje
název sekce, jen v Move a Litániích se mění podle plochy:
touch-action: pan-y nechává vertikální scroll prohlížeči. attachSwipe je
napojený na všech 6 sekcí a meze se počítají z počtu panelů
(0..children.length-1, na krajích odpor 0.3). Progress a Tasks mají jen
jeden panel, takže se jen pružně odrazí. Gesta, která začnou na
input[type=range] nebo na řádku rychlých voleb (.blog-quick-row), se
ignorují.applyViewToTrack je generický:
translateX(-min(curView,n-1)*100/n %). curView je jedna globální
proměnná. Když swipneš v Move na view 4 a přejdeš jinam, clamp to ošetří
a klik na tab ho stejně resetuje.
Neaktivní plochy dostanou .panel.off (height:0), jinak by track měl
výšku nejdelší plochy sekce a krátká plocha by scrollovala do prázdna.
Skládá se až po dojetí animace (260 ms); swipe je přes unfoldPanels
rozloží hned, jak se gesto rozhodne pro vodorovný směr.visibility:hidden (ať layout neposkakuje). Tečky 2–4 jsou vidět jen v Move.
Habits, Free a Litánie mají 2 tečky.updateUI() je centrální. Volá resetScrollOnPageChange() (při změně
sekce:view odscrolluje nahoru), posune tracky, přepne
.page.active / .tab.active / tečky, nastaví hTitle a pager a schová
FAB jen na Move.openTaskSheet(id,true)), jinak založí nový projekt.openUtSheet(null).openSheet(curSection-2, null).openLitSheet(curView===1 ? LIT_COLS.ot : LIT_COLS.lit, null).visualViewport nastavuje CSS proměnné
--kb/--vvh, aby sheet nepřekryla klávesnice.⚠️ Past: #trackMove .panel nesmí dostat overflow-y:auto. Vlastní
scrollovací kontejner na Androidu sebere vodorovné gesto (Chrome zahájí scroll
a pošle pointercancel) a swipe jen cukne a odskočí. Move scrolluje oknem jako
všechny ostatní sekce.
⚠️ Layout past (vyřešená): .wrap je display:flex column bez stretch,
takže se smrskával na šířku obsahu a úzká Move se lepila vlevo. Fix je
.wrap{width:100%; max-width:560px}.
Hlavička obsahuje hTitle, datum (hDate) a ozubené kolečko ⚙. To
otevře sheet „Data“ s těmito tlačítky:
habity-zaloha.json s celým state.sections/habits/tasks,
po potvrzení přepíše všechno, projede normalize() a renderAll().state.daily = {}. Popisek je relikt, Přehled
úkolů už neexistuje (viz 4g).<script>)var state = load(): načte habity.v2, jinak zmigruje habity.v1, jinak
blankState(). Vždycky přes normalize().hDate.renderAll(): renderTasksPanel, renderCheck, renderFreeFilterBar,
renderTodayPanel(0/1), renderOverviewPanel(0/1), renderMove,
renderCold, renderVibe, renderSleep, renderRun, refreshLit.updateUI().fetchMoveData() a fetchRunData() (async, tiše).litLoadFromState(): Litánie se tak vykreslí podruhé. Neškodí to, jen první
render proběhne před naplněním cache.centerTabIcons() po načtení fontů.Žádný rollup úkolů se při startu nedělá (zrušen 2. 7. 2026, viz 4e).
Check a Přehled u Habits a Free jsou v jednom tracku vedle sebe a během swipu jsou vidět oba, takže akce na jedné straně musí refreshnout i druhou:
renderTodayPanel i renderOverviewPanel.refreshGodzilla(). renderTodayPanel volá jen když klikneš na
dnešek.openDetail) nesyncuje nic. detailBack přerenderuje jen
Přehled. Když v detailu změníš dnešek, Check panel zůstane zastaralý až do
dalšího renderu (drobný bug, viz 8).Progress a Tasks mají jednu plochu: subtask toggly volají renderTasksPanel,
akce úkolů renderCheck.
habity.v2)PAST: klíče ve
statenesedí na názvy sekcí v UI.
state.tasks= sekce Progress (projekty s kroky).state.utasks= sekce Tasks (úkoly s termínem).state.sections[0]= Habits,state.sections[1]= Free.Slovo „task“ tak v kódu znamená různé věci podle místa. I
data-scope="tasks"u backlog pickeru míří nautasks. Je to historický relikt a záměrně se nepřejmenovává, protože rename klíčů bez migrace by smazal data.
Klíč zůstal habity.v2. Nové struktury přidává normalize() nedestruktivně a
doplní chybějící pole. Migrace v1→v2 existuje: load() načte habity.v1,
znormalizuje ho a zapíše jako v2. Komentář na začátku <script> schéma
neudržuje a odkazuje sem. Platí tohle:
// localStorage "habity.v2"; normalize() doplní vše chybějící
{
// ----- HABITS + FREE -----
sections: [
{ // [0] = HABITS — habits projdou normalize() RAW (bez validace)
habits: [ {id:"h…", name, priority:1-10, createdAt:ms, star?:bool} ], // star = Godzilla
log: { [habitId]: { "YYYY-MM-DD": "done"|"missed" } } // chybí klíč = nic
},
{ // [1] = FREE — ŽÁDNÉ backlogy (normalize zahodí cokoli navíc, i backlog/star)
habits: [ {id, name,
priority:1-10, // v UI „Zábava“ (badge Z{n}), filtr funMin
createdAt:ms,
tags:[…], // z TAG_GROUPS; neznámé se zahodí, TAG_RENAME migruje
durationMin:0-16, // index do DURATION_TICKS, default 0
durationMax:0-16} ], // default 3 (=30min); normalize prohodí min>max
log: { …jako [0]… }
}
],
freeFilter: { tags:{ [groupIdx]:["Tag",…] }, durationMin:0, durationMax:16,
funMin:1, expanded:false, search:"" }, // sdílený Check i Přehledem Free
// ----- PROGRESS -----
tasks: [ {id:"t…", name, createdAt, completedAt:ms|null,
subtasks:[ {id:"s…", name, weight:>=1 (def 5), cells:>=1|null, done:bool} ]} ],
// ŽÁDNÉ priority: pořadí = pořadí v poli
// ----- TASKS -----
backlogs: ["Chill","Done","Wip","Mix", …vlastní], // první čtyři garantované, systémové
blV: 3, // verze systémových backlogů: <2 Wip→Chill + Future→Wip; <3 vlastní „Mix“→„Mix 2“
taskBacklogSel: ["Chill"], // poslední výběr backlogů (jen existující)
mixMinPrio: 10, // práh agregátoru Mix (1-10), „N a víc“
backlogOrder: { [název]: n>=1 }, // pořadí prefixů v rychlých volbách (není v blankState, doplní normalize)
utasks: [ {id:"u…", name, priority:1-10 (def 1), createdAt,
date:"YYYY-MM-DD"|null, // null = bydlí v backlogu; jinak v kalendáři
backlog:"Chill", // domovský backlog; nikdy "Mix" (normalize → Chill), neplatný → Chill
repeat:"none"|"daily"|"weekly"|"biweekly"|"monthly"|"quarterly"|"halfyear"|"yearly",
bouncer:null|{dow:0-6}|{date:"YYYY-MM-DD"},
done:bool} ],
daily: { "YYYY-MM-DD": {done:n, missed:n} }, // denní agregát; počítá se, nikde se nezobrazuje
// ----- MOVE (sekce 5) -----
move: { days:{ "YYYY-MM-DD":{kcal,durMin,distM,hr|null} }, // ze syncu, merge
overrides:{ "YYYY-MM-DD":{kcal,durMin,distM,hr|null} }, // ruční zámky (sync nepřepíše)
caps:{ kcal:600, dist:5000, hrBase:55, hrCap:130 } }, // defaulty; kcal/dist = p90 při importu
vibe: { days:{ "YYYY-MM-DD":{mood,soc,act,body,head} } }, // 1-5 (UI chce všech 5)
sleep: { days:{ "YYYY-MM-DD":{dur, bed:"HHMM"|null, feel:1-10|null, fall:min|null, dream:n|null} } },
run: { days:{ "YYYY-MM-DD":{distM,durSec,movSec,hr|null,vo2|null} },
manual:{ "YYYY-MM-DD":{heavy:0-10|null, ach:0-10|null} } },
cold: { days:{ "YYYY-MM-DD":{tw, ta|null, min, wind|null, flow|null} } },
// ----- KOLEKCE VĚT (LIT_COLS, sekce 6) — SOUKROMÉ, nikdy online. <k> = "lit" | "ot" -----
litBase: [ {id:String, text, temata:[], casti:[], skupiny:[], priorita, oblibena, aktivni} ],
litTemata: ["…", "terapie"], // "terapie" vždy zajištěná (LIT_TEMATA_SEED)
litCasti: [ … ],
litSkupiny: ["Otázky","Litánie","Udělat","Výzvy", …], // seed jen v prázdném stavu / importu
litOverlay: { [id]: {text?,temata?,casti?,skupiny?,priorita?,oblibena?,aktivni?} },
litNew: [ {id:"ul…", …tvar jako litBase} ],
litFilter: { temata:[], casti:[], skupiny:[], prioMin:1, fav:false, noTags:false, expanded:false, search:"" },
otBase:[…], otTemata:[], otCasti:[], otSkupiny:[], otOverlay:{}, otNew:[{id:"uq…"}], otFilter:{…}
}
Konstanty, které se neukládají:
TAG_GROUPS: štítky Free.TAG_RENAME: migrace starých názvů štítků.DURATION_TICKS: 17 kroků 0,5min,15min,30min,1h,2h,3h,6h,1d,3d,1t,2t,1m,3m,6m,1r,Delší.GRID = 90.LIT_COLS a LIT_TAG_RENAME.Proč denní agregát a ne per-task event log: stačily počty zelených a
červených, ne identita úkolů. Agregát má ~15 KB za rok, per-task log by měl
0,5–1 MB za rok a zatěžoval by každý save() (serializuje celý stav při každém
ťuknutí). Dnes se daily nezobrazuje vůbec (Přehled úkolů je zrušený, viz
4g), ale počítá se dál.
sections[0])done, ✕ nastaví nebo zruší missed. Řazení
sortedHabits: priorita DESC, pak createdAt ASC (platí i pro Free).P{priorita}, název, nn/90 a šipka.
Pod ní je mřížka 90 dnů (repeat(30,1fr), 3×30). Klik do buňky cykluje
nic → done → missed → nic (cycle()), takže jde zapisovat i zpětně..name, stopPropagation)
otevře editaci (openSheet), klik na čísla a šipku otevře detail
(openDetail). Detail je jedna souvislá mřížka od nejstaršího dne
(createdAt / nejstarší log) po dnešek, bez měsíčních nadpisů, a má tlačítko
Upravit.star:true (☆/★ v sheetu,
jen pro Habits). Existující habit uloží hvězdičku hned, bez tlačítka Uložit.
V Přehledu je pak úplně nahoře odvozený blok „Godzilla“
(buildGodzillaBlock, barva --god). Má 90 čtverečků a den je modrý,
když žádný ohvězdičkovaný habit nemá ten den missed. Nechecknuté dny
bod nekazí, takže prázdná minulost je modrá (vědomý trade-off). Nic se
neukládá, počítá se za běhu. Buňky nejsou klikací. ★ je vidět v hlavičce
Přehledu i na Check kartě.sections[1])Druhá instance stejné sekce (tentýž kód přes index, tentýž sheet
openSheet(curSection-2,…)). Navíc má štítky, délku, filtr a hledání.
Backlogy Free NEMÁ. Kdysi existovaly, ale v kódu nejsou ani v historii
gitu, a normalize() pole backlog zahodí.
priority se v UI jmenuje „Zábava“ (badge Z{n} místo P{n}).
K tomu tags a durationMin/Max (indexy do DURATION_TICKS). Nový habit má
default Zábavu 5 a délku 0–30min. Změny štítků a délky existujícího habitu se
ukládají hned (persistHabitEditIfExisting).TAG_GROUPS: Plánování[Plan,Spon,We] · Prostředí[Uvnitř,Air] ·
Společnost[Sám,Kámo,Dav] · Energie[Klid,Pohyb,Int,Sport] ·
Mysl[Intel,Fok,Výplach] · Smysl[Smysl,Bzz] ·
Vzdálenost[Dom,Out,Okl,Ph,100,ČR,EU,O]. Každá skupina má vlastní barvu.
Mrtvé mapování: „Sousedi“ se přejmenuje na „Sou“, které v žádné skupině není,
takže se zahodí.freeSearch /
freeFilterBar) i Přehled (freeSearchOv / freeFilterBarOv). Oba jedou přes
jeden state.freeFilter, takže jsou vždy v sync. renderFreeFilterBar staví
všechny .ffbar. Pozor: search input dědí globální
input[type=text]{margin-bottom:12px}, nulováno přes
.free-search-row .free-search.matchesFreeFilter):
priority >= funMin.nn/90 v Přehledu Free (.tofut → addHabitToFuture):
pošle stejnojmenný úkol bez data do Tasks / Chill s prioritou 1. Slouží k
ranním plánům „co budu večer dělat“. Nehotový duplikát v Chill jen toastne.state.tasks) — vážený checklist, pořadí místo prioritreorderTaskInState). U hotového projektu je
skrytý. Nový projekt jde na konec. sortedTasks() vrací nehotové v pořadí
pole, za nimi hotové podle completedAt DESC. Používá ho seznam, FAB i picker
projektů v Tasks.change, protože přeskládat DOM pod prstem by na mobilu utrhlo drag.cells) 1–90.
renumberSubRows se volá po každé změně. Uložení převezme pořadí z DOMu.taskProgress).
cellCounts rozdělí GRID=90 metodou největších zbytků: pevné cells
drží, volné kroky dostanou poměr podle váhy (aspoň 1, když je místo). Dnes
se to používá jen jako flex poměry mini proužku na placce. Hotové kroky
jsou v proužku vlevo (displayOrder).openTaskSheet(id,true)).
Rozbalená placka ukazuje kroky (nehotové v pořadí pole, hotové dole, vpravo
váha) a odkaz „Upravit projekt“. Hotový projekt nemá proužek a název má
--dim. Přidáním kroku se „odhotoví“ (updateTaskCompletedAt).addSubRowFocused(container) je obecný helper „přidat krok + kurzor +
klávesnice“. Používej ho u každého „+ krok“ tlačítka.daily a nespouští piňu.Úkol s date žije v kalendáři, úkol s date=null žije ve svém domovském
backlogu. Kalendářní úkol si domov nese dál (je vidět, až ztratí datum).
| backlog | role (jak s tím Bob pracuje) | technicky |
|---|---|---|
| Chill | Večerní radosti: nápady z Free přes „+“. Top 5 je vidět nad kalendářem, takže je má Bob pořád na očích. | Náhled na Check stránce. První rychlá volba. ✓ na nedatovaném ne-repeat úkolu ho smaže (do Done se nearchivuje). Fallback všude (neplatný backlog → Chill). |
| Wip | „Work in process“: rozdělané věci. Bob jim dává datum nebo opakování, takže žijou v kalendáři. | Default backlog nového úkolu (FAB i kalendář). Schválně není v rychlých volbách, jen v dropdownu. |
| Done | Archiv splněných. | ✓ ne-repeat úkolu → backlog="Done", date=null, done=true. Jen v dropdownu, v sheetu se vybrat nedá. |
| Mix | Agregátor priorit: co hoří napříč vlastními backlogy. | Virtuální, nic v něm nebydlí. Viz 4b. Druhá rychlá volba. |
Historie názvů: 1. 10. 2026 se přejmenovaly systémové backlogy (starý
Wip → Chill, starý Future → Wip, migrace blV<2). 9. 10. 2026 přibyl
Mix (blV<3: případný vlastní backlog „Mix“ se přejmenuje na „Mix 2“).
Future už neexistuje.
Systémové backlogy (Chill/Done/Wip/Mix) nejdou smazat ani přejmenovat
(BACKLOG_SCOPES.tasks.protectedNames). V pickeru jsou nahoře a tučně v pořadí
SYS_BL = ["Done","Wip","Chill","Mix"].
backlogTasks("Mix"))priority >= state.mixMinPrio ze všech
backlogů kromě MIX_EXCLUDE = ["Chill","Done","Wip","Mix"]. Řazení je
priorita DESC, pak createdAt ASC.#mixPrioRow / #mixPrio) je hned pod roletou a
vidět jen, když je Mix ve výběru. Rozsah 1–10, default 10 (jen desítky).
Bob si ho sníží, uloží se do state.mixMinPrio a překreslí se při oninput.fillBacklogPicker vynechá Done a Mix),normalize() přesune případný úkol s backlog:"Mix" do Chill..thome,
makeUtaskRow(t, ctx, showHome)).seen v renderBlogView): úkol se ukáže jen
jednou, v první skupině podle abecedy. Vlastní backlog, který je abecedně
před „Mix“, si proto úkol vezme sám a Mix ho neukáže.renderCheckDefault)Jedna scroll-plocha, shora dolů:
.wipbar):
.wip-sel--mini, data-notext).
Otevře picker s multi-výběrem.#blogQuickRow,
renderBlogQuick()): CHILL · MIX · a pak prefixy.isUpperLetter). Systémové backlogy se nepočítají.state.backlogOrder (skupina bere nejnižší
pořadí svých backlogů). Prefixy bez pořadí jdou na konec, shoda pak
abecedně.applyTaskBacklogMulti).WIP_PREVIEW_MIN = 5). Odkaz „+ N dalších“ není, zbytek je
vidět v backlog view. Řádky mají jen ✓.Backlog view (openBlogView / applyTaskBacklogMulti / renderBlogView)
je plnoobrazovkový výpis. Otevře se vždycky odscrollovaný nahoru
(blogScrollTop, jen při otevření). Ukazuje jen nedatované úkoly vybraných
backlogů (Mix podle 4b). Když je vybraných víc, člení se skupinovými
hlavičkami (abecedně), každá s „+“ (kromě Mixu), které přidá úkol do dané
skupiny. Tlačítko „‹ Zpět“ vrací na default obrazovku.
Picker backlogů (fselOpen):
<select> zůstává skrytý jako zdroj pravdy, viditelné je tlačítko
.fsel-trigger, které otevře vlastní sheet.#blEditScrim): název + „Pořadí v rychlých
volbách“ (celé číslo ≥1, prázdné = na konec). Kontroluje duplicitu.
Přejmenování (renameTaskBacklog) přepíše název na úkolech, ve výběru, v
backlogOrder i v otevřeném sheetu.deleteTaskBacklog). Smaže jeho nedatované úkoly
a datované přesune do Chill."cal"): ✓ splnit · → zítřek · » týden · ✕
nesplněno."backlog"): jen ✓. V Done není nic,
protože Done je log a nic se v něm neodškrtává.Co jednotlivé akce dělají:
utComplete): +1 done k dnešku v daily, piňa 🍹 a hlas (viz 7).
date=null, done=true).utMiss): +1 missed k dnešku, přesune úkol na zítřek. Platí pro
repeat i ne-repeat a je to jediný zdroj červené.utMove): datum úkolu + 1 den, i když je datum v minulosti (bez clampu).
Bez penalizace.utMove): datum + 7 dní. Propadlý úkol se posune o týden od
dneška (clamp na dnešek). Do 11. 10. 2026 tu byl bug, kvůli kterému »
u propadlého úkolu nedělal nic.Červená vzniká JEN z dobrovolného ✕: Bobova ruka, „mohl jsem, zflákl
jsem“. Ruční přesun ani repeat červenou nedělají.
Auto-rollup byl ZRUŠEN 2. 7. 2026 (commit 5149489). Dřív se propadlý
ne-repeat úkol přesouval na dnešek s červenou, i když byl hotový, jen
neodškrtnutý. Teď propadlé úkoly prostě zůstávají pod svým datem, dokud je
Bob neodškrtne nebo nepřesune. rollupUtasks ani lastRollup v kódu
neexistují.
Přehled úkolů (čtverečky z daily) je stejně zrušený (4g), takže dnes je
červená jen interní číslo.
nextRepeatDate, addMonths)Respawn nastává VÝHRADNĚ po ✓. Existuje jedna živá instance, která se
NIKDY neklonuje. Přesun ji jen posouvá. Tohle je tvrdé pravidlo.
{dow}. Řada se kotví na datu splňované
instance (t.date), srovná se na cílový den v týdnu a posouvá se po
7/14 dnech, dokud není striktně po dni splnění.
⚠️ Na kotvu nesahej. Dřív se počítalo „nejbližší dow po dni splnění“ a
biweekly pak vracelo vždycky příští týden. Parita dvoutýdenní řady stojí na
kotvení na t.date.{date}, posun o 1/3/6/12 měsíců, dokud není po dni
splnění. Přetečení 31. na kratší měsíc ošetřuje addMonths.Známé chování, které Bob nechal být: utComplete počítá respawn vždycky
od dneška. Když zakotvený úkol naplánovaný na zítřek splníš dnes, zůstane
na zítřku a jde splnit znovu. Oprava by byla max(dnešek, t.date), vědomě
odloženo.
openUtSheet / utSave)Pole sheetu:
prioColor
(hue 210 chladná → 0 sytě červená).0/d/t/tt/m/3m/6m/Y.Výchozí backlog nového úkolu: presetBacklog (z „+“ u skupiny), jinak v
otevřeném backlog view aktuální blogCurrent, jinak Wip. Mix i Done se
vždycky nahradí Wip.
Repeat bez data:
„→ projekt…“ (fillProjectPicker, projekty z sortedTasks): Uložit
nezaloží ani neuloží úkol. Vepíše jeho název jako nový krok projektu
(weight 5, bez pevných čtverečků) a úkol smaže z utasks. Datum, priorita,
repeat i backlog se zahodí a cesta zpět není. Zapisuje se tam i mrtvé pole
prio:5, které normalize() při načtení zahodí.
Čtverečková plocha Tasks (Dny/Měsíce) je odstraněná (30. 9. 2026), Bob ji
nepoužíval. state.daily se dál plní, jen se nezobrazuje. ⚙ „Vymazat
historii úkolů“ ho maže.
Všechny plochy používají stejnou mřížku:
viewBox 0 0 100 100 a width:100% buňky, takže se škálují
s obrazovkou a tloušťka prstence zůstává konstantní.Ruční plochy (Nálada, Spánek, Běh, Otužování) kreslí mřížku vždycky, aspoň 8 týdnů zpět, aby bylo kam kliknout. Garmin kreslí od prvního týdne s daty a bez dat ukáže jen empty-state.
⚠️ Past v moveSemi(cx,cy,r,side,ratio): side +1 kreslí LEVOU
půlku, side −1 PRAVOU. Je to obráceně, než bys čekal. Oblouk jde od
spodního bodu k hornímu a sweep=1 vede přes levou stranu. Ověřeno přes
getBBox, nesahej na to bez změření.
| view | plocha | hTitle | data |
|---|---|---|---|
| 0 | Nálada | Nálada | ruční state.vibe |
| 1 | Spánek | Spánek | ruční state.sleep |
| 2 | Garmin | Move | sync state.move + ruční zámky |
| 3 | Běh | Běh | sync state.run.days + ruční state.run.manual a state.run.overrides |
| 4 | Otužování | Otužování | ruční state.cold |
renderMove / moveCell, view 2)Účel: „donutit se hýbat“ přes rychlou vizuální gestalt. Jedno mrknutí a víš, jak plnej byl den.
--done) = vzdálenost ke stropu p90.--missed) = tep, škála
(hr−hrBase)/(hrCap−hrBase) = 55→130, clamp 0..1. Baseline je tam proto, aby
prázdná půlka znamenala klid, ne „málo dat“.Stropy (state.move.caps, default kcal 600, dist 5000, hr 55→130):
recalcMoveCaps() počítá p90 (jen hodnoty >0, lineární interpolace) z
efektivních dnů. Běží při importu, uložení zámku a odemčení, ne při
startu, takže vizualizace roste s tebou. Tep je napevno. kJ se zavrhly
(korelují s tep×čas), jede se na kcal.
Ruční zámek dne (override odolný syncu):
null).state.move.overrides[datum].overrides ?? days (moveEffDays()), import sahá
jen do days, takže editace přežije další sync.Appka je statická PWA a s Garminem nikdy nemluví přímo (CORS/OAuth). Tok je jednosměrný:
Garmin Connect
│ local/garmin/build_data.py — token login, 90 dní, agregace po dnech
▼
move_data.json + run_data.json (KOŘEN repa) → git commit + push (jen při změně)
▼
GitHub Pages → appka je při startu tiše fetchne (fetchMoveData + fetchRunData)
move_data.json je pole {date,kcal,durMin,distM,hr,count} (count se
ignoruje). run_data.json je pole {date,distM,durSec,movSec,hr,vo2}
(aktivity s typeKey obsahujícím „running“, sečtené po dnech, hr vážený
délkou).importMoveData / importRunData): datum ze
souboru přepíše uložený den a dny, které v souboru chybí, zůstávají
navždy. Historie tak přežije 90denní okno. Aktivita smazaná v Garminu ale
z appky nezmizí (na to je zámek).#btnRefreshMove) stáhne
move_data.json s ?t= a cache:no-store a ohlásí výsledek toastem. Po
úspěchu tiše stáhne i run_data.json (bez cache-bustu) a zavře ⚙. Ruční
import souboru v UI není.local/garmin/ (mimo git, v cloudu není)build_data.py zapíše oba json soubory do kořene repa a pak udělá
commit a push s pull --rebase --autostash. Mění jen datové soubory, takže
s lidským vývojem nekonfliktuje..garmintoken/ drží token (garth). Heslo je potřeba jen poprvé. Token
vyprší zhruba po roce: ruční běh si pak řekne o heslo, automatický běh jen
zaloguje a skončí.sync.log je log běhů.Habity\GarminSync, pythonw.exe build_data.py, 3× denně v 10:00 / 18:00 / 22:00, StartWhenAvailable.move_data.json a run_data.json jsou veřejně čitelné
na Pages. Bob to vědomě schválil. Osobní sekce se takhle NIKDY vystavovat
nesmí, Pages nemá autentizaci.⚙ → „Exportovat zálohu (JSON)“ stáhne habity-zaloha.json s pevným
názvem (přepisuje se a nehromadí v Downloads). Obsahuje celý state,
tedy i Litánie a Move. Bobův režim: soubor občas houpne do Dropboxu, občas ho
přejmenuje s datem jako milník. localStorage je jediná ostrá kopie, proto
zálohovat pravidelně.
Budoucnost (neřešeno): sync osobních sekcí desktop↔mobil přes privátní autentizovanou vrstvu (ne veřejné Pages). Je to velký samostatný projekt.
renderCold / coldCell, view 4)Čistě ruční zápis (žádný sync, žádné zámky). Dialog coldScrim má pět polí.
tw a min jsou povinné, zbytek smí zůstat prázdný. Existující zápis jde
smazat. Dialog ukazuje „Výkon X ze stropu Y“.
| Prvek | Veličina | Škála |
|---|---|---|
levý půlprstenec (--water #5B87A8) | teplota vody | (20 − tw)/20 |
pravý půlprstenec (--done) | teplota vzduchu | (40 − ta)/60 |
průměr kruhu (--accent) | výkon | sqrt(perf/cap) |
| číslo uvnitř | minuty (desetinná čárka) | — |
| text pod kruhem | vítr (1 desetinné) / průtok (0 desetinných) | — |
min / max(1, tw). Je to pravidlo „minuta za stupeň“, voda ≤1 °C
se počítá jako 1 °C. Průtok do vzorce schválně nevstupuje. Průtok řeky
(m³/s, ČHMÚ) není rychlost proudu v místě ponoru a chlad je už zapsaný v tw.coldCap) drží na COLD_PERF_BASE = 7.5 (Bobův „hodně slušný
den“ = 15 min ve 2 °C). Povolí se jen NAHORU, na p90 zápisů od 4 zápisů
výš. Čisté klouzavé p90 by zlepšení mezi zimami smazalo.~r²), ne poloměr.normalize přijímá tw −5..45 a ta −50..55.renderRun / runCell, view 3)Bobův návrat k běhu (srpen 2026, Po-St-Pá ~2×800 m okolo plavání). Data jsou
ze syncu (run_data.json) a k nim ruční pocity (runScrim: těžkost a
achilovky 0–10, state.run.manual).
Ruční běh (od 27. 10. 2026, když sync selže nebo Bob běžel bez hodinek):
stejný dialog má i km, čas (min) a průměrný tep, předvyplněné ze syncu. Změna
kteréhokoli z nich zapíše state.run.overrides[date] = zámek natvrdo jako
u Move (runEffDays() = overrides ?? days, import ho nepřepíše, buňka má
move-cell--locked → čísla kurzívou). Jen pocity den nezamykají. Nezměněná
pole drží přesnou hodnotu syncu (ne zaokrouhlenou z dialogu); ručně změněný
čas nastaví movSec=0, EF pak jede z durSec. „Smazat zápis“ maže pocity
i zámek, den zas řídí sync.
Filozofie indexu (KOUČOVSKÁ, nesahej na ni bez rozmyslu): index nesmí odměňovat tempo. Bob má achilovky náchylné k zánětu a psychologický odpor, a honění tempa by znamenalo konec. Odměňuje se docházka a klesající tep na fixní trase.
| Prvek | Veličina | Jak |
|---|---|---|
| plocha kruhu | Forma | EF = (distM/movSec)/hr, klouzavý průměr 5 běhů ÷ baseline (průměr prvních 6 EF-běhů, dokud jich je míň, tak všech). Okno RUN_FORMA_LO=0.85 (prázdno) až RUN_FORMA_HI=1.25 (plný = +25 %), lineárně na poloměr. Strop se povolí nahoru na p90 od ≥4 hodnot. Před prvním EF-dnem je forma 1. |
| číslo uvnitř | průměrný tep dne („–“ bez tepu) | má KLESAT |
levý půlprstenec (--done) | konzistence | běžecké dny za 28 dní ÷ 12 |
| pravý půlprstenec | Brzda | acute:chronic z durSec: dny 0–6 ÷ (dny 7–27 / 3). Půlka = 1.0, plný = 2.0, od 1.4 červená. Akutní týden se do reference nepočítá, jinak by restart z nuly byl věčně červený. |
| čísla pod kruhem | těžkost / achilovky | ruční (–/– když chybí; ukazuje se i u neběžeckého dne) |
Běžecký den = distM>0 nebo durSec>0. Den bez tepu nebo vzdálenosti se
počítá do konzistence a Brzdy, ne do EF. #runStats nad mřížkou ukazuje:
renderVibe / vibeCell, view 0, default landing Move)Ruční zápis (vibeScrim, v UI všech 5 povinných, škála 1–5).
| Prvek | Veličina | Škála |
|---|---|---|
průměr kruhu (--accent) | nálada mood | (mood−1)/4 |
| číslo uvnitř | mood | — |
levý půlprstenec (--done) | kontakt soc | soc/5 (1 samota → 5 hodně lidí) |
pravý půlprstenec (--missed) | aktivita act | act/5 (1 línej → 5 frmol) |
| čísla pod kruhem | tělo / hlava | reverzně: tělo 1 forma → 5 na hovno, hlava 1 klid → 5 ruminace turbo |
Prstence se plní po pětinách od v/5. Kdyby se plnily od (v−1)/4, nula by
splývala s nevyplněným dnem.
renderSleep / sleepCell, view 1)Ruční zápis (sleepScrim), povinná je jen délka.
| Prvek | Veličina | Škála |
|---|---|---|
| průměr kruhu | délka dur (h, po 0.5) | 4 h → 8 h, clamp (číslo ukazuje reálnou hodnotu) |
| barva kruhu | délka | --done ≥ 7 h, jinak --missed |
levý půlprstenec (--accent) | usnutí bed "HHMM" | 23:00 plný → 04:00 prázdný. Hodiny 14–22 = „před 23“ (plný), 5–13 = „po 4“ (prázdný) |
pravý půlprstenec (--god) | pocit feel | feel/10 |
| čísla pod kruhem | usínání (min) / sny (počet) | — |
2320, 23:20, 0040 nebo 040 (input má
maxlength=5, nečíslice se zahodí a normalizuje se na "HHMM").Knihovna podpůrných vět, přerámování, manter a vděčností. Vznikla z Bobova Evernotu (10 let poznámek, deduplikace a AI tagging, ~1250 vět). Druhá plocha jsou Otázky, poznávací otázky na cestu pro dva (980 z xlsx). Účel je dvojí: čistit (vyhazovat balast) a tvořit (přidávat nové).
⚠️ Litánie a otázky jsou hodně osobní a NESMÍ jít na veřejný GitHub/Pages. Zdrojové soubory (
local/litanie.json,local/otazky/…) se nikdy necommitujou. Data žijí jen v localStorage a v JSON exportu.Proč ne Pages fetch: Pages je veřejné bez autentizace. Strategie „pushnout, jednou stáhnout a smazat“ je iluze bezpečí (zůstane historie gitu, forky, cache, archivy).
Kolekce (LIT_COLS): jeden motor a dvě kolekce. Každá je objekt
{key, base, overlay, nw, filter, temata, casti, skupiny, seedSkupiny, seedTemata?, prioMax, prioDef, idPrefix, dom:{…}, lab:{…}}. Všechny funkce
motoru berou c jako první argument (litEffective(c), renderLitList(c),
openLitSheet(c,id), litToggleFav(c,id), litSetAktivni(c,id,on)…).
refreshLit() / litLoadFromState() bez argumentu zpracují všechny kolekce.
| klíč | plocha | priorita | id nových | tagy |
|---|---|---|---|---|
lit | Litánie (view 0) | 1–5, default 2 | ul… | temata (modré, vždy včetně „terapie“) · casti (červené, IFS figury) · skupiny (zelené, seed Otázky/Litánie/Udělat/Výzvy) |
ot | Otázky (view 1) | 1–10, default 6 (skóre z xlsx) | uq… | temata = 11 kategorií z xlsx · casti/skupiny prázdné · bez seedů |
Klíče ve state na kolekci (litColBlank / litColNormalize):
<k>Base: základ z importu.<k>Temata/Casti/Skupiny: definice tagů.<k>Overlay[id]: přepisy základních vět. Klíč je string id. Uložení
editace zapíše všechna pole sheetu, toggle ★ a smazání jen jedno pole.<k>New: vlastní věty.<k>Filter: persistovaný filtr včetně textu hledání (max 100 znaků).normalize ořízne text overlay a new na 4000 znaků. Runtime cache (c.BASE,
c.TEMATA, c.CASTI, c.SKUPINY, c.LOADED) plní litLoadFromState(c).
Migrace a seedy tagů:
LIT_TAG_RENAME je jednorázové idempotentní přejmenování přes
litRenArr (např. Postoje → Litánie, Don-Omnipotent → Don).LIT_SKUPINY_SEED se aplikuje jen v prázdném stavu nebo při importu bez
skupiny.LIT_TEMATA_SEED = ["terapie"] + litEnsureTags (9. 10. 2026):
„terapie“ se doplní na konec modrých témat Litánií, kdykoli chybí (porovnává
bez ohledu na velikost písmen). Běží v litColBlank, každém
litColNormalize i v litImport. Otázky ho nemají.Import (litImport(c,d)): formát
{polozky|litanie:[…], temata:[], casti:[], skupiny:[]}. Naplní base a
definice tagů. Overlay a new drží přes stabilní id. Nemá UI volajícího:
obě kolekce jsou nasáté (Litánie dávno, Otázky 14. 9. 2026) a tlačítka jsou
odstraněná. Při potřebě reimportu ho dočasně zavěs. Komentáře v kódu, které
mluví o importu přes ⚙, jsou zastaralé. Jediná cesta dat dovnitř je dnes
import celé zálohy v ⚙. Zdroj Otázek: local/otazky/Otazky.xlsx →
xlsx2json.py → otazky.json (id q0001…).
Slití (litEffective(c)): základ + overlay (přebije) + new (flag _new).
Každá plocha má vlastní search, ☆ rychlý filtr, filtr bar, počítadlo a
seznam (dom id v LIT_COLS[k].dom). Sheet #litSheet je sdílený: při otevření
nastaví placeholder, text tlačítka pro smazání a max slideru podle kolekce.
P{priorita} · text · ☆/★. Klik na text = editace, klik na
hvězdu = oblíbená. Na seznam je jeden delegovaný click handler.
CAP), nad ním poznámka „…a dalších N“.renderLitFilterBar): tři skupiny chipů bez popisků:
témata modré (#6E8FAE), části červené (#BC5B43), skupiny
zelené (#74955E). Prázdná skupina se schová. Dál slider priorita-min
(1..prioMax), toggle „Bez tagů“, Reset a sbalení (i klikem mimo).
#litFavQuick) sedí vedle searche, ne ve filtr baru, aby měl
filtr plnou šířku.<textarea> (bez maxlength, výška do 40vh), slider
priority 1..prioMax, toggle ★ a tři řádky chipů (prázdné se skryjí). Prázdný
text nejde uložit. Nová věta jde do state[c.nw], úprava základní do
state[c.overlay][id], úprava _new zmutuje objekt.aktivni:false (litSetAktivni), ne fyzické smazání. Po
smazání vyskočí undo lišta (#undoBar) na 6 s s tlačítkem „Vrátit“.
Když overlay držel jen aktivni, smaže se celý. Undo slot je jeden
společný, takže druhé smazání do 6 s přepíše první. Koš ani obnova není:
vyházené jdou vrátit jen ručně v JSONu.videno (👁 a filtr „Neviděné“) sloužil k jednorázovému pročištění.
Je odstraněný (9/2026).Denní vážená rotace (věta dne podle priority a ★) byla pro první verzi
odmítnutá: „na špinavý databázi to stejně servíruje blbosti“. Až bude databáze
pročištěná, je to kandidát na druhé kolo. Data (priorita, oblibena) jsou
připravená.
Tmavé „kokosové“ téma. CSS proměnné v :root:
| proměnná | hodnota | použití |
|---|---|---|
--bg | #15120E | |
--surface | #201B15 | |
--surface2 | #2A241C | |
--border | #3A332A | |
--text | #EDE6D8 | |
--dim | #9A9081 | |
--empty | #34302A | |
--empty2 | #46403A | alternace proužků |
--done | #74955E | |
--done2 | #8FB179 | alternace proužků |
--missed | #BC5B43 | |
--accent | #D9A441 | |
--god | #5C8DC9 | Godzilla, pocit spánku |
--water | #5B87A8 | voda v Otužování |
Fonty: --serif (Georgia…) na názvy, --sans (system) na UI, --mono na
metadata a čísla. Modré jsou jen --god, --water a modré tagy Litánií.
Jinak je paleta zemitá. Bob má rád hustotu (malé paddingy, malé písmo, názvy
14px serif).
🍹 Piňa odměna (pina()): po ✓ úkolu v Tasks vyskočí velké 🍹
uprostřed (@keyframes pinapop, 1.9 s). Restart animace je ošetřený pro
rychlé klikání. Habits ✓ ani kroky Progressu piňu nespouští.
🗣️ Hlasová odměna (speakPina) jde s každou piňou. Systémový TTS řekne
jednu z PINA_LINES („Dej si piňu!“, „Šikovnej osel!“, „Král prdele!“,
„Hustý, vole!“, „To je na piňu!“, „Híhá!“). Hlášky se střídají v rotaci.
Hlas je cs-CZ, když je k dispozici, rychlost 1.05, a předchozí hláška se před
novou zruší (cancel()).
HOTOVÉ a otestované:
blV 2 i 3.Opraveno po auditu 11. 10. 2026:
» u propadlého úkolu.23:20.Známé drobnosti, vědomě nechané být:
<k>Base prázdný, LOADED=false a vlastní nové věty
(<k>New) se nezobrazí (ukazuje se „Načítám…“). Boba to nepálí, base má.Reálně neověřené: dlouhý výpadek syncu nad 90 dní udělá nedoplnitelnou
díru (dá se zvednout okno DAYS ve skriptu). Token Garminu vyprší zhruba po
roce.
Filozofie dalšího vývoje: „Základ máme, bude to o dolaďování během úkolování.“ Čekej drobné UX úpravy z reálného provozu, ne velké přestavby. Nepřeengineeruj dopředu. Bob slíbil, že po dovolené (10/2026) se backlogy v Tasks spolu „pročistí“.
Mapa souboru (orientačně, hledej grep -n):
<style> (:root kolem ř. 15).<section class="page"> (Progress 1
panel, Tasks 1, Habits 2, Free 2, Move 5, Litánie 2), nav (~916), sheety a
modaly.<script>:
TAG_GROUPS, LIT_COLS…), blankState, normalize,
load/save (~1300–1850).sortedTasks, cellCounts, renderTasksPanel,
openTaskSheet).backlogTasks, calendarGroups, nextRepeatDate, utComplete,
makeUtaskRow, renderCheckDefault, renderBlogView, openUtSheet,
backlogy, fselOpen, renderBlogQuick).SEC_*, applyViewToTrack, updateUI, attachSwipe, FAB,
taby, klávesnice).moveSemi, moveCell, renderMove, caps, import/fetch, zámky).litLoadFromState, litImport, litEffective, filtr,
renderLitList, sheet, undo).renderAll a init.Rady pro zásahy:
renderCheck() je router Tasks (default obrazovka nebo backlog view).
renderAll() volá všechny rendery najednou (všechny plochy jsou pořád
v DOMu, jen schované).parseKey, key, today, addDays, addMonths,
fmtCalLabel, bumpDaily, utaskById, sortUtasks, backlogTasks,
calendarGroups, prioColor, repeatShort.move_data.json, musí sedět build_data.py (výstup),
importMoveData (vstup) a moveCell (render).getElementById existují v HTML, a v cloudu pusť headless test
(viz 1, Workflow).blV, přidat
migraci v normalize a upravit seznamy na těchto místech: blankState,
normalize (bl), deleteTaskBacklog, BACKLOG_SCOPES.protectedNames,
SYS_BL v fselOpen a renderBlogQuick, MIX_EXCLUDE, fillBacklogPicker.