>https://starshipos-f18.com/bin/view/Inquiries/]], newest first. Open one for the whole message and to set its status. Only the administrator can see this space.
+
+{{liveData id="inquiries" source="liveTable" sourceParameters="className=Inquiries.Code.InquiryClass&translationPrefix=" properties="doc.title,doc.creationDate,name,email,topic,status" sort="doc.creationDate:desc" limit="25"}}{{/liveData}}
diff --git a/site/org/inquiries/InquirySheet.xwiki b/site/org/inquiries/InquirySheet.xwiki
new file mode 100644
index 00000000..7a78e4a1
--- /dev/null
+++ b/site/org/inquiries/InquirySheet.xwiki
@@ -0,0 +1,26 @@
+{{velocity}}
+#set ($o = $doc.getObject('Inquiries.Code.InquiryClass'))
+#if (!$o)
+This page holds no inquiry.
+#else
+[[← All inquiries>>Inquiries.WebHome]]
+
+|=Received|$escapetool.xml($xwiki.formatDate($doc.creationDate))
+|=Status|$doc.display('status', $o)
+|=Name|$doc.display('name', $o)
+|=Email|$doc.display('email', $o)
+|=Organization|$doc.display('organization', $o)
+|=Topic|$doc.display('topic', $o)
+|=Announcements|$doc.display('announcements', $o)
+|=Source|$doc.display('source', $o)
+
+== Message ==
+
+$doc.display('message', $o)
+
+#if ($xcontext.action == 'view')
+## the e-mail is checked as plain characters when it arrives (PublicInquiry), and escaped here too
+{{html}}
Set its status #if ("$!o.getProperty('email').value" != '') · Reply by e-mail #end
{{/html}}
+#end
+#end
+{{/velocity}}
diff --git a/site/org/inquiries/PublicInquiry.WebHome.xwiki b/site/org/inquiries/PublicInquiry.WebHome.xwiki
new file mode 100644
index 00000000..0ff60d79
--- /dev/null
+++ b/site/org/inquiries/PublicInquiry.WebHome.xwiki
@@ -0,0 +1,41 @@
+{{velocity}}
+## Receives the inquiry form on starshipos-f18.com (POST) and files it as a page under
+## Inquiries.Entries, which only the administrator can read. It runs with this page's author's
+## rights (getDocumentAsAuthor, saveAsAuthor): a guest can reach nothing but this. Nothing a visitor
+## sends is ever written into this page's output or an entry's title: with this author's rights,
+## text that reached the wiki markup could run a macro.
+#macro (field $n $max)#set ($v = "$!request.getParameter($n)")#set ($v = $v.trim())#if ($v.length() > $max)#set ($v = $v.substring(0, $max))#end#end
+#if ($request.method != 'POST')
+This page receives the inquiry form on [[starshipos-f18.com>>https://starshipos-f18.com/bin/view/Inquiries/]].
+#else
+#field('website' 200)#set ($trap = $v)
+#field('name' 200)#set ($name = $v)
+#field('email' 200)#set ($email = $v)
+#field('organization' 200)#set ($org = $v)
+#field('topic' 100)#set ($topic = $v)
+#field('message' 10000)#set ($message = $v)
+#set ($wants = "$!request.getParameter('announcements')" != '')
+#if ($trap != '')
+## a robot filled the field people cannot see: say thanks, keep nothing
+{{html}}
Thank you. Your inquiry has been received.
{{/html}}
+#elseif ($name == '' || $message == '' || !$email.matches('^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'))
+{{html}}
Your inquiry was not sent: a name, a valid e-mail address and a message are needed. Back to the form .
{{/html}}
+#else
+#set ($stamp = $datetool.format('yyyyMMdd-HHmmss', $datetool.date))
+#set ($ref = "Inquiries.Entries.I${stamp}-$mathtool.random(1000, 9999)")
+#set ($entry = $xwiki.getDocumentAsAuthor($ref))
+#set ($d = $entry.setTitle("Inquiry $stamp"))
+#set ($o = $entry.newObject('Inquiries.Code.InquiryClass'))
+#set ($d = $o.set('name', $name))
+#set ($d = $o.set('email', $email))
+#set ($d = $o.set('organization', $org))
+#set ($d = $o.set('topic', $topic))
+#set ($d = $o.set('message', $message))
+#if ($wants)#set ($d = $o.set('announcements', 1))#else#set ($d = $o.set('announcements', 0))#end
+#set ($d = $o.set('source', 'starshipos-f18.com'))
+#set ($d = $o.set('status', 'new'))
+#set ($d = $entry.saveAsAuthor('An inquiry from the .com form'))
+{{html}}
Thank you. Your inquiry has been received; every message is read by a human, and replies usually come within a few days.
Back to starshipos-f18.com
{{/html}}
+#end
+#end
+{{/velocity}}
diff --git a/site/org/inquiries/install.sh b/site/org/inquiries/install.sh
new file mode 100755
index 00000000..bbdb2e76
--- /dev/null
+++ b/site/org/inquiries/install.sh
@@ -0,0 +1,115 @@
+#!/bin/bash
+# install.sh -- inquiries on the .org XWiki (Captain Bob, 2026-11-07: the .com's inquiries go to an
+# admin-only page on the .org, master/detail):
+#
+# Inquiries.WebPreferences view, edit, comment, delete: $ADMIN_USER alone -- made first
+# Inquiries.Code.InquiryClass the fields (made by Inquiries.Code.ClassSetup, viewed once as the
+# administrator: the class has no REST API), bound to the sheet
+# Inquiries.Code.InquirySheet the detail: one inquiry, its status set by editing the page
+# Inquiries.WebHome the master: every inquiry, newest first (a Live Data table)
+# Inquiries.Entries.I
one page per inquiry, made by PublicInquiry
+# PublicInquiry.WebHome public: the .com's form posts here; it files the inquiry with its
+# author's rights and thanks the visitor, echoing nothing they sent
+#
+# XWIKI_USER=rajames XWIKI_PASSWORD=... site/org/inquiries/install.sh [https://starshipos-f18.org]
+#
+# It ends by checking a guest's view, then sends one test inquiry as a guest, finds it as the
+# administrator, and deletes it.
+set -euo pipefail
+cd "$(dirname "$0")"
+base=${1:-https://starshipos-f18.org}
+rest="$base/rest/wikis/xwiki"
+user=${XWIKI_USER:?set XWIKI_USER}
+pass=${XWIKI_PASSWORD:?set XWIKI_PASSWORD}
+admin=${ADMIN_USER:-XWiki.$user}
+auth=(-u "$user:$pass")
+
+xml_page() { # xml_page
+ python3 -c '
+import sys
+from xml.sax.saxutils import escape
+print("%s xwiki/2.1 "
+ "%s " % (escape(sys.argv[1]), escape(open(sys.argv[2]).read())))' "$1" "$2"
+}
+page() { # page
+ local spaces=${1//\//\/spaces\/}
+ curl -fsS -o /dev/null -w " page ${1//\//.}.$2: %{http_code}\n" "${auth[@]}" -X PUT \
+ -H 'Content-Type: application/xml' --data-binary "$(xml_page "$3" "$4")" "$rest/spaces/$spaces/pages/$2"
+}
+objects() { # objects : how many
+ local spaces=${1//\//\/spaces\/}
+ curl -fsS "${auth[@]}" -H 'Accept: application/json' "$rest/spaces/$spaces/pages/$2/objects/$3" |
+ python3 -c 'import json,sys; print(len(json.load(sys.stdin).get("objectSummaries") or []))'
+}
+object() { # object ...: add one, as XML
+ local spaces=${1//\//\/spaces\/} s=$1 p=$2 c=$3; shift 3
+ local body
+ body=$(python3 -c '
+import sys
+from xml.sax.saxutils import escape
+props = "".join("%s " % (escape(k), escape(v))
+ for k, v in (a.split("=", 1) for a in sys.argv[2:]))
+print("%s %s " % (escape(sys.argv[1]), props))' "$c" "$@")
+ curl -fsS -o /dev/null -w " ${s//\//.}.$p: $c added (%{http_code})\n" "${auth[@]}" -X POST \
+ -H 'Content-Type: application/xml' --data-binary "$body" "$rest/spaces/$spaces/pages/$p/objects"
+}
+
+echo "=== $base, as $user"
+echo "=== rights first: Inquiries is the administrator's alone"
+page Inquiries WebPreferences "Inquiries rights" /dev/null
+if [ "$(objects Inquiries WebPreferences XWiki.XWikiGlobalRights)" = 0 ]; then
+ object Inquiries WebPreferences XWiki.XWikiGlobalRights \
+ "levels=view,edit,comment,delete" "users=$admin" "groups=" "allow=1"
+else
+ echo " rights there already, left as they are"
+fi
+
+echo "=== the class, its sheet"
+page Inquiries/Code InquiryClass "Inquiry class" /dev/null
+page Inquiries/Code ClassSetup "Inquiry class setup" ClassSetup.xwiki
+out=$(curl -fsS "${auth[@]}" "$base/bin/view/Inquiries/Code/ClassSetup?basicauth=1")
+case "$out" in
+ *CLASS-SAVED*) echo " the class's fields: made";;
+ *CLASS-UNCHANGED*) echo " the class's fields: there already";;
+ *) echo "install: the class setup did not run (programming right?)" >&2; exit 1;;
+esac
+page Inquiries/Code InquirySheet "Inquiry sheet" InquirySheet.xwiki
+if [ "$(objects Inquiries/Code InquiryClass XWiki.ClassSheetBinding)" = 0 ]; then
+ object Inquiries/Code InquiryClass XWiki.ClassSheetBinding "sheet=Inquiries.Code.InquirySheet"
+else
+ echo " sheet bound already"
+fi
+
+echo "=== the master, and the public intake"
+page Inquiries WebHome "Inquiries" Inquiries.WebHome.xwiki
+page PublicInquiry WebHome "Inquiry" PublicInquiry.WebHome.xwiki
+
+echo "=== as a guest"
+bad=
+for p in Inquiries:401 PublicInquiry:200; do
+ code=$(curl -s -o /dev/null -w '%{http_code}' -H 'Accept: application/json' "$rest/spaces/${p%%:*}/pages/WebHome")
+ if [ "$code" = "${p##*:}" ]; then echo " ${p%%:*}.WebHome: $code -- as it should be"
+ else echo " ${p%%:*}.WebHome: $code -- NOT as it should be"; bad=1; fi
+done
+[ -z "$bad" ] || { echo "install: a guest's view is wrong; see above" >&2; exit 1; }
+
+echo "=== a test inquiry, sent as a guest"
+reply=$(curl -fsS -X POST "$base/bin/view/PublicInquiry/" \
+ --data-urlencode "name=install.sh test" --data-urlencode "email=test@example.org" \
+ --data-urlencode "topic=General question" --data-urlencode "message=A test from site/org/inquiries/install.sh; it deletes this.")
+case "$reply" in *"has been received"*) echo " thanked";; *) echo "install: no thanks for the test" >&2; exit 1;; esac
+found=$(curl -fsS "${auth[@]}" -H 'Accept: application/json' "$rest/spaces/Inquiries/spaces/Entries/pages" |
+ python3 -c '
+import json,sys
+print(" ".join(p["name"] for p in json.load(sys.stdin)["pageSummaries"]))')
+test_page=
+for n in $found; do
+ if curl -fsS "${auth[@]}" -H 'Accept: application/json' \
+ "$rest/spaces/Inquiries/spaces/Entries/pages/$n/objects/Inquiries.Code.InquiryClass/0/properties/name" |
+ grep -q 'install.sh test'; then test_page=$n; fi
+done
+[ -n "$test_page" ] || { echo "install: the test inquiry was not filed" >&2; exit 1; }
+echo " filed as Inquiries.Entries.$test_page"
+curl -fsS -o /dev/null -w " deleted: %{http_code}\n" "${auth[@]}" -X DELETE \
+ "$rest/spaces/Inquiries/spaces/Entries/pages/$test_page"
+echo "=== done: $base/bin/view/Inquiries/"