This is a work in progress!
buenzlimarks is a web application for bookmark management and more.
Check it out at marks.buenzli.dev.
The full documentation is embedded in the application (marks.buenzli.dev/docs).
A simple web app for bookmark management
License: The Unlicense
This is a work in progress!
buenzlimarks is a web application for bookmark management and more.
Check it out at marks.buenzli.dev.
The full documentation is embedded in the application (marks.buenzli.dev/docs).
As a user, I want to use buenzlimarks on different devices and different browsers. They all shall show the same persistent bookmarks, so that i can use my bookmarks from anywhere.
If there is no internet connection, the latest known set of bookmarks should be shown (for bookmarks in the local network).
A bookmark stores a URL. Every url should have a schema at the beginning, for example:
http://
https://
file://
If users submit a bookmark with a url that lacks a schema, we should add a default schema of https
, before storing the bookmark in the database.
This is the first example of business logic that we will have in our program. Business logic should go in the module models
.
In front of the saved bookmarks, the favicon of the page should be shown. If the page is not fund, or has no favicon, a default buenzlimarks-icon should be displayed.
Currently, database methods contain expressions like:
self.root_dir.join(format!("users/{user_id}/bookmarks"))
to create the filesystem paths for accessing the files in the database. Constructing these paths inline is error prone.
Create some sort of abstraction (function, module, trait... ??) which is in charge of the file paths. It should expose a type safe API to help the programmer avoid silly mistakes when trying to access the files stored on disk.
It should be possible, to add any active webpage by an extension to the bookmarks. Therefore, it should be asked, in which page and widget to be placed.
which ones are enabled should be configurable at startup time.
to consider:
The GUI needs to group bookmarks by their widget, so the server should be able to provide a list of bookmarks only for a given widget_id.
Implement this additional behavior in the handler get_bookmarks
that already exists. Use the Query
extractor to get the query arguments form the HTTP request. (Similar to how path arguments are extracted with the Path
extractor, for example in delete_bookmark
.)
Query(params): Query<HashMap<String, String>>
The query is an optional set of unordered key-value pairs. In the url, it might look like this:
buenzlimarks.remlse.dev/api/bookmarks?widget_id=blabla
The part after the ?
is the query. widget_id
is the key, blabla
is the value, separated by an equal sign.
You can find more information about this extractor here and here.
All three entities should have five corresponding requests:
Some text appears black for light-mode users. That's a bad contrast on the dark background.
I imagine a <Text></Text>
component which encapsulates default tailwind classes for text. This would be a great opportunity to add "tailwind merge" to the code base.
refer to matklad/how-to-test
Compiling for realease takes a long time on the raspberry pi. Doing the build externally will make updating on it more seamless.
other options:
Building on copr is the option that most seamlessly integrated with the system.
(Maybe even consider making buenzlimarks a systemd service. Update process: dnf update
& systemctl restart
. Nice.)
https://docs.fedoraproject.org/en-US/quick-docs/publish-rpm-on-copr/
Purpose:
Scripts in dev directory may remain so the justfile doesn't get too long. Just recipes can simply call those scripts.
As a user, I want to see bookmarks when navigating to buenzlimarks.remlse.dev
, so that I don't have to remember the URLs of my favorite websites.
I want to be able to click on the bookmarks, so that the linked webpage opens in the same tab. Each bookmark should show the label, not the URL.
Currently, it is not possible to add new bookmarks for users other than the default development user. The reason is that other, newly-created users don't have any pages or widgets yet. A valid bookmark requires an ID of a valid widget. A valid widget, in turn, requires an ID of a valid page. Therefore, it must be possible to create and fetch pages and widgets, before bookmarks are fully functional for new users.
This is in a dire state right now and needs to be improved. Maybe save this as a challenge for Silvia?
tailwind should work!
The response is 502 Bad Gateway
- which comes from caddy, not axum. Very weird!
We are going to use axum to write the rust server. Familiarize yourself with its documentation. Once you have a good idea of what axum is and what it does, use it to write a web server that responds with "Hello world!".
Expect to spend some time on reading. You will probably have to search for and read other resources as well, just to understand the documentation of axum. Once you do, the coding part will be easy.
When a single entity is fetched with an invalid ID, i.e. one that doesn't exist in the database, the server returns a 500 "internal server error" response. It would be more appropriate to return a 404 "not found".
example: GET request to /widgets/invalid-id
This will be a more consistent user experience where everything buenzlimarks-related is served from the same domain.
Also it allows greater flexibility, as we are no longer tied to github pages.
Widgets should be placed in multiple columns in a page. It should be customizable by the user, how many rows he want.
The webserver framework we are using is asynchronous. That means request handlers may be stopped and have to wait while other handlers continue their work. This leads to better performance, because while one handler is waiting for some IO operation like networking or access to the filesystem, another handler may do useful work in the meantime.
You may have noticed the keywords asnyc
and await
in the code base. An asnyc
function can be stopped and wait for some other thing, while other tasks make progress. The await
keyword is used to await the completion of such a task, like networking or filesystem access.
Even though we're technically using async
, our filesystem access is using the blocking1 functions from std
. Our async runtime tokio
has its own functions for accessing the filesystem in an asynchronous manner. Unlike the blocking operations from std
, the return values of such asynchronous filesystem operations need to be awaited with .await
. See the documentation of tokio for examples.
Replace the use of std::fs
with tokio::fs
.
"blocking" is the opposite of "async". When a blocking operation is waiting for something, like the filesystem, other tasks are blocked from making progress. We don't want that! โฉ
SeoORM is nice, but overkill for our purposes. As such, it feels too heavyweight for our purposes.
Remove SeoOrm and replace it with a simple, file based persistence approach.
version
, based on which the service can determine what migrations should be applied.users (strings) match bookmarks.
get-request from api/user/ return bookmarks.
Default values
When a chrome-based browser is restarted, the user-login gets lost. It should persist. In firefox, this works already.
download is very slow. could it be due to slow compression time on the raspberry pi?
Let's send our logs to a monitoring service via OpenTelemetry. One option for a self-hosted monitoring service would be Jaeger.
https://broch.tech/posts/rust-tracing-opentelemetry/
Originally posted by @remlse in #30 (comment)
Currently, our tracing logs go to stdout
and can be retrieved via journalctl
, since we deploy as a systemd service. Sifting through the logs could be much nicer.
Apart from a little enabling work in buenzlimarks itself, I suspect the bigger task would be to self-host Jaeger and wire everything up in the deployment repo.
Node packages are now supported!
As a user, I want to persist bookmarks across browser sessions, so that my added bookmarks from an earlier session will show up in future sessions.
Use something like transform
with regular class constructors, mixins etc. to enable a little more object oriented programming, mimicking what can be done on the server side with Rust's traits.
After the ts-trait experiment with falk, I'm not very keen to do this. There's also no reason to believe we'll have enough business logic client-side for this to actually be useful.
Let's stick to functional-ish style on the client.
As stated in the requirement #10, non-empty widgets should not be able to be deleted.
Most importantly, this should be enforced on the backend.
However, to ensure a good user experience, let's also prevent the user from deleting non-empty widgets in the first place on the frontend. For example, the button that deletes a widget could be disabled and greyed out a little for visual distinction.
And while we're at it, let's also do the same for pages. Same problem there.
A widget shoult be freely placeable on the page and in a column by the user. Changes must be persistent.
Enable users to add new bookmarks. Some of the steps required include:
POST
in this case.)The id of the bookmark should always be generated randomly by the server. However, for deserialization to work easily, the user has to provide some dummy id. make sure to override the user-provided id with a random one. Use the function uuid::Uuid::new_v4()
for this purpose, it provides industry-standard random ids.
A bookmark stores the id of the widget it belongs to. The database should verify that such a widget exists.
The user may provide broken links / urls in the new bookmark. Let's not consider this for now and accept any string as bookmark.
The frontend is a bunch of statically served files located in buenzlimarks/app/dist
. So, a GET request to the root path should serve files in that directory. Axum has an examle of serving files from a directory here. Relevant is the ServeDir
service from tower-http.
Prefix your existing routes (e.g. the hello world stuff) with /api/
.
Also, while you're at it, please change the port from 3000 to 4000. This will make it easier to have both frontend and backend running locally in developer mode.
Once that's done, I can get started with the deployment!
As a user, I want to delete Bookmarks, so that i get rid of bookmarks that are not needed any more.
from gaphor, for documentation diagrams.
C4 diagrams are supported experimentally by mermaid.However, it may be preferrable to drop the C4 specific style and go for simpler diagrams instead. Advantages:
As a user, I want to add new bookmarks, so that I don't have to remember the URLs of my favorite websites.
In Addition to the URL, i want to enter a label
As a user, I want to identify myself and only see my bookmarks. This should also apply to any number of other users. My bookmarks must not be visible to other users.
When creating a new bookmark with an invalid widget_id, the server responds with
500 INTERNAL SERVER ERROR
This is because the database returns DbError::WhoopsieDoopsie
in such a case. We recently created a new error variant NotFound
, let's reuse that. The user will then see the error message:
404 NOT FOUND
That is much better and will suffice for the moment.
In its default state, the UI should be optimized for finding and using the bookmarks.
Editing, configuration etc. should not clutter the main UI.
One approach to achieve this could be to implement a global "edit mode" which activates a large number of otherwise hidden UI elements.
We have already started abstracting over the concept of an entity with our DbEntity
trait. Let's expand this trait to ideally encompass all entity specific behavior.
We will know that we have succeeded once the database doesn't have any methods like get_bookmark
anymore, only get_entity
.
environment variables are sometimes loaded by just
, sometimes by the program... relative paths can mess things up. find a maintainable way to handle this. maybe go back to dotenvy
if that helps.
consider using conditional compilation for the DB directory: in debug mode, it defaults to ../dev/db (unless another directory is specified). in release mode, it should panic at startup if the DB dir is not set.
consider using clap to parse the environment variables, which would also generate documentation about available variables with buenzlimarks --help
.
Create a new endpoint for deleting bookmarks.
We currently don't have any API endpoints to update an entity. Let's start with bookmarks for now. Once we have an 'update' endpoint, we will have an example of every kind of endpoint of a CRUD API!
As a user, I want to group Bookmarks in widgets, so that I can summarize them according to different topics.
The widgets should be blocks that have an editable title.
Non-empty widgets cannot be deleted.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.