Compare commits
6 commits
feat/20260
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 34847cad58 | |||
| a04672b4d0 | |||
| bfc2d4c312 | |||
| 96fb092f35 | |||
| 4d3ab2e92f | |||
| 6de77d23b6 |
5 changed files with 192 additions and 23 deletions
|
|
@ -16,30 +16,42 @@ jobs:
|
||||||
|
|
||||||
- name: Build OCI image
|
- name: Build OCI image
|
||||||
run: |
|
run: |
|
||||||
nix build .#solidhaus-image \
|
# Try with cache first, fall back to building from source
|
||||||
|
nix build .#kammer-image \
|
||||||
--print-build-logs \
|
--print-build-logs \
|
||||||
--show-trace
|
--show-trace || {
|
||||||
|
echo "Build failed, retrying with --no-substitute to build from source..."
|
||||||
|
nix build .#kammer-image \
|
||||||
|
--print-build-logs \
|
||||||
|
--show-trace \
|
||||||
|
--option substitute false
|
||||||
|
}
|
||||||
|
|
||||||
- name: Push to registry
|
- name: Push to registry
|
||||||
if: github.ref == 'refs/heads/main'
|
if: github.ref == 'refs/heads/main'
|
||||||
run: |
|
run: |
|
||||||
image=$(nix build --no-link --print-out-paths .#solidhaus-image)
|
# Ensure temp directory exists for skopeo
|
||||||
|
mkdir -p /var/tmp /tmp
|
||||||
|
|
||||||
|
image=$(nix build --no-link --print-out-paths .#kammer-image)
|
||||||
skopeo copy \
|
skopeo copy \
|
||||||
--dest-tls-verify=false \
|
--dest-tls-verify=false \
|
||||||
|
--tmpdir /tmp \
|
||||||
"docker-archive:$image" \
|
"docker-archive:$image" \
|
||||||
"docker://registry.toph.so/solidhaus:latest"
|
"docker://registry.toph.so/kammer:latest"
|
||||||
|
|
||||||
# Also tag with commit SHA
|
# Also tag with commit SHA
|
||||||
skopeo copy \
|
skopeo copy \
|
||||||
--dest-tls-verify=false \
|
--dest-tls-verify=false \
|
||||||
|
--tmpdir /tmp \
|
||||||
"docker-archive:$image" \
|
"docker-archive:$image" \
|
||||||
"docker://registry.toph.so/solidhaus:${GITHUB_SHA:0:7}"
|
"docker://registry.toph.so/kammer:${GITHUB_SHA:0:7}"
|
||||||
|
|
||||||
- name: Build summary
|
- name: Build summary
|
||||||
if: github.ref == 'refs/heads/main'
|
if: github.ref == 'refs/heads/main'
|
||||||
run: |
|
run: |
|
||||||
echo "### ✅ Image Built and Pushed" >> $GITHUB_STEP_SUMMARY
|
echo "### ✅ Image Built and Pushed" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- **Image**: registry.toph.so/solidhaus:latest" >> $GITHUB_STEP_SUMMARY
|
echo "- **Image**: registry.toph.so/kammer:latest" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- **Tag**: ${GITHUB_SHA:0:7}" >> $GITHUB_STEP_SUMMARY
|
echo "- **Tag**: ${GITHUB_SHA:0:7}" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- **Commit**: ${GITHUB_SHA}" >> $GITHUB_STEP_SUMMARY
|
echo "- **Commit**: ${GITHUB_SHA}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
|
||||||
156
features/item-uid-generation.feature
Normal file
156
features/item-uid-generation.feature
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
Feature: Item UID Generation - Short Collision-Resistant IDs
|
||||||
|
As a user creating a new item
|
||||||
|
I want to see a pre-generated short UID (7 characters)
|
||||||
|
So that IDs remain compact, memorable, and shareable while avoiding collisions
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given I am on the "New Item" form
|
||||||
|
|
||||||
|
Rule: UIDs are pre-generated on form load
|
||||||
|
|
||||||
|
Scenario: UID is pre-generated when form loads
|
||||||
|
When the form loads
|
||||||
|
Then I should see a "UID" field
|
||||||
|
And the UID field should contain a 7-character ID
|
||||||
|
And the UID should use only characters from "23456789abcdefghjkmnpqrstuvwxyz"
|
||||||
|
And the UID should be displayed prominently near the top of the form
|
||||||
|
|
||||||
|
Scenario: UID field is read-only with regenerate option
|
||||||
|
When the form loads
|
||||||
|
Then the UID field should be read-only or disabled for manual editing
|
||||||
|
And I should see a "Regenerate" button/icon next to the UID field
|
||||||
|
And the regenerate control should be clearly clickable
|
||||||
|
|
||||||
|
Rule: Users can regenerate UIDs before submission
|
||||||
|
|
||||||
|
Scenario: User regenerates UID once
|
||||||
|
Given the form has loaded with UID "qe3saa3"
|
||||||
|
When I click the "Regenerate" button
|
||||||
|
Then a new 7-character UID should be generated
|
||||||
|
And the new UID should be different from "qe3saa3"
|
||||||
|
And the new UID should be displayed in the UID field immediately
|
||||||
|
|
||||||
|
Scenario: User regenerates UID multiple times
|
||||||
|
Given the form has loaded with UID "qe3saa3"
|
||||||
|
When I click the "Regenerate" button
|
||||||
|
And the new UID is "n4p7m2k"
|
||||||
|
And I click the "Regenerate" button again
|
||||||
|
Then another new 7-character UID should be generated
|
||||||
|
And each generated UID should be unique
|
||||||
|
And the latest UID should replace the previous one
|
||||||
|
|
||||||
|
Scenario: Regenerated UID is used on submission
|
||||||
|
Given the form has loaded with UID "qe3saa3"
|
||||||
|
And I fill in "Name" with "Cable Drum"
|
||||||
|
When I click the "Regenerate" button
|
||||||
|
And the new UID is "n4p7m2k"
|
||||||
|
And I submit the form
|
||||||
|
Then the item should be created with shortId "n4p7m2k"
|
||||||
|
And the barcode URI should be "https://haus.toph.so/n4p7m2k"
|
||||||
|
And the original UID "qe3saa3" should NOT be used
|
||||||
|
|
||||||
|
Rule: UID format and collision resistance
|
||||||
|
|
||||||
|
Scenario: UID uses safe alphabet
|
||||||
|
When a UID is generated
|
||||||
|
Then it should use alphabet "23456789abcdefghjkmnpqrstuvwxyz"
|
||||||
|
And it should exclude confusing characters (0, 1, i, l, o)
|
||||||
|
And it should be exactly 7 characters long
|
||||||
|
And all characters should be lowercase
|
||||||
|
|
||||||
|
Scenario: Collision probability is negligible
|
||||||
|
Given the alphabet has 29 characters (excluding 0, 1, i, l, o)
|
||||||
|
And the UID length is 7 characters
|
||||||
|
Then the total possible IDs should be 29^7 = 17,249,876,309
|
||||||
|
And collision probability for 1,000 items should be approximately 0.000003%
|
||||||
|
And collision probability for 10,000 items should be approximately 0.0003%
|
||||||
|
And collision probability for 100,000 items should be approximately 0.03%
|
||||||
|
|
||||||
|
Scenario Outline: UID examples are valid
|
||||||
|
When a UID "<uid>" is generated
|
||||||
|
Then it should match the pattern "^[23456789a-hjkmnp-z]{7}$"
|
||||||
|
And it should NOT contain any of "0 1 i l o"
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
| uid |
|
||||||
|
| qe3saa3 |
|
||||||
|
| n4p7m2k |
|
||||||
|
| 2jk8xab |
|
||||||
|
| zzz9999 |
|
||||||
|
| 2222222 |
|
||||||
|
|
||||||
|
Rule: UID field UI/UX specifications
|
||||||
|
|
||||||
|
Scenario: UID field placement
|
||||||
|
When the form loads
|
||||||
|
Then the UID field should be positioned near the top
|
||||||
|
And it should appear before or after the "Name" field
|
||||||
|
And it should be clearly labeled "UID" or "Item ID"
|
||||||
|
|
||||||
|
Scenario: UID field styling
|
||||||
|
When the form loads
|
||||||
|
Then the UID field should use monospace font
|
||||||
|
And it should have larger text size for readability
|
||||||
|
And it should have letter-spacing for clarity
|
||||||
|
And the field should visually indicate it's read-only
|
||||||
|
|
||||||
|
Scenario: Regenerate button styling
|
||||||
|
When the form loads
|
||||||
|
Then the "Regenerate" button should be compact
|
||||||
|
And it should use an icon (🔄 or ↻) or short text
|
||||||
|
And it should be positioned inline with the UID field (right side)
|
||||||
|
And it should have a clear hover state
|
||||||
|
And it should have appropriate spacing from the UID field
|
||||||
|
|
||||||
|
Scenario: Regenerate button interaction
|
||||||
|
When I hover over the "Regenerate" button
|
||||||
|
Then it should show a visual hover state
|
||||||
|
And optionally show a tooltip "Generate new ID"
|
||||||
|
When I click the "Regenerate" button
|
||||||
|
Then the button should show brief loading/animation feedback
|
||||||
|
And the new UID should appear immediately
|
||||||
|
|
||||||
|
Rule: UID persistence and submission
|
||||||
|
|
||||||
|
Scenario: UID is submitted with item
|
||||||
|
Given the form has pre-generated UID "qe3saa3"
|
||||||
|
And I fill in "Name" with "Laptop"
|
||||||
|
And I fill in "Category" with "Electronics"
|
||||||
|
When I submit the form
|
||||||
|
Then the item should be created with shortId "qe3saa3"
|
||||||
|
And the database should store shortId as primary key
|
||||||
|
And the item detail page should display "qe3saa3"
|
||||||
|
And the barcode should encode "https://haus.toph.so/qe3saa3"
|
||||||
|
|
||||||
|
Scenario: Creation timestamp stored separately
|
||||||
|
Given the form has pre-generated UID "qe3saa3"
|
||||||
|
When I submit the form at 2024-02-27T14:30:00Z
|
||||||
|
Then the item should have shortId "qe3saa3"
|
||||||
|
And the item should have createdAt "2024-02-27T14:30:00Z"
|
||||||
|
And the item should have updatedAt "2024-02-27T14:30:00Z"
|
||||||
|
And timestamps should be independent of the UID
|
||||||
|
|
||||||
|
Rule: Edge cases and validation
|
||||||
|
|
||||||
|
Scenario: Duplicate UID is detected (rare collision)
|
||||||
|
Given an item exists with UID "qe3saa3"
|
||||||
|
And the form generates the same UID "qe3saa3" by chance
|
||||||
|
When I submit the form
|
||||||
|
Then the system should detect the collision
|
||||||
|
And the system should automatically generate a new UID
|
||||||
|
And the item should be created successfully with a different UID
|
||||||
|
And the user should be notified "ID regenerated due to conflict"
|
||||||
|
|
||||||
|
Scenario: UID persists during form edits
|
||||||
|
Given the form has loaded with UID "qe3saa3"
|
||||||
|
When I fill in "Name" with "Cable"
|
||||||
|
And I change "Name" to "Cable Drum"
|
||||||
|
And I fill in other fields
|
||||||
|
Then the UID should remain "qe3saa3"
|
||||||
|
And the UID should NOT auto-regenerate on field changes
|
||||||
|
|
||||||
|
Scenario: UID regenerates on explicit action only
|
||||||
|
Given the form has loaded with UID "qe3saa3"
|
||||||
|
When I interact with any form field
|
||||||
|
Then the UID should remain unchanged
|
||||||
|
And the UID should only change when "Regenerate" is clicked
|
||||||
8
flake.lock
generated
8
flake.lock
generated
|
|
@ -38,16 +38,16 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1771903837,
|
"lastModified": 1771848320,
|
||||||
"narHash": "sha256-sdaqdnsQCv3iifzxwB22tUwN/fSHoN7j2myFW5EIkGk=",
|
"narHash": "sha256-0MAd+0mun3K/Ns8JATeHT1sX28faLII5hVLq0L3BdZU=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "e764fc9a405871f1f6ca3d1394fb422e0a0c3951",
|
"rev": "2fc6539b481e1d2569f25f8799236694180c0993",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"ref": "nixos-25.11",
|
"ref": "nixos-unstable",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
26
flake.nix
26
flake.nix
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
description = "SolidHaus — Local-first household inventory app";
|
description = "Kammer — Local-first household inventory app";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
flake-parts.url = "github:hercules-ci/flake-parts";
|
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||||
sd-card.url = "git+ssh://git@git.toph.so/toph/sd-card";
|
sd-card.url = "git+ssh://git@git.toph.so/toph/sd-card";
|
||||||
};
|
};
|
||||||
|
|
@ -29,8 +29,8 @@
|
||||||
in {
|
in {
|
||||||
packages = {
|
packages = {
|
||||||
# Build the SvelteKit app
|
# Build the SvelteKit app
|
||||||
solidhaus = buildNpmPackage {
|
kammer = buildNpmPackage {
|
||||||
pname = "solidhaus";
|
pname = "kammer";
|
||||||
version = "0.0.1";
|
version = "0.0.1";
|
||||||
|
|
||||||
src = ./.;
|
src = ./.;
|
||||||
|
|
@ -48,13 +48,13 @@
|
||||||
|
|
||||||
meta = {
|
meta = {
|
||||||
description = "Local-first household inventory app with barcode scanning";
|
description = "Local-first household inventory app with barcode scanning";
|
||||||
homepage = "https://git.toph.so/toph/solidhaus";
|
homepage = "https://git.toph.so/toph/kammer";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# OCI image with nginx serving the built app
|
# OCI image with nginx serving the built app
|
||||||
solidhaus-image = pkgs.dockerTools.buildLayeredImage {
|
kammer-image = pkgs.dockerTools.buildLayeredImage {
|
||||||
name = "registry.toph.so/solidhaus";
|
name = "registry.toph.so/kammer";
|
||||||
tag = "latest";
|
tag = "latest";
|
||||||
|
|
||||||
contents = with pkgs; [
|
contents = with pkgs; [
|
||||||
|
|
@ -85,7 +85,7 @@
|
||||||
|
|
||||||
# Copy built app
|
# Copy built app
|
||||||
mkdir -p usr/share/nginx/html
|
mkdir -p usr/share/nginx/html
|
||||||
cp -r ${config.packages.solidhaus}/* usr/share/nginx/html/
|
cp -r ${config.packages.kammer}/* usr/share/nginx/html/
|
||||||
|
|
||||||
# Create nginx config
|
# Create nginx config
|
||||||
cat > etc/nginx/nginx.conf <<'EOF'
|
cat > etc/nginx/nginx.conf <<'EOF'
|
||||||
|
|
@ -147,22 +147,22 @@
|
||||||
devPort = 5173; # Vite default port
|
devPort = 5173; # Vite default port
|
||||||
};
|
};
|
||||||
|
|
||||||
default = config.packages.solidhaus;
|
default = config.packages.kammer;
|
||||||
};
|
};
|
||||||
|
|
||||||
apps = {
|
apps = {
|
||||||
# Push image to registry
|
# Push image to registry
|
||||||
push-solidhaus-image = {
|
push-kammer-image = {
|
||||||
type = "app";
|
type = "app";
|
||||||
program = pkgs.lib.getExe (pkgs.writeShellApplication {
|
program = pkgs.lib.getExe (pkgs.writeShellApplication {
|
||||||
name = "push-solidhaus-image";
|
name = "push-kammer-image";
|
||||||
runtimeInputs = [pkgs.skopeo];
|
runtimeInputs = [pkgs.skopeo];
|
||||||
text = ''
|
text = ''
|
||||||
image=$(nix build --no-link --print-out-paths .#solidhaus-image)
|
image=$(nix build --no-link --print-out-paths .#kammer-image)
|
||||||
skopeo copy \
|
skopeo copy \
|
||||||
--insecure-policy \
|
--insecure-policy \
|
||||||
"docker-archive:$image" \
|
"docker-archive:$image" \
|
||||||
"docker://registry.toph.so/solidhaus:latest"
|
"docker://registry.toph.so/kammer:latest"
|
||||||
'';
|
'';
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
1
result
Symbolic link
1
result
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/nix/store/3dna41ydmr1ia5fiicgzf19p1z68l10v-solidhaus.tar.gz
|
||||||
Loading…
Add table
Reference in a new issue