diff --git a/src/wuttapos/sample-data/Customer.csv b/src/wuttapos/sample-data/Customer.csv
new file mode 100644
index 0000000..d5a2e17
--- /dev/null
+++ b/src/wuttapos/sample-data/Customer.csv
@@ -0,0 +1,4 @@
+uuid,customer_id,name,account_holder_uuid,phone_number,email_address
+06958b42-e542-7914-8000-841155e871ba,001,Fred Flintstone,06958b40-8610-7caa-8000-28c14769a3f5,,
+06958b43-5daf-7af5-8000-7fe6c8e19b73,002,Barney Rubble,06958b40-23ee-7eba-8000-7a6081601e4c,,
+06958b43-d443-7fb0-8000-5585fb86c3ce,003,Betty Boop,06958b40-f601-7a3c-8000-638088595cea,,
diff --git a/src/wuttapos/sample-data/Department.csv b/src/wuttapos/sample-data/Department.csv
new file mode 100644
index 0000000..ee03530
--- /dev/null
+++ b/src/wuttapos/sample-data/Department.csv
@@ -0,0 +1,3 @@
+uuid,department_id,name,for_products,for_personnel,exempt_from_gross_sales
+06958b44-cc3c-7b56-8000-997dd91c0d51,01,Grocery,True,False,False
+06958b45-8bb4-79ed-8000-e61ebf1d545d,02,Books,True,False,False
diff --git a/src/wuttapos/sample-data/Employee.csv b/src/wuttapos/sample-data/Employee.csv
new file mode 100644
index 0000000..39b0123
--- /dev/null
+++ b/src/wuttapos/sample-data/Employee.csv
@@ -0,0 +1,2 @@
+uuid,person_uuid,name,public_name,active
+06958b42-17ae-78bd-8000-56322ad38684,06958b40-8610-7caa-8000-28c14769a3f5,Fred Flintstone,,True
diff --git a/src/wuttapos/sample-data/InventoryAdjustmentType.csv b/src/wuttapos/sample-data/InventoryAdjustmentType.csv
new file mode 100644
index 0000000..aca45d8
--- /dev/null
+++ b/src/wuttapos/sample-data/InventoryAdjustmentType.csv
@@ -0,0 +1,4 @@
+uuid,type_code,name
+06958b49-eee0-7a8e-8000-8fa8d1648d55,1,Manual
+06958b4a-7e73-7443-8000-c9a826760dbd,2,Receiving
+06958b4a-c9b8-736d-8000-ee9974208f34,3,Sales
diff --git a/src/wuttapos/sample-data/Person.csv b/src/wuttapos/sample-data/Person.csv
new file mode 100644
index 0000000..2a6444c
--- /dev/null
+++ b/src/wuttapos/sample-data/Person.csv
@@ -0,0 +1,4 @@
+uuid,full_name,first_name,middle_name,last_name
+06958b40-23ee-7eba-8000-7a6081601e4c,Barney Rubble,Barney,,Rubble
+06958b40-8610-7caa-8000-28c14769a3f5,Fred Flintstone,Fred,,Flintstone
+06958b40-f601-7a3c-8000-638088595cea,Betty Boop,Betty,,Boop
diff --git a/src/wuttapos/sample-data/Product.csv b/src/wuttapos/sample-data/Product.csv
new file mode 100644
index 0000000..1e66aca
--- /dev/null
+++ b/src/wuttapos/sample-data/Product.csv
@@ -0,0 +1,3 @@
+uuid,product_id,brand_name,description,size,sold_by_weight,department_uuid,special_order,case_size,unit_cost,unit_price_reg,notes
+06958b47-6c11-7bf9-8000-d47cf9ecabbd,07430500132,Bragg's,Apple Cider Vinegar,32oz,False,06958b44-cc3c-7b56-8000-997dd91c0d51,False,12.0000,3.99000,5.990,This is what notes look like for a product.
+06958b49-46ce-7ded-8000-f5e2504ccb81,07430500116,Bragg's,Apple Cider Vinegar,16oz,False,06958b44-cc3c-7b56-8000-997dd91c0d51,False,12.0000,2.79000,4.490,
diff --git a/src/wuttapos/sample-data/Store.csv b/src/wuttapos/sample-data/Store.csv
new file mode 100644
index 0000000..b09d874
--- /dev/null
+++ b/src/wuttapos/sample-data/Store.csv
@@ -0,0 +1,2 @@
+uuid,store_id,name,active
+06958b38-22d3-79c4-8000-5ac6198dee37,001,Main Store,True
diff --git a/src/wuttapos/sample-data/Tax.csv b/src/wuttapos/sample-data/Tax.csv
new file mode 100644
index 0000000..2b3329f
--- /dev/null
+++ b/src/wuttapos/sample-data/Tax.csv
@@ -0,0 +1,3 @@
+uuid,tax_id,name,rate
+06958b3c-c350-7820-8000-0650492afa09,1,Food,3.00000
+06958b3d-3d8a-7ae4-8000-461c96456694,2,Non-Food,5.00000
diff --git a/src/wuttapos/sample-data/Tender.csv b/src/wuttapos/sample-data/Tender.csv
new file mode 100644
index 0000000..73dba8e
--- /dev/null
+++ b/src/wuttapos/sample-data/Tender.csv
@@ -0,0 +1,4 @@
+uuid,tender_id,name,notes,is_cash,is_foodstamp,allow_cashback,kick_drawer,active
+06958b39-d29b-7f62-8000-914f55432288,CASH,Cash,,True,False,True,True,True
+06958b3a-906d-7d16-8000-369bde264e74,CHECK,Check,,False,False,False,True,True
+06958b3b-bd36-765e-8000-b68a1d64a8de,FS,Food Stamps,,False,True,False,False,True
diff --git a/src/wuttapos/sample-data/Terminal.csv b/src/wuttapos/sample-data/Terminal.csv
new file mode 100644
index 0000000..4f498ff
--- /dev/null
+++ b/src/wuttapos/sample-data/Terminal.csv
@@ -0,0 +1,2 @@
+uuid,terminal_id,name
+06958b39-0ef2-72f1-8000-24b87acb7c6d,099,Test Terminal
diff --git a/src/wuttapos/server/templates/appinfo/index.mako b/src/wuttapos/server/templates/appinfo/index.mako
new file mode 100644
index 0000000..f674138
--- /dev/null
+++ b/src/wuttapos/server/templates/appinfo/index.mako
@@ -0,0 +1,38 @@
+## -*- coding: utf-8; -*-
+<%inherit file="wuttaweb:templates/appinfo/index.mako" />
+
+<%def name="middle_buttons()">
+ ${parent.middle_buttons()}
+
+ % if request.is_root:
+
+ {{ installingSampleData ? "Working, please wait..." : "Install Sample Data" }}
+
+ ${h.form(request.route_url(f"{route_prefix}.install_sample_data"), ref="installSampleDataForm")}
+ ${h.csrf_token(request)}
+ ${h.end_form()}
+ % endif
+%def>
+
+<%def name="modify_vue_vars()">
+ ${parent.modify_vue_vars()}
+
+ % if request.is_root:
+
+ % endif
+%def>
diff --git a/src/wuttapos/server/views/__init__.py b/src/wuttapos/server/views/__init__.py
index f25671c..60cf2b9 100644
--- a/src/wuttapos/server/views/__init__.py
+++ b/src/wuttapos/server/views/__init__.py
@@ -31,7 +31,11 @@ def includeme(config):
# wuttaweb
essential.defaults(
- config, **{"wuttaweb.views.common": "wuttapos.server.views.common"}
+ config,
+ **{
+ "wuttaweb.views.common": "wuttapos.server.views.common",
+ "wuttaweb.views.settings": "wuttapos.server.views.settings",
+ }
)
# wuttapos
diff --git a/src/wuttapos/server/views/settings.py b/src/wuttapos/server/views/settings.py
new file mode 100644
index 0000000..c33b335
--- /dev/null
+++ b/src/wuttapos/server/views/settings.py
@@ -0,0 +1,120 @@
+# -*- coding: utf-8; -*-
+################################################################################
+#
+# WuttaPOS -- Point of Sale system based on Wutta Framework
+# Copyright © 2026 Lance Edgar
+#
+# This file is part of WuttaPOS.
+#
+# WuttaPOS is free software: you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# WuttaPOS is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# WuttaPOS. If not, see .
+#
+################################################################################
+"""
+Views for app settings
+"""
+
+import glob
+import os
+import logging
+
+from wuttaweb.views import settings as base
+
+
+log = logging.getLogger(__name__)
+
+
+class AppInfoView(base.AppInfoView):
+ """
+ We override this view to add the :meth:`install_sample_data()` route.
+
+ See also parent docs, :class:`~wuttaweb:wuttaweb.views.settings.AppInfoView`
+ """
+
+ def install_sample_data(self):
+ """
+ Special view to install sample data. Probably just temporary until
+ something better can be worked out...
+
+ Requires root access; imports (create only) from small CSV
+ files within the WuttaPOS code base.
+ """
+
+ # only root gets to do this
+ if not self.request.is_root:
+ raise self.forbidden()
+
+ # we can't (yet?) tell the importer to import "everything"
+ # because it will think that means "all models" as opposed to
+ # "all files" found, and that raises error for missing files.
+ # so we first inspect the folder to see what's there.
+ models = []
+ samples = self.app.resource_path("wuttapos:sample-data")
+ # nb. should use root_dir param for glob() but requires python 3.10
+ for path in glob.glob(os.path.join(samples, "*.csv")):
+ name, ext = os.path.splitext(os.path.basename(path))
+ models.append(name)
+
+ # import all CSV files found in wuttapos/sample-data, but only
+ # allow new records to be created, never update/delete
+ try:
+ handler = self.app.get_import_handler("import.to_wutta.from_csv")
+ models = [name for name in models if name in handler.importers]
+ kw = dict(input_file_path=samples, allow_update=False, allow_delete=False)
+ handler.process_data(*models, **kw)
+ except Exception as err:
+ log.warning("failed to install sample data", exc_info=True)
+ self.request.session.flash(
+ f"Install failed: {self.app.render_error(err)}", "error"
+ )
+ else:
+ self.request.session.flash("Sample data has been installed.")
+
+ return self.redirect(self.get_index_url())
+
+ @classmethod
+ def defaults(cls, config): # pylint: disable=empty-docstring
+ """ """
+ cls._defaults(config)
+ cls._appinfo_defaults(config)
+ cls._wuttapos_defaults(config)
+
+ @classmethod
+ def _wuttapos_defaults(cls, config):
+ route_prefix = cls.get_route_prefix()
+ permission_prefix = cls.get_permission_prefix()
+ url_prefix = cls.get_url_prefix()
+
+ # install sample data
+ config.add_route(
+ f"{route_prefix}.install_sample_data",
+ f"{url_prefix}/install-sample-data",
+ request_method="POST",
+ )
+ config.add_view(
+ cls,
+ attr="install_sample_data",
+ route_name=f"{route_prefix}.install_sample_data",
+ )
+
+
+def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
+ local = globals()
+
+ AppInfoView = kwargs.get("AppInfoView", local["AppInfoView"])
+
+ base.defaults(config, **{"AppInfoView": AppInfoView})
+
+
+def includeme(config): # pylint: disable=missing-function-docstring
+ defaults(config)