about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--.guix-channel8
-rwxr-xr-x.guix-shell7
-rw-r--r--.guix/modules/gn-guile.scm34
-rw-r--r--README.md145
-rw-r--r--doc/git-markdown-editor.md117
-rw-r--r--doc/gn-guile.md1
-rwxr-xr-xgn-guile.sh3
-rw-r--r--gn/data/dataset.scm61
-rw-r--r--gn/data/genotype.scm1
-rw-r--r--gn/data/strains.scm2
-rw-r--r--gn/db/mysql.scm9
-rw-r--r--gn/db/sources/wikidata.scm65
-rw-r--r--gn/db/sparql.scm60
-rw-r--r--gn/runner/gemma.scm15
l---------[-rw-r--r--]guix.scm72
-rwxr-xr-xscripts/lmdb-publishdata-export.scm229
-rwxr-xr-xscripts/precompute/list-traits-to-compute.scm4
-rw-r--r--web/.guix-shell8
-rw-r--r--web/css/gn-template-style.css39
-rw-r--r--web/templates/genenetwork.scm18
-rw-r--r--web/view/brand/aging.scm2
-rw-r--r--web/view/brand/msk.scm2
-rw-r--r--web/view/doc.scm2
-rw-r--r--web/view/markdown.scm29
-rw-r--r--web/view/view.scm12
-rw-r--r--web/webserver.scm158
27 files changed, 824 insertions, 280 deletions
diff --git a/.gitignore b/.gitignore
index 55bfd36..5f81cf8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@ BXD.*
 pheno.txt
 GWA.json
 K.json
