Beaver Habit Tracker Onboard
When switching from Android to iOS, I was unable to find a light-weighted but handy habit tracking app, so I decided to make one by myself :)
For the name of the project, it came from a game called “Against the Storm” (which I spent over 100 hours, highly recommended). In the game, my favorite city builder species is beaver, hoping this web app works as a beaver to save ur precious moments in your fleeting life.
GitHub: https://github.com/daya0576/beaverhabits/ Demo: https://beaverhabits.com/demo/
Tech stacks
Inspired the idea of “web UIs with plain Python” from Three Python trends in 2023, finally chose NiceGUI as the full-stack framework (based on Quasar, Tailwind CSS, FastAPI, …).
So this web app is 100% built with Python <3
Some thoughts after several weeks development:
- Good things ✅
- WebSocket based communication between client and server, works perfectly with Python asyncio.
- Light-weighted session based storage provided, out of the box to use.
- Plenty of UI components provided, straightforward and highly customizable.
- …
- Worries 🤔
- “NiceGUI follows a backend-first philosophy: It handles all the web development details” -> high network latency would be a big issue.
- …
Persistent Storage
As mentioned above, NiceGUI handles everything in server side, high network latency would destroy user experiences.
Some solutions:
- Global CDN: Utilizing a global CDN helps mitigate network latency by distributing content across multiple edge servers located strategically worldwide.
- Self-host option: Providing a self-host option allows users to host the NiceGUI application on their own infrastructure or servers.
- …
In order to provide flexible backend storage options, interfaces were defined with various implementations, e.g. session-based file or user-based file/database.
BTW, the code below leverages the latest features of Python 3.12: PEP 695: Type Parameter Syntax
class CheckedRecord(Protocol):
@property
def day(self) -> datetime.date: ...
@property
def done(self) -> bool: ...
@done.setter
def done(self, value: bool) -> None: ...
def __str__(self):
return f"{self.day} {'[x]' if self.done else '[ ]'}"
__repr__ = __str__
class Habit[R: CheckedRecord](Protocol):
@property
def name(self) -> str: ...
@name.setter
def name(self, value: str) -> None: ...
@property
def priority(self) -> int: ...
@priority.setter
def priority(self, value: int) -> None: ...
@property
def records(self) -> List[R]: ...
def get_records_by_days(self, days: List[datetime.date]) -> List[R]: ...
async def tick(self, record: R) -> None: ...
def __str__(self):
return self.name
__repr__ = __str__
Future..
- Pages:
- Index page
- Habit list page
- Habit detail page, e.g. records over years
- Storage:
- Session-based file storage
- User-based file storage
- User-based sqlite storage
- CICD:
- Custom domain
- Global CDN
- Self-hosting support
- Unit tests & deployment pipeline
- Others:
- Export & Import
- User management
- User timezone