+.aider*
diff --git a/.guix-channel b/.guix-channel
new file mode 100644
index 0000000..6e0982a
--- /dev/null
+++ b/.guix-channel
@@ -0,0 +1,8 @@
+(channel
+ (version 0)
+ (directory ".guix/modules")
+ (dependencies
+  (channel
+   (name guix-bioinformatics)
+   (url "https://git.genenetwork.org/guix-bioinformatics")
+   (branch "master"))))
\ No newline at end of file
diff --git a/.guix-shell b/.guix-shell
deleted file mode 100755
index bc81e06..0000000
--- a/.guix-shell
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/bin/bash
-#
-#  . .guix-shell -- guile -L . --fresh-auto-compile --listen=1970
-
-echo "Create a shell to run tools."
-
-guix shell  -L ~/guix-bioinformatics -C -D -F --network coreutils guile guile-dbi guile-dbd-mysql guile-fibers guile-json guile-gnutls guile-readline guile-redis openssl nss-certs gemma parallel tar xz python python-lmdb python-cffi guile-gcrypt guile-hashing time gemma-gn2 $*
diff --git a/.guix/modules/gn-guile.scm b/.guix/modules/gn-guile.scm
new file mode 100644
index 0000000..03f2b14
--- /dev/null
+++ b/.guix/modules/gn-guile.scm
@@ -0,0 +1,34 @@
+;; To use this file to build HEAD of gn-guile:
+;;
+;;   guix build -f guix.scm
+;;
+;; To get a development container
+;;
+;;   guix shell -C -D -f guix.scm
+;;
+
+(define-module (gn-guile)
+  #:use-module ((gn packages guile) #:select (gn-guile) #:prefix gn:)
+  #:use-module (guix gexp)
+  #:use-module (guix utils)
+  #:use-module (guix packages)
+  #:use-module (guix git-download))
+
+(define %source-dir (dirname (dirname (current-source-directory))))
+
+(define vcs-file?
+  (or (git-predicate %source-dir)
+      (const #t)))
+
+(define-public gn-guile
+  (package
+    (inherit gn:gn-guile)
+    (source
+     (local-file "../.."
+                 "gn-guile-checkout"
+                 #:recursive? #t
+                 #:select? vcs-file?))))
+
+;; Add definition for tests should you need it, here.
+
+gn-guile
diff --git a/README.md b/README.md
index 11f892a..79b00bd 100644
--- a/README.md
+++ b/README.md
@@ -2,151 +2,82 @@
 
 This directory provides a Guile web service incl. the new REST API. It is used in conjunction with the Python web services and (very much) WIP.
 
-# Documentation
-
-Start with this file and then the documentation in [doc](./doc/gn-guile.md).
+## Run
 
-# Development
+1. **Navigate to the Web Directory and Start the Server**
 
-The current repository lives at
+Running the web server is documented in [guix script](./web/.guix-shell).
 
-```sh
-git clone tux02.genenetwork.org:/home/git/public/gn-guile
 ```
-
-GNU Guile allows you to develop against a live running web server using emacs-geiser. To try this fire up the web server from the `web` directory as
-
-```sh
-cd web
-unset GUIX_PROFILE
-. .guix-shell -- guile -L .. --listen=1970 -e main ./webserver.scm 8091
+curl http://127.0.0.1:8091/version
+"4.0.0"
 ```
 
-Note the leading dot. The .guix-shell is defined in `gn-guile/web` and loads required packages using GNU Guix.
-If you are on Debian you may need to unset GUIX_PROFILE first.
-
-Next fire up emacs with `emacs-geiser-guile` and connect to the running web server with `M-x geiser-connect` and the port `1970`. Now you can not only inspect procedures, but also update any prodedure on the live server using `C-M-x` and get updated output from the webserver!
-
-# Tools
-
-Some tooling and scripts that run independently are stored in `./scripts`.
-
-Here’s the entire markdown content combined into a single, copyable file:
-
-
-# Gn-Markdown
+# Documentation
 
-Gn-Markdown is an API endpoint to edit, parse, and commit markdown files for gn-docs.
+Start with this file and then the documentation in [doc](./doc/gn-guile.md).
 
-## How to Test the APIs
+# Development
 
-1. **Navigate to the Web Directory and Start the Server**
+The current repository lives at
 
 ```sh
-cd web
-export GN_REPO_PATH=<repo_path>
-. .guix-shell -- guile -L .. --listen=1970 -e main ./webserver.scm 8091
+git clone tux02.genenetwork.org:/home/git/public/gn-guile
 ```
 
-2. **Test Endpoints**
-
-The main endpoints provided are `/edit` and `/commit`. More endpoints may be added in the future.
-
-## Edit (GET)
-
-This is a GET request to retrieve a file's details. Make sure you pass a valid file_path as search_query (the path should be relative to the repo)
-
-**Request Example:**
-
-```bash
-
-curl -G -d "file_path=test.md"  localhost:8091/edit
+or
 
 ```
-
-**Expected Success Response:**
-
-```json
-{
-"file_path": "test.md",
-"content": "Test for new user\n test 2 for line\n test 3 for new line\n ## real markdown two test\n",
-"hash": "ecd96f27c45301279150fbda411544687db1aa45"
-}
+git remote add gn git.genenetwork.org:/home/git/public/gn-guile
 ```
 
-**Expected Error Response (Status 400):**
+GNU Guile allows you to develop against a live running web server using emacs-geiser on port 1970. To try this fire up the web server from the `web` directory as
 
-```json
-{
-"error": <error_type>,
-"msg": <error_reason>
-}
+```sh
+guix shell -L ~/guix-bioinformatics -m manifest.scm --container --network --file=guix.scm -- guile -L . --fresh-auto-compile --listen=1970 -e main web/webserver.scm 8091
 ```
 
-## Commit (POST)
+By default the root points to the API:
 
-This is a POST request to commit changes to a file.
+curl http://127.0.0.1:8091
 
-**Request URL:**
+We also have some services
 
-```bash
+curl http://127.0.0.1:8091/home/msk
+curl http://127.0.0.1:8091/home/aging
 
-curl -X POST http://127.0.0.1:8091/commit \
--H 'Content-Type: application/json' \
--d '{
-"content": "make test commit",
-"filename": "test.md",
-"email": "test@gmail.com",
-"username": "test",
-"commit_message": "init commit",
-"prev_commit": "7cbfc40d98b49a64e98e7cd562f373053d0325bd"
-}'
+The `--container` option runs the code in an isolated container, and the `--network` option connects that container's networking to the host to allow you to access the running service.
 
-```
+If you get an error `no code for module (gn packages guile)` add the appropriate guix-bioinformatics repo to the load path (`-L` switch). Note that, instead of hard-wiring guix-bioinformatics, the recommended way is to use a guix channel as defined in [guix-channel](./.guix-channel).
 
+We recommend checking the Guix documentation for manifests, channels and guix.scm definitions.
 
+## Welcome to the world of interactive Lisp programming
 
-**Expected Response for success:**
+Next fire up emacs with `emacs-geiser-guile` and connect to the running web server with `M-x geiser-connect` and the port `1970`. Now you can not only inspect procedures, but also update any prodedure on the live server using `C-M-x` on code, call and get updated output from the webserver! No need to save/reload files and all that.
 
-```json
-{
-"status": "201",
-"message": "Committed file successfully",
-"content": "Test for new user\n test 2 for line\n test 3 for new line\n ## real markdown two test\n",
-"commit_sha": "47df3b7f13a935d50cc8b40e98ca9e513cba104c",
-"commit_message": "commit by genetics"
-}
+Note that you may have to try different versions of guile+emacs to succeed.
 
-```
+Also these days it may be a better bet to use Andrew Tropin's alternatives ares and arei that are very powerful alternatives to geiser (with a more common lisp type interactive experience):
 
-**If No Changes to File:**
+=> https://git.sr.ht/~abcdw/guile-ares-rs
+=> https://git.sr.ht/~abcdw/emacs-arei
 
-```json
-{
-"status": "200",
-"message": "Nothing to commit, working tree clean",
-"commit_sha": "ecd96f27c45301279150fbda411544687db1aa45"
-}
-```
 
-**Expected Error Response:**
+# Tools
 
-```json
-{
-"error": "system-error",
-"msg": "Commits do not match. Please pull in the latest changes for the current commit *ecd96f27c45301279150fbda411544687db1aa45* and previous commits."
-}
-```
+Some tooling and scripts that run independently are stored in `./scripts`.
 
-## Notes
+# Forwarding a MySQL port
 
-This is meant to be used as api endpoint only to edit any local repo; Clients are expected to handle other service e.g User Interface, authentication
+You may want to forward a mysql port if there is no DB locally you can do something like:
 
-# Development
+ssh -L 3306:127.0.0.1:3306 -f -N tux02.genenetwork.org
 
-```
-git remote add gn git.genenetwork.org:/home/git/public/gn-guile
-```
+# Topics
+
+* More on [gn-guile](./doc/gn-guile.md)
+* Markdown editor with git backend see [markdown](./doc/git-markdown-editor.md).
 
 # LICENSE
 
diff --git a/doc/git-markdown-editor.md b/doc/git-markdown-editor.md
new file mode 100644
index 0000000..f286284
--- /dev/null
+++ b/doc/git-markdown-editor.md
@@ -0,0 +1,117 @@
+# Gn-Markdown
+
+Gn-Markdown is an API endpoint to edit, parse, and commit markdown files for gn-docs.
+
+## How to Test the APIs
+
+1. **Navigate to the Web Directory and Start the Server**
+
+Running the web server is documented in [guix script](./web/.guix-shell).
+
+```
+curl http://127.0.0.1:8091/version
+"4.0.0"
+```
+
+```sh
+cd web
+export CURRENT_REPO_PATH=<path-to-git-repo-with-files>
+export CGIT_REPO_PATH=<path-to-git-bare-repo>
+. .guix-shell -- guile -L .. --listen=1970 -e main ./webserver.scm 8091
+```
+
+
+
+2. **Test Endpoints**
+
+The main endpoints provided are `/edit` and `/commit`. More endpoints may be added in the future.
+
+## Edit (GET)
+
+This is a GET request to retrieve a file's details. Make sure you pass a valid file_path as search_query (the path should be relative to the repo)
+
+**Request Example:**
+
+```bash
+
+curl -G -d "file_path=test.md"  localhost:8091/edit
+
+```
+
+**Expected Success Response:**
+
+```json
+{
+"file_path": "test.md",
+"content": "Test for new user\n test 2 for line\n test 3 for new line\n ## real markdown two test\n",
+"hash": "ecd96f27c45301279150fbda411544687db1aa45"
+}
+```
+
+**Expected Error Response (Status 400):**
+
+```json
+{
+"error": <error_type>,
+"msg": <error_reason>
+}
+```
+
+## Commit (POST)
+
+This is a POST request to commit changes to a file.
+
+**Request URL:**
+
+```bash
+
+curl -X POST http://127.0.0.1:8091/commit \
+-H 'Content-Type: application/json' \
+-d '{
+"content": "make test commit",
+"filename": "test.md",
+"email": "test@gmail.com",
+"username": "test",
+"commit_message": "init commit",
+"prev_commit": "7cbfc40d98b49a64e98e7cd562f373053d0325bd"
+}'
+
+```
+
+
+
+**Expected Response for success:**
+
+```json
+{
+"status": "201",
+"message": "Committed file successfully",
+"content": "Test for new user\n test 2 for line\n test 3 for new line\n ## real markdown two test\n",
+"commit_sha": "47df3b7f13a935d50cc8b40e98ca9e513cba104c",
+"commit_message": "commit by genetics"
+}
+
+```
+
+**If No Changes to File:**
+
+```json
+{
+"status": "200",
+"message": "Nothing to commit, working tree clean",
+"commit_sha": "ecd96f27c45301279150fbda411544687db1aa45"
+}
+```
+
+**Expected Error Response:**
+
+```json
+{
+"error": "system-error",
+"msg": "Commits do not match. Please pull in the latest changes for the current commit *ecd96f27c45301279150fbda411544687db1aa45* and previous commits."
+}
+```
+
+## Notes
+
+This is meant to be used as api endpoint only to edit any local repo; Clients are expected to handle other service e.g User Interface, authentication
diff --git a/doc/gn-guile.md b/doc/gn-guile.md
index 7a86c13..9e84e23 100644
--- a/doc/gn-guile.md
+++ b/doc/gn-guile.md
@@ -4,4 +4,5 @@ The GeneNetwork Guile web server serves an exploratory REST API as well as HTML
 
 Topics are:
 
+* [Markdown editor](./git-markdown-editor.md).
 * [Branding GN](./branding.md)
diff --git a/gn-guile.sh b/gn-guile.sh
new file mode 100755
index 0000000..9341b26
--- /dev/null
+++ b/gn-guile.sh
@@ -0,0 +1,3 @@
+#! @SHELL@
+
+guile -e main web/webserver.scm "$@"
diff --git a/gn/data/dataset.scm b/gn/data/dataset.scm
index c28cf25..afe75ba 100644
--- a/gn/data/dataset.scm
+++ b/gn/data/dataset.scm
@@ -4,14 +4,21 @@
   #:use-module (ice-9 iconv)
   #:use-module (ice-9 receive)
   #:use-module (ice-9 string-fun)
+  #:use-module (srfi srfi-1)
   #:use-module (dbi dbi)
   #:use-module (gn db mysql)
+  #:use-module (gn data genotype)
   #:use-module (gn data group)
   #:use-module (gn util convert)
   #:use-module (web gn-uri)
+  #:use-module (rnrs base) ; for assert
 
   #:export (
             dataset-name
+            get-bxd-publish-list
+            get-bxd-publish-values-list
+            get-bxd-publish-name-value-dict
+            get-bxd-publish-dataid-name-value-dict
             ))
 
 (define (get-dataset db probesetfreeze-id)
@@ -22,3 +29,57 @@
 
 (define (dataset-name db probesetfreeze-id)
   (assoc-ref (get-dataset db probesetfreeze-id) "Name"))
+
+(define (get-dataid-from-publishxrefid id)
+  "Get the internal dataid from publishxref - which is the same as used in the GN2 web interface"
+  (call-with-db
+   (lambda (db)
+     (let [(query (string-append "SELECT Id,PhenotypeId,DataId FROM PublishXRef WHERE Id=" id " AND InbredSetId=1 LIMIT 1"))]
+       (dbi-query db query)
+       (pk (int-to-string (assoc-ref (get-row db) "DataId")))))))
+
+(define (get-bxd-publish-list)
+  (call-with-db
+   (lambda (db)
+     (let [(query "SELECT Id,PhenotypeId,DataId FROM PublishXRef WHERE InbredSetId=1")]
+       (dbi-query db query)
+       (get-rows db '())))))
+
+(define* (get-bxd-publish-values-list dataid #:optional used-for-mapping?)
+  "Returns dict of name values , e.g. [{\"Name\":\"C57BL/6J\",\"value\":9.136},{\"Name\":\"DBA/2J\",\"value\":4.401},{\"Name\":\"BXD9\",\"value\":4.36}, ... used-for-mapping? skips the founders and maybe other unmappable inds. Note, currently unused."
+  (call-with-db
+   (lambda (db)
+     (let [(query (string-append "SELECT Strain.Name, PublishData.value FROM Strain, PublishData WHERE PublishData.Id=" dataid " and Strain.Id=StrainID;"))]
+       (dbi-query db query)
+       (if used-for-mapping?
+           (remove null? (pk (get-rows-apply db
+                                             (lambda (r)
+                                               (if (string-contains (assoc-ref r "Name") "BXD")
+                                                   `(("Name" . ,(assoc-ref r "Name")) ("value" . ,(assoc-ref r "value")))
+                                                   '() ) ;; return empty on no match
+                                               ) '())))
+           (get-rows db '())
+           )))))
+
+(define* (get-bxd-publish-dataid-name-value-dict dataid #:optional used-for-mapping?)
+  "Returns dict of name values, e.g. (((\"C57BL/6J\" . 9.136) (\"DBA/2J\" . 4.401) (\"BXD9\" . 4.36) ... used-for-mapping? skips the founders and maybe other unmappable inds."
+  (call-with-db
+   (lambda (db)
+     (let [(query (string-append "SELECT Strain.Name, PublishData.value FROM Strain, PublishData WHERE PublishData.Id=" dataid " and Strain.Id=StrainID;"))]
+       (dbi-query db query)
+       (if used-for-mapping?
+           (remove null? (pk (get-rows-apply db
+                                             (lambda (r)
+                                               (if (string-contains (assoc-ref r "Name") "BXD")
+                                                   `(,(assoc-ref r "Name") . ,(assoc-ref r "value"))
+                                                   '() ) ;; return empty on no match
+                                               ) '())))
+           (remove null? (pk (get-rows-apply db
+                                             (lambda (r)
+                                               `(,(assoc-ref r "Name") . ,(assoc-ref r "value"))
+                                               ) '())))
+           )))))
+
+(define* (get-bxd-publish-name-value-dict id #:optional used-for-mapping?)
+  "Same as above function, but starting from data id"
+  (get-bxd-publish-dataid-name-value-dict (get-dataid-from-publishxrefid id) used-for-mapping?))
diff --git a/gn/data/genotype.scm b/gn/data/genotype.scm
index c7cb63c..5574382 100644
--- a/gn/data/genotype.scm
+++ b/gn/data/genotype.scm
@@ -16,6 +16,7 @@
             ))
 
 (define (geno-inds-bxd fn)
+  "Returns information from GN's BXD.json, note it fetches the first geno file info, now BXD.8.geno"
   (let [(js (call-with-input-file fn
               (lambda (port)
                 (json->scm port))))]
diff --git a/gn/data/strains.scm b/gn/data/strains.scm
index e5f839b..07b69ff 100644
--- a/gn/data/strains.scm
+++ b/gn/data/strains.scm
@@ -25,7 +25,7 @@
   "Return assoc list of tuples of strain id+names:
    ((4 . BXD1) (5 . BXD2) (6 . BXD5) (7 . BXD6)...
 
-used-for-mapping? will say whether the strains/individuals are used for mapping. Always True, FIXME
+optional key used-for-mapping? will say whether the strains/individuals are used for mapping.
 "
   (call-with-db
    (lambda (db)
diff --git a/gn/db/mysql.scm b/gn/db/mysql.scm
index ccd414a..223b5fd 100644
--- a/gn/db/mysql.scm
+++ b/gn/db/mysql.scm
@@ -32,22 +32,23 @@
     ;; (display "===> OPENING DB")
     ;; (newline)
     (let [(db (dbi-open "mysql" "webqtlout:webqtlout:db_webqtl:tcp:127.0.0.1:3306"))]
-      (ensure db)
+      (ensure db "Can't open connection")
       db
     )))
 
 (define (call-with-db thunk)
   (thunk (db-open)))
 
-(define (ensure db)
+(define (ensure db msg1)
   "Use DBI-style handle to report an error. On error the program will stop."
   (match (dbi-get_status db)
     ((stat . msg) (if (= stat 0)
                      #t
                      (begin
-                       (display msg)
+                       (display "SQL Connection ERROR! ")
+                       (display (string-append msg1 " - " msg)
                        (newline)
-                       (assert stat))))))
+                       (assert #f)))))))
 
 (define (has-result? db)
   "Return #t or #f if result is valid"
diff --git a/gn/db/sources/wikidata.scm b/gn/db/sources/wikidata.scm
new file mode 100644
index 0000000..954ce93
--- /dev/null
+++ b/gn/db/sources/wikidata.scm
@@ -0,0 +1,65 @@
+#!
+
+Wikidata queries, initially lifted over from the gn3 gene-alias code (that was written in Racket).
+
+Note you can take a SPARQL query and push it into https://query.wikidata.org/. E.g. generate a query and
+copy paste into the query service:
+
+scheme@(guile-user) [3]> (display (wikidata-query-geneids "Shh"))
+```
+SELECT DISTINCT ?wikidata_id
+            WHERE {
+              ?wikidata_id wdt:P31 wd:Q7187;
+                           wdt:P703 ?species .
+              VALUES (?species) { (wd:Q15978631 ) ( wd:Q83310 ) ( wd:Q184224 ) } .
+              ?wikidata_id rdfs:label "Shh"@en .
+              }
+```
+
+It is possible to run queries through curl with
+
+```
+curl -G https://query.wikidata.org/sparql -H "Accept: application/json; charset=utf-8" --data-urlencode query="
+    SELECT DISTINCT ?alias
+             WHERE {
+                     wd:Q24420953 rdfs:label ?name ;
+                         skos:altLabel ?alias .
+                     FILTER(LANG(?name) = \"en\" && LANG(?alias) = \"en\").
+                   }"
+```
+!#
+
+(define-module (gn db sources wikidata)
+  #:export (wikidata-query-geneids
+            wikidata-query-gene-aliases
+            )
+)
+
+(define ps-encoded-by "ps:P702")
+(define wdt-instance-of "wdt:P31")
+(define wdt-in-taxon "wdt:P703")
+(define wd-human "wd:Q15978631")
+(define wd-mouse "wd:Q83310")
+(define wd-rat "wd:Q184224")
+(define wd-gene "wd:Q7187")
+(define wd-shh-rat "wd:Q24420953")
+
+(define (wikidata-query-geneids gene_name)
+  "SPARQL query to get the wikidata identifiers pointing to genes of listed species, e.g. 'Shh'"
+  (string-append
+     "SELECT DISTINCT ?wikidata_id
+            WHERE {
+              ?wikidata_id " wdt-instance-of " " wd-gene ";
+                           " wdt-in-taxon " ?species .
+              VALUES (?species) { (" wd-human " ) ( " wd-mouse" ) ( " wd-rat" ) } .
+              ?wikidata_id rdfs:label \"" gene_name "\"@en .}"))
+
+(define (wikidata-query-gene-aliases wikidata_id)
+  "SPARQL query to get a list of gene aliases based on a wikidata identifier, e.g. for Q24420953. This
+version supports the expanded id only, so <http://www.wikidata.org/entity/Q24420953> including the <,>."
+  (string-append
+      "SELECT DISTINCT ?stripped_alias
+             WHERE { " wikidata_id " rdfs:label ?name ;
+                         skos:altLabel ?alias .
+                         BIND (STR(?alias)  AS ?stripped_alias) .
+                     FILTER(LANG(?name) = \"en\" && LANG(?alias) = \"en\").}"))
diff --git a/gn/db/sparql.scm b/gn/db/sparql.scm
index b7d94f3..bd7a306 100644
--- a/gn/db/sparql.scm
+++ b/gn/db/sparql.scm
@@ -2,23 +2,25 @@
 
 Module for handling SPARQL primitives.
 
-Note that GN queries should go into gn/data - this is currently not
+Note that GN queries should go into gn/db/sources - this is currently not
 the case.
 
 !#
 
 (define-module (gn db sparql)
-  #:use-module (json)
-  #:use-module (ice-9 match)
+  #:use-module (gn cache memoize)
+  #:use-module (gn db sources wikidata)
   #:use-module (ice-9 format)
   #:use-module (ice-9 iconv)
+  #:use-module (ice-9 match)
   #:use-module (ice-9 receive)
   #:use-module (ice-9 string-fun)
+  #:use-module (json)
+  #:use-module (srfi srfi-1)
   #:use-module (web client)
+  #:use-module (web gn-uri)
   #:use-module (web request)
   #:use-module (web uri)
-  #:use-module (gn cache memoize)
-  #:use-module (web gn-uri)
 
   #:export (memo-sparql-species
             memo-sparql-species-meta
@@ -26,6 +28,8 @@ the case.
             sparql-groups-meta
             sparql-group-info
             memo-sparql-wd-species-info
+            memo-sparql-wd-gene-aliases
+            memo-sparql-wd-geneids
             compile-species
             compile-groups-meta
             get-rows
@@ -72,7 +76,9 @@ PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
 
 (define (sparql-tsv endpoint-url query)
   "Execute raw SPARQL query returning response as a UTF8 string, e.g.
-(tsv->scm (sparql-tsv (wd-sparql-endpoint-url) \"wd:Q158695\"))
+(tsv->scm (sparql-tsv (wd-sparql-endpoint-url) \"wd:Q158695\")).
+
+Note this procedure works for wikidata, but not for gn!
 "
   ; GET /sparql?query=SELECT%20DISTINCT%20%2A%20where%20%7B%0A%20%20wd%3AQ158695%20wdt%3AP225%20%3Fo%20.%0A%7D%20limit%205 HTTP/2
   (receive (response-status response-body)
@@ -92,7 +98,9 @@ PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
   (unpack "bindings" (unpack "results" response)))
 
 (define (sparql-scm endpoint-url query)
-  "Return dual S-exp 'resultset' of varnames and results"
+  "Return dual S-exp 'resultset' of varnames and results.
+
+Note this procedure works for GN, but does not yet work for wikidata"
   (let ((response (json-string->scm
                    (sparql-exec endpoint-url (gn-sparql-prefix query)))))
    (values (sparql-names response) (sparql-results response))))
@@ -160,9 +168,47 @@ SELECT DISTINCT ?taxon ?ncbi ?descr where {
 
 ")))
 
+(define (flatten lst)
+  (cond ((null? lst) '())
+        ((pair? lst) (append (flatten (car lst)) (flatten (cdr lst))))
+        (else (list lst))))
+
+(define (remove-quotes s)
+  (substring s 1 (- (string-length s) 1)))
+
 (define memo-sparql-wd-species-info
   (memoize sparql-wd-species-info))
 
+(define (sparql-wd-geneids gene-name)
+  "Return a list of expanded wikidata ids, e.g.
+(\"<http://www.wikidata.org/entity/Q14860079>\" \"<http://www.wikidata.org/entity/Q24420953>\")"
+  (receive (type values)
+      (tsv->scm (sparql-tsv (wd-sparql-endpoint-url) (wikidata-query-geneids gene-name)))
+    (map (lambda (item) (car item)) values) ;; flatten list
+    ))
+
+(define memo-sparql-wd-geneids
+  (memoize sparql-wd-geneids))
+
+(define (sparql-wd-gene-aliases geneids)
+  "Returns a flattened and dedpulicated list of geneids with
+(sparql-wd-gene-aliases '(\"Q14860079\" \"Q24420953\"))
+"
+  (let* ([aliases
+         (map (lambda (geneid)
+                (receive (type values)
+                    (tsv->scm (sparql-tsv (wd-sparql-endpoint-url) (wikidata-query-gene-aliases (pk geneid))))
+                  (map (lambda (item) (car item)) values) ;; flatten list))
+                  )
+                ) geneids)]
+         [rm-quotes-aliases (map (lambda (s) (remove-quotes s)) (flatten aliases))]
+         )
+    (delete-duplicates rm-quotes-aliases)))
+
+(define memo-sparql-wd-gene-aliases
+  (memoize sparql-wd-gene-aliases))
+
+
 #!
 gn:Mus_musculus rdf:type gnc:species .
 gn:Mus_musculus gnt:name "Mouse" .
diff --git a/gn/runner/gemma.scm b/gn/runner/gemma.scm
index 9a5c0fc..c577305 100644
--- a/gn/runner/gemma.scm
+++ b/gn/runner/gemma.scm
@@ -10,11 +10,24 @@
   #:use-module (rnrs base)
 
   #:export (
-            write-pheno-file
+            gemma-pheno-txt
             invoke-gemma-wrapper-loco
             run-gemma
             ))
 
+(define (gemma-pheno-txt family traits)
+  "Return a list of values for GEMMA"
+  (assert (string=? family "BXD")) ; only supported right now
+  (define bxd-inds (geno-inds-bxd "BXD.json"))
+  (assert (= 235 (length bxd-inds)))
+  (map (lambda (ind)
+         (let [(value (assoc-ref traits ind))]
+           (if value
+               (format #f "~a" value)
+               "NA\n")
+           ))
+       bxd-inds))
+
 (define (write-pheno-file fn traits)
   (define bxd-inds (geno-inds-bxd "BXD.json"))
   (assert (= 235 (length bxd-inds)))
diff --git a/guix.scm b/guix.scm
index 4417452..973f44f 100644..120000
--- a/guix.scm
+++ b/guix.scm
@@ -1,71 +1 @@
-;; To use this file to build HEAD of gn-guile:
-;;
-;;   guix build -f guix.scm
-;;
-;; To get a development container
-;;
-;;   guix shell -C -D -f guix.scm
-;;
-
-(use-modules
- ((guix licenses) #:prefix license:)
- (guix gexp)
- (guix packages)
- (guix git-download)
- (guix build-system guile)
- (gnu packages algebra)
- (gnu packages base)
- (gnu packages bash)
- (gnu packages compression)
- (gnu packages bioinformatics)
- (gnu packages build-tools)
- (gnu packages certs)
- (gnu packages curl)
- (gnu packages gcc)
- (gnu packages guile)
- (gnu packages guile-xyz)
- (gnu packages llvm)
- (gnu packages ninja)
- (gnu packages parallel)
- (gnu packages perl)
- (gnu packages perl6)
- (gnu packages pkg-config)
- (gnu packages python)
- (gnu packages tls)
- (srfi srfi-1)
- (ice-9 popen)
- (ice-9 rdelim))
-
-(define %source-dir (dirname (current-filename)))
-
-(define %git-commit
-    (read-string (open-pipe "git show HEAD | head -1 | cut -d ' ' -f 2" OPEN_READ)))
-
-(define-public gn-guile-git
-  (package
-    (name "gn-guile-git")
-    (version (git-version "4.0.0-" "HEAD" %git-commit))
-    (source (local-file %source-dir #:recursive? #t))
-    (build-system guile-build-system)
-
-     (inputs
-      (list guile-3.0-latest bash-minimal perl
-            guile-dbi guile-dbd-mysql guile-fibers guile-gnutls guile-readline guile-redis openssl nss-certs gemma parallel))
-     (propagated-inputs
-      (list guile-json-4))
-
-#!
-     (arguments
-      `(#:compile-flags '("--r6rs" "-Wunbound-variable" "-Warity-mismatch")
-        #:modules ((guix build guile-build-system)
-                   (guix build utils)
-                   (srfi srfi-26)
-                   (ice-9 ftw)
-                   (json))))
-!#
-     (home-page "https://git.genenetwork.com/gn-guile")
-     (synopsis "Next generation GN code in guile")
-     (description "Use of guile.")
-     (license license:gpl3)))
-
-gn-guile-git
+.guix/modules/gn-guile.scm
\ No newline at end of file
diff --git a/scripts/lmdb-publishdata-export.scm b/scripts/lmdb-publishdata-export.scm
new file mode 100755
index 0000000..8427112
--- /dev/null
+++ b/scripts/lmdb-publishdata-export.scm
@@ -0,0 +1,229 @@
+#! /usr/bin/env guile
+!#
+;; To run this script run:
+;;
+;; $ guix shell guile guile-hashing guile3-dbi guile3-dbd-mysql \
+;; guile-lmdb -- guile lmdb-publishdata-export.scm
+;; conn.scm
+;;
+;; Example conn.scm:
+;; ((sql-username . "webqtlout")
+;;  (sql-password . "xxxx")
+;;  (sql-database . "db_webqtl")
+;;  (sql-host . "localhost")
+;;  (sql-port . 3306)
+;;  (output-dir . "/tmp/data")
+;;  (log-file . "/tmp/export.log"))
+
+(use-modules (dbi dbi)
+             (rnrs bytevectors)
+             (system foreign)
+             (ice-9 match)
+             (srfi srfi-1)
+             (srfi srfi-26)
+	     (srfi srfi-43)
+             (rnrs io ports)
+             (hashing md5)
+             ((lmdb lmdb) #:prefix mdb:)
+             (ice-9 format)
+             (ice-9 exceptions)
+	     (json)
+             (logging logger)
+             (logging rotating-log)
+             (logging port-log)
+             (oop goops))
+
+
+;; Set up logging
+(define* (setup-logging #:key (log-file "lmdb-dump-log"))
+  "Initialize the logging system with rotating file logs and error port output.
+   Creates a new logger, adds rotating and error port handlers,
+   sets it as the default logger, and opens the log for writing."
+  (let ((lgr (make <logger>))
+	(rotating (make <rotating-log>
+		    #:num-files 3
+		    #:size-limit 1024
+		    #:file-name log-file))
+	(err (make <port-log> #:port (current-error-port))))
+    ;; add the handlers to our logger
+    (add-handler! lgr rotating)
+    (add-handler! lgr err)
+    ;; make this the application's default logger
+    (set-default-logger! lgr)
+    (open-log! lgr)))
+
+(define (shutdown-logging)
+  "Properly shutdown the logging system.
+   Flushes any pending log messages, closes the log handlers,
+   and removes the default logger reference."
+  (flush-log)  ;; since no args, it uses the default
+  (close-log!) ;; since no args, it uses the default
+  (set-default-logger! #f))
+
+(define (call-with-database backend connection-string proc)
+  "Execute PROC with an open database connection.  BACKEND is the
+database type (e.g. \"mysql\").  CONNECTION-STRING is the database
+connection string."
+  (let ((db #f))
+    (dynamic-wind
+      (lambda ()
+        (set! db (dbi-open backend connection-string)))
+      (cut proc db)
+      (lambda ()
+        (when db
+          (dbi-close db))))))
+
+(define (call-with-target-database connection-settings proc)
+  "Connect to the target database using CONNECTION-SETTINGS and execute
+PROC."
+  (call-with-database "mysql" (string-join
+                               (list (assq-ref connection-settings 'sql-username)
+                                     (assq-ref connection-settings 'sql-password)
+                                     (assq-ref connection-settings 'sql-database)
+                                     "tcp"
+                                     (assq-ref connection-settings 'sql-host)
+                                     (number->string
+                                      (assq-ref connection-settings 'sql-port)))
+                               ":")
+                      proc))
+
+(define* (lmdb-save path key value)
+  "Save a NUM with KEY to PATH."
+  (mdb:with-env-and-txn
+     (path) (env txn)
+     (let ((dbi (mdb:dbi-open txn #f 0)))
+       (mdb:put txn dbi key
+		(if (number? value)
+                    (number->string value)
+		    value)))))
+
+(define (sql-exec db statement)
+  "Execute an SQL STATEMENT on database connection DB.  Throws an error
+if the statement execution fails."
+  (dbi-query db statement)
+  (database-check-status db))
+
+(define (sql-fold proc init db statement)
+  "Fold over SQL query results."
+  (sql-exec db statement)
+  (let loop ((result init))
+    (let ((row (dbi-get_row db)))
+      (if row
+          (loop (proc row result))
+          result))))
+
+(define (sql-for-each proc db statement)
+  "Apply PROC to each row returned by STATEMENT."
+  (sql-fold (lambda (row _)
+              (proc row))
+            #f db statement))
+
+(define (sql-map proc db statement)
+  "Map PROC over rows returned by STATEMENT."
+  (sql-fold (lambda (row result)
+              (cons (proc row) result))
+            (list) db statement))
+
+(define (sql-find db statement)
+  (sql-exec db statement)
+  (dbi-get_row db))
+
+(define (database-check-status db)
+  "Check the status of the last database operation on DB.  Throws an
+error if the status code is non-zero."
+  (match (dbi-get_status db)
+    ((code . str)
+     (unless (zero? code)
+       (error str)))))
+
+(define* (save-dataset-values settings)
+  "Main function to extract and save dataset values.  Queries the
+database for datasets and their values, computes MD5 hashes for
+dataset-trait combinations, and saves strain values to LMDB files in
+/export5/lmdb-data-hashes/."
+  (dynamic-wind
+    (lambda ()
+      (setup-logging #:log-file (assq-ref settings 'log-file))
+      (log-msg 'INFO "Starting dataset value extraction"))
+    (lambda ()
+      (call-with-target-database
+       settings
+       (lambda (db)
+         (sql-for-each
+          (lambda (row)
+	    (match row
+	      ((("Name" . dataset-name)
+		("Id" . trait-id))
+			    (let* ((data-query (format #f "SELECT
+JSON_ARRAYAGG(JSON_ARRAY(Strain.Name, PublishData.Value)) AS data,
+ MD5(JSON_ARRAY(Strain.Name, PublishData.Value)) as md5hash
+FROM
+    PublishData
+    INNER JOIN Strain ON PublishData.StrainId = Strain.Id
+    INNER JOIN PublishXRef ON PublishData.Id = PublishXRef.DataId
+    INNER JOIN PublishFreeze ON PublishXRef.InbredSetId = PublishFreeze.InbredSetId
+LEFT JOIN PublishSE ON
+    PublishSE.DataId = PublishData.Id AND
+    PublishSE.StrainId = PublishData.StrainId
+LEFT JOIN NStrain ON
+    NStrain.DataId = PublishData.Id AND
+    NStrain.StrainId = PublishData.StrainId
+WHERE
+    PublishFreeze.Name = \"~a\" AND
+    PublishXRef.Id = ~a AND
+    PublishFreeze.public > 0 AND
+    PublishData.value IS NOT NULL AND
+    PublishFreeze.confidentiality < 1
+ORDER BY
+    LENGTH(Strain.Name), Strain.Name" dataset-name trait-id)))
+			      (match (call-with-target-database
+				      settings
+				      (lambda (db2) (sql-find db2 data-query)))
+				((("data" . data)
+				  ("md5hash" . md5-hash))
+				 (let* ((trait-name (format #f "~a~a" dataset-name trait-id))
+					(base-dir (assq-ref settings 'output-dir))
+					(out (format #f "~a-~a" trait-name
+						     (substring md5-hash 0 12)))
+					(out-dir (format #f "~a/~a" base-dir out)))
+				   (log-msg
+				    'INFO (format #f "Writing ~a to: ~a" trait-name out-dir))
+				   (unless (file-exists? out-dir)
+				     (mkdir out-dir))
+				   (lmdb-save (format #f "~a/index" base-dir) trait-name out)
+				   (vector-for-each
+				    (lambda (_ x)
+				      (match x
+					(#(strain value)
+					 (lmdb-save out-dir strain value))))
+				    (json-string->scm data)))))))))
+          db
+          "SELECT DISTINCT PublishFreeze.Name, PublishXRef.Id FROM
+PublishData INNER JOIN Strain ON PublishData.StrainId = Strain.Id
+INNER JOIN PublishXRef ON PublishData.Id = PublishXRef.DataId
+INNER JOIN PublishFreeze ON PublishXRef.InbredSetId = PublishFreeze.InbredSetId
+LEFT JOIN PublishSE ON
+    PublishSE.DataId = PublishData.Id AND
+    PublishSE.StrainId = PublishData.StrainId
+LEFT JOIN NStrain ON
+    NStrain.DataId = PublishData.Id AND
+    NStrain.StrainId = PublishData.StrainId
+WHERE
+    PublishFreeze.public > 0 AND
+    PublishFreeze.confidentiality < 1
+ORDER BY
+    PublishFreeze.Id, PublishXRef.Id"))))
+    (lambda ()
+      (shutdown-logging))))
+
+(define main
+  (match-lambda*
+    ((_ connection-settings-file)
+     (save-dataset-values (call-with-input-file connection-settings-file
+			    read)))
+    ((arg0 _ ...)
+     (display (format "Usage: ~a CONNECTION-SETTINGS-FILE~%" arg0)
+	      (current-error-port))
+     (exit #f))))
+
+(apply main (command-line))
diff --git a/scripts/precompute/list-traits-to-compute.scm b/scripts/precompute/list-traits-to-compute.scm
index 9f900d1..102a6fa 100755
--- a/scripts/precompute/list-traits-to-compute.scm
+++ b/scripts/precompute/list-traits-to-compute.scm
@@ -15,6 +15,10 @@ You may want to forward a mysql port if there is no DB locally
 
     ssh -L 3306:127.0.0.1:3306 -f -N tux02.genenetwork.org
 
+ignore IPv6 message:
+
+    bind [::1]:3306: Cannot assign requested address
+
 test connection with mysql client:
 
     mysql -uwebqtlout -pwebqtlout -A -h 127.0.0.1 -P 3306 db_webqtl -e "show tables;"
diff --git a/web/.guix-shell b/web/.guix-shell
deleted file mode 100644
index b4aee2a..0000000
--- a/web/.guix-shell
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/bin/bash
-#
-#  run with options '-- ./webserver.scm 8091' e.g.
-#  . .guix-shell -- guile -L .. --fresh-auto-compile --listen=1970 -e main ./webserver.scm 8091
-
-echo "Note run: running web-server"
-
-guix shell guile guile-commonmark guile-fibers guile-json guile-gnutls guile-readline guile-redis openssl nss-certs $*
diff --git a/web/css/gn-template-style.css b/web/css/gn-template-style.css
new file mode 100644
index 0000000..38893c6
--- /dev/null
+++ b/web/css/gn-template-style.css
@@ -0,0 +1,39 @@
+* {
+    box-sizing: border-box;
+}
+
+body {
+    margin: 0.7em;
+    display: grid;
+    grid-template-columns: 9fr 1fr;
+    grid-gap: 20px;
+
+    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+    font-style: normal;
+    font-size: 20px;
+}
+
+#header {
+    grid-column-start: 1;
+    grid-column-end: 3;
+
+    background-color: #336699;
+    color: #FFFFFF;
+    border-radius: 3px;
+    min-height: 30px;
+}
+
+#header #header-text {
+    padding-left: 0.2em;
+}
+
+#main {
+    grid-column-start: 1;
+    grid-column-end: 2;
+
+    max-width: 650px;
+}
+
+#main img {
+    max-width: 650px;
+}
diff --git a/web/templates/genenetwork.scm b/web/templates/genenetwork.scm
new file mode 100644
index 0000000..64e9852
--- /dev/null
+++ b/web/templates/genenetwork.scm
@@ -0,0 +1,18 @@
+(define-module (web templates genenetwork)
+  #:use-module (web view markdown)
+
+  #:export (default-gn-template))
+
+(define* (default-gn-template path #:optional (title "Default Page Template"))
+  "Render `PATH' with a default template and styling that fits in with
+ GeneNetwork's look and feel."
+  `(html
+    (head
+     (meta (@ (charset "UTF-8")))
+     (meta (@ (name "viewport") (content "width=device-width, initial-scale=1.0")))
+     (title ,title)
+     (link (@ (rel "stylesheet") (type "text/css")
+              (href "/css/gn-template-style.css"))))
+    (body
+     (header (@ (id "header")) (span (@ (id "header-text")) "GeneNetwork"))
+     (main (@ (id "main")) ,(markdown-github->sxml path)))))
diff --git a/web/view/brand/aging.scm b/web/view/brand/aging.scm
index 19db4d7..040c711 100644
--- a/web/view/brand/aging.scm
+++ b/web/view/brand/aging.scm
@@ -53,7 +53,7 @@
 ,info)
       (footer
        (hr)
-       (p "Copyright © 2005-2023 "
+       (p "Copyright © 2005-2025 "
           (a (@ (href "https://genenetwork.org/")) "GeneNetwork Webservices") " | GeneNetwork and this website runs fully on free software. See status and download the "
           (a (@ (href "https://ci.genenetwork.org/"))
              "source code") ".")))
diff --git a/web/view/brand/msk.scm b/web/view/brand/msk.scm
index 69c1253..4cbcec4 100644
--- a/web/view/brand/msk.scm
+++ b/web/view/brand/msk.scm
@@ -51,7 +51,7 @@
         (p ,info)
         (footer
        (hr)
-       (p "Copyright © 2005-2023 "
+       (p "Copyright © 2005-2025 "
           (a (@ (href "https://genenetwork.org/")) "GeneNetwork Webservices") " | GeneNetwork and this website runs fully on free software. See status and download the "
           (a (@ (href "https://ci.genenetwork.org/"))
              "source code") ".")))
diff --git a/web/view/doc.scm b/web/view/doc.scm
index 71112eb..cec4400 100644
--- a/web/view/doc.scm
+++ b/web/view/doc.scm
@@ -44,7 +44,7 @@
            ,(scm->json-string body #:pretty #t))
            ; (p ,(parse-html "<b>some raw really <i>text</i> here</b>"))
            (footer
-            (p "Copyright © 2005—2023 by the GeneNetwork community with a touch of " (span (@ (class "lambda")) "λ") "!")
+            (p "Copyright © 2005—2025 by the GeneNetwork community with a touch of " (span (@ (class "lambda")) "λ") "!")
             (p "This is free software. Download the "
                (a (@ (href "https://ci.genenetwork.org/"))
                   "source code") "."))
diff --git a/web/view/markdown.scm b/web/view/markdown.scm
index 653596f..6aa2935 100644
--- a/web/view/markdown.scm
+++ b/web/view/markdown.scm
@@ -15,9 +15,8 @@
   #:use-module (web request)
   #:use-module (web sxml)
   #:use-module (commonmark)
-
   #:export (markdown-file->sxml markdown-github->sxml fetch-file
-                                fetch-raw-file commit-file))
+                                fetch-raw-file commit-file git-invoke))
 
 (define (markdown-file->sxml fn)
   "Parse a local file"
@@ -26,26 +25,26 @@
 
 (define (fetch-raw-file url)
   (receive (response-status response-body)
-           (http-request url) response-body))
-
-;; https://github.com/genenetwork/gn-docs/master/general/brand/aging/home.md
-;; https://raw.githubusercontent.com/genenetwork/gn-docs/master/general/brand/aging/home.md
-;; https://github.com/genenetwork/gn-docs/edit/master/general/brand/aging/home.md
+      (http-request url) response-body))
 
-(define (form-github-raw-url project repo page)
+(define* (form-github-raw-url project repo page #:optional (branch "master"))
   (string-append "https://raw.githubusercontent.com/"
                  project
                  "/"
                  repo
-                 "/master/"
+                 "/"
+		 branch
+		 "/"
                  (string-join page "/")))
 
-(define (form-github-edit-url project repo page)
+(define* (form-github-edit-url project repo page #:optional (branch "master"))
   (string-append "https://github.com/"
                  project
                  "/"
                  repo
-                 "/edit/master/"
+		 "/edit/"
+		 branch
+		 "/"
                  (string-join page "/")))
 
 (define (markdown-github->sxml path)
@@ -120,12 +119,12 @@
            (if (zero? git-commit-file)
                `(("status" . "201")
                  ("message" . "committed file successfully")
-                 ("content" unquote content)
-                 ("commit_sha" unquote git-commit-sha)
-                 ("commit_message" unquote commit-message))
+                 ("content" . ,content)
+                 ("commit_sha" . ,git-commit-sha)
+                 ("commit_message" . ,commit-message))
                `(("status" . "200")
                  ("message" . "Nothing to commit, working tree clean")
-                 ("commit_sha" unquote git-commit-sha)))))
+                 ("commit_sha" . ,git-commit-sha)))))
         (#f (throw 'system-error
                    (format #f "~a File does not exist error" file-path))))
       (throw 'system-error
diff --git a/web/view/view.scm b/web/view/view.scm
index 4584cf8..95ff0a1 100644
--- a/web/view/view.scm
+++ b/web/view/view.scm
@@ -10,11 +10,12 @@
   #:use-module (web view markdown)
   #:use-module (web view brand msk)
   #:use-module (web view brand aging)
+  #:use-module (web templates genenetwork)
 
   #:export (view-brand))
 
 
-(define (view-aging)
+(define (view-aging-home)
   (aging-html #:info
               `(
 		,(markdown-github->sxml "genenetwork/gn-docs/general/brand/aging/home.md")
@@ -44,7 +45,14 @@ data to benefit from the power of integrated datasets, please contact:")
 
 (define* (view-brand path)
   (match path
-    ("aging" (view-aging))
+    ("aging/UMHET-3" (aging-html #:info
+              `(,(markdown-github->sxml "genenetwork/gn-docs/general/brand/aging/home.md"))))
+    ("aging/umhet-3" (aging-html #:info
+              `(,(markdown-github->sxml "genenetwork/gn-docs/general/brand/aging/home.md"))))
+    ("aging" (view-aging-home))
+    ("gnqa" (default-gn-template
+                   "genenetwork/gn-docs/general/brand/gnqa/gnqa.md"
+                   "GeneNetwork Question and Answer System"))
     ( _ (msk-html #:info
             `(
 	      ,(markdown-github->sxml "genenetwork/gn-docs/general/brand/msk/home.md")
diff --git a/web/webserver.scm b/web/webserver.scm
index 145f192..0c0bdd1 100644
--- a/web/webserver.scm
+++ b/web/webserver.scm
@@ -7,6 +7,7 @@
              (ice-9 exceptions)
              (srfi srfi-1)
              (srfi srfi-11)
+             (srfi srfi-13)
              (srfi srfi-19)
              (srfi srfi-26)
              (rnrs io ports)
@@ -16,33 +17,45 @@
              (web request)
              (web response)
              (web uri)
-             (fibers web server)
+             (web server)
              (gn cache memoize)
              (web gn-uri)
              (gn db sparql)
+             (gn data dataset)
              (gn data species)
              (gn data group)
+             (gn runner gemma)
              (web sxml)
              (web view view)
              (web view doc)
              (web view markdown))
 
+(define (get-extension filename)
+  (let ((dot-pos (string-rindex filename #\.)))
+    (if dot-pos
+        (substring filename dot-pos) "")))
+
+(define +current-repo-path+
+  (getenv "CURRENT_REPO_PATH"))
+
+(define +cgit-repo-path+
+  (getenv "CGIT_REPO_PATH"))
+
 (define +info+
-  `(("name" . "GeneNetwork REST API") ("version" unquote get-version)
+  `(("name" . "GeneNetwork REST API") ("version" . ,get-version)
     ("comment" . "This is the official REST API for the GeneNetwork service hosted at https://genenetwork.org/")
     ("license"
      ("source code (unless otherwise specified)" . "Affero GNU Public License 3.0 (AGPL3)")
      ("data (unless otherwise specified)" . "Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0)"))
     ("note" . "This is work in progress (WIP). Note that the final base URL will change! The temporary prefix is:")
-    ("prefix" ,(prefix))
-    ("links" ("species" ,(mk-meta "species")))))
+    ("prefix" . ,(prefix))
+    ("links" ("species" . ,(mk-meta "species")))))
 
 (define +info-meta+
-  `(("doc" unquote
-     (mk-html "info"))
-    ("API" ((unquote (mk-url "species")) . "Get a list of all species")
-     ((unquote (mk-url "mouse")) . "Get information on mouse")
-     ((unquote (mk-url "datasets")) . "Get a list of datasets"))))
+  `(("doc" ,(mk-html "info"))
+    ("API" ((,(mk-url "species")) . "Get a list of all species")
+     ((,(mk-url "mouse")) . "Get information on mouse")
+     ((,(mk-url "datasets")) . "Get a list of datasets"))))
 
 (define (get-id-data id)
   "Get data based on identifier id. If it is a taxon return the taxon data,
@@ -51,6 +64,20 @@ otherwise search for set/group data"
     (if taxoninfo taxoninfo
         (cdr (get-group-data id)))))
 
+(define (get-bxd-publish)
+  "Return a list of published datasets by their record ID. We add the dataset ID and phenotype ID for quick reference"
+  (list->vector (get-bxd-publish-list)))
+
+(define* (get-bxd-publish-dataid-values dataid #:optional used-for-mapping?)
+  (get-bxd-publish-dataid-name-value-dict dataid used-for-mapping?))
+
+(define* (get-bxd-publish-values dataid #:optional used-for-mapping?)
+  (get-bxd-publish-name-value-dict dataid used-for-mapping?))
+
+(define (get-gene-aliases genename)
+  "Return a vector of aliases for genename."
+  (list->vector (memo-sparql-wd-gene-aliases (memo-sparql-wd-geneids genename))))
+
 (define (not-found2 request)
   (values (build-response #:code 404)
           (string-append "Resource X not found: "
@@ -74,7 +101,7 @@ otherwise search for set/group data"
     ("html" text/html)))
 
 (define (file-extension file-name)
-  (last (string-split file-name #\.)))
+  (last (string-split file-name #\.))) ;; FIXME: does not handle files with multiple dots
 
 (define* (render-static-image file-name
                               #:key (extra-headers '()))
@@ -82,11 +109,9 @@ otherwise search for set/group data"
          (modified (and stat
                         (make-time time-utc 0
                                    (stat:mtime stat)))))
-    (list `((content-type unquote
-                          (assoc-ref file-mime-types
-                                     (file-extension file-name)))
-            (last-modified unquote
-                           (time-utc->date modified)))
+    (list `((content-type . ,(assoc-ref file-mime-types
+				        (file-extension file-name)))
+            (last-modified . ,(time-utc->date modified)))
           (call-with-input-file file-name
             get-bytevector-all))))
 
@@ -97,11 +122,9 @@ otherwise search for set/group data"
          (modified (and stat
                         (make-time time-utc 0
                                    (stat:mtime stat)))))
-    (list `((content-type unquote
-                          (assoc-ref file-mime-types
-                                     (file-extension path)))
-            (last-modified unquote
-                           (time-utc->date modified)))
+    (list `((content-type . ,(assoc-ref file-mime-types
+				        (file-extension path)))
+            (last-modified . ,(time-utc->date modified)))
           (call-with-input-file path
             get-bytevector-all))))
 
@@ -122,6 +145,11 @@ otherwise search for set/group data"
         (lambda (port)
           (sxml->html (view-brand path) port))))
 
+(define (render-string str)
+  (list '((content-type application/txt))
+        (lambda (port)
+          (put-string port str))))
+
 (define (render-json json)
   (list '((content-type application/json))
         (lambda (port)
@@ -171,11 +199,8 @@ otherwise search for set/group data"
          (lambda (key . args)
            (let ((msg (car args)))
              (build-json-response 400
-                                  `(("error" unquote key)
-                                    ("msg" unquote msg)))))))
-
-(define +global-repo+
-  (getenv "REPO_PATH"))
+                                  `(("error" . ,key)
+                                    ("msg" . ,msg)))))))
 
 (define (invalid-data? data target)
   (if (string? (assoc-ref data target))
@@ -190,30 +215,34 @@ otherwise search for set/group data"
 (define (commit-file-handler repo request body)
   (catch 'system-error
          (lambda ()
-           (let* ((post-data (decode-request-json body))
-                  (_ (for-each (lambda (target)
-                                 (invalid-data? post-data target))
-                               '("filename" "content" "username" "email"
-                                 "prev_commit")))
-                  (file-name (assoc-ref post-data "filename"))
-                  (content (assoc-ref post-data "content"))
-                  (username (assoc-ref post-data "username"))
-                  (email (assoc-ref post-data "email"))
-                  (commit-message (assoc-ref post-data "commit_message"))
-                  (prev-commit (assoc-ref post-data "prev_commit")))
-             (build-json-response 200
-                                  (commit-file repo
-                                               file-name
-                                               content
-                                               commit-message
-                                               username
-                                               email
-                                               prev-commit))))
+	   (let* ((post-data (decode-request-json body))
+		  (_ (for-each (lambda (target)
+				 (invalid-data? post-data target))
+			       '("filename" "content" "username" "email"
+				 "prev_commit")))
+		  (file-name (assoc-ref post-data "filename"))
+		  (content (assoc-ref post-data "content"))
+		  (username (assoc-ref post-data "username"))
+		  (email (assoc-ref post-data "email"))
+		  (commit-message (assoc-ref post-data "commit_message"))
+		  (prev-commit (assoc-ref post-data "prev_commit")))
+	     (build-json-response 200
+				  ((lambda ()
+				     (let ((message
+					    (commit-file +current-repo-path+
+							 file-name
+							 content
+							 commit-message
+							 username
+							 email
+							 prev-commit)))
+				       (git-invoke +current-repo-path+ "push" +cgit-repo-path+)
+				       message))))))
          (lambda (key . args)
            (let ((msg (car args)))
              (build-json-response 400
-                                  `(("error" unquote key)
-                                    ("msg" unquote msg)))))))
+                                  `(("error" . ,key)
+                                    ("msg" . ,msg)))))))
 
 (define (controller request body)
   (match-lambda
@@ -222,13 +251,31 @@ otherwise search for set/group data"
     (('GET "version")
      (render-json get-version))
     (('GET "css" fn)
-     (render-static-file (string-append "css/" fn)))
+     (render-static-file (string-append (dirname (current-filename)) "/css/" fn)))
     (('GET "map" fn)
-     (render-static-file (string-append "css/" fn)))
+     (render-static-file (string-append (dirname (current-filename)) "/css/" fn)))
     (('GET "static" "images" fn)
-     (render-static-image (string-append "static/images/" fn)))
+     (render-static-image (string-append (dirname (current-filename)) "/static/images/" fn)))
     (('GET "home" path)
-     (render-brand path))
+     (render-brand path)) ; branding route for /home/aging, /home/msk etc
+    (('GET "home" "aging" path)
+     (render-brand (string-append "aging/" path))) ; branding route subs of /home/aging/...
+    (('GET "dataset" "bxd-publish" "list")
+     (render-json (get-bxd-publish)))
+    (('GET "dataset" "bxd-publish" "dataid" "values" page)
+     (match (get-extension page)
+       (".json"
+        (render-json (get-bxd-publish-dataid-values (basename page ".json"))))
+       (else    (display "ERROR: unknown file extension"))))
+    (('GET "dataset" "bxd-publish" "values" page)
+     (match (get-extension page)
+       (".json"
+        (render-json (get-bxd-publish-values (basename page ".json"))))
+       ;; (".tsv"  (render-string "TEST1\nTEST2"))
+       ;; (".gemma" (render-string (string-join (gemma-pheno-txt "BXD" (get-bxd-publish-values (basename page ".gemma"))) "")))
+       (else    (display "ERROR: unknown file extension"))))
+    (('GET "dataset" "bxd-publish" "mapping" "values" (string-append dataid ".json"))
+     (render-json (get-bxd-publish-values dataid #t)))
     (('GET "doc" "species.html")
      (render-doc "doc" "species.html"
                  (get-species-meta)))
@@ -245,6 +292,8 @@ otherwise search for set/group data"
     (('GET "doc" path ... page)
      ;; serve documents from /doc/
      (render-doc path page))
+    (('GET "gene" "aliases" genename)
+     (render-json (get-gene-aliases genename)))
     (('GET "species.json")
      (render-json (get-species-data)))
     (('GET "species.meta.json")
@@ -252,9 +301,9 @@ otherwise search for set/group data"
     (('GET "species")
      (render-json (get-species-meta)))
     (('GET "edit")
-     (edit-file-handler +global-repo+ request))
+     (edit-file-handler +current-repo-path+ request))
     (('POST "commit")
-     (commit-file-handler +global-repo+ request body))
+     (commit-file-handler +current-repo-path+ request body))
     (('GET id)
      (let ((names (get-species-shortnames (get-expanded-species))))
        (match (string->list id)
@@ -301,8 +350,9 @@ otherwise search for set/group data"
   ;; only way to update the handler reference held by the web server
   ;; would be to restart the web server.
   (run-server (cut handler <> <>)
-              #:addr (inet-pton AF_INET address)
-              #:port port))
+              'http
+              (list #:addr (inet-pton AF_INET address)
+                    #:port port)))
 
 (define (main args)
   (write (string-append "Starting Guile REST API " get-version " server!"